diff --git a/package.json b/package.json index 8d6ea5e..ed78a3d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "main": "server.js", "scripts": { "start": "node server.js", + "start:dev1": "PORT=3000 ENABLE_CHAT=true TOPIC_NAME=hypermind-dev node server.js", + "start:dev2": "PORT=3001 ENABLE_CHAT=true TOPIC_NAME=hypermind-dev node server.js", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { diff --git a/public/app.js b/public/app.js index 2523137..b269c9d 100644 --- a/public/app.js +++ b/public/app.js @@ -244,12 +244,14 @@ const updateMap = async (peers) => { const terminal = document.getElementById('terminal'); const terminalOutput = document.getElementById('terminal-output'); +const systemStatusBar = document.getElementById('system-status-bar'); const terminalInput = document.getElementById('terminal-input'); const terminalToggle = document.getElementById('terminal-toggle'); const mapContainer = document.getElementById('map-container'); const promptEl = document.querySelector('.prompt'); let myId = null; let myChatHistory = []; +let globalChatEnabled = true; terminalToggle.addEventListener('click', (e) => { e.stopPropagation(); @@ -315,17 +317,15 @@ const getColorFromId = (id) => { const appendMessage = (msg) => { const div = document.createElement('div'); - if (msg.type === 'SYSTEM') { - div.className = 'msg-system'; - div.innerText = `[SYSTEM] ${msg.content}`; - } else if (msg.type === 'CHAT') { + if (msg.type === 'CHAT') { const senderColor = getColorFromId(msg.sender); const senderName = msg.sender === myId ? 'You' : msg.sender.slice(-4); + const scopeLabel = msg.scope === 'GLOBAL' ? '[GLOBAL] ' : ''; const senderSpan = document.createElement('span'); senderSpan.className = 'msg-sender'; senderSpan.style.color = senderColor; - senderSpan.innerText = `[${senderName}]`; + senderSpan.innerText = `${scopeLabel}[${senderName}]`; const contentSpan = document.createElement('span'); contentSpan.className = 'msg-content'; @@ -341,16 +341,35 @@ const appendMessage = (msg) => { terminalInput.addEventListener('keypress', async (e) => { if (e.key === 'Enter') { - const content = terminalInput.value.trim(); + let content = terminalInput.value.trim(); if (!content) return; terminalInput.value = ''; + + if (content === '/global on') { + globalChatEnabled = true; + systemStatusBar.innerText = `[SYSTEM] Global chat messages enabled.`; + return; + } + + if (content === '/global off') { + globalChatEnabled = false; + systemStatusBar.innerText = `[SYSTEM] Global chat messages disabled.`; + return; + } + + let scope = 'LOCAL'; + if (content.startsWith('/global ')) { + scope = 'GLOBAL'; + content = content.replace(/^\/global\s+/, '').trim(); + if (!content) return; + } try { const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content }) + body: JSON.stringify({ content, scope }) }); if (res.ok) { @@ -375,7 +394,15 @@ const evtSource = new EventSource("/events"); evtSource.onmessage = (event) => { const data = JSON.parse(event.data); - if (data.type === 'CHAT' || data.type === 'SYSTEM') { + if (data.type === 'SYSTEM') { + systemStatusBar.innerText = `[SYSTEM] ${data.content}`; + return; + } + + if (data.type === 'CHAT') { + if (data.scope === 'GLOBAL' && !globalChatEnabled) { + return; + } appendMessage(data); return; } diff --git a/public/index.html b/public/index.html index 3be5eb3..00d4feb 100644 --- a/public/index.html +++ b/public/index.html @@ -77,10 +77,11 @@
diff --git a/public/style.css b/public/style.css index 4193dc7..72cbe7a 100644 --- a/public/style.css +++ b/public/style.css @@ -28,26 +28,16 @@ body.chat-collapsed { .footer { margin: 2rem auto 0; font-size: 0.9rem; - color: #4b5563; - background: rgba(0, 0, 0, 0.6); - padding: 2px 8px; - border-radius: 4px; - display: block; - width: fit-content; + color: #9ca3af; } .debug { font-size: 0.8rem; - color: #4b5563; + color: #9ca3af; margin: 1rem auto 0; - background: rgba(0, 0, 0, 0.6); - padding: 5px 10px; - border-radius: 4px; - display: block; - width: fit-content; } -.debug-link { color: #4b5563; border-bottom: 1px dotted #4b5563; cursor: pointer; } -.debug-link:hover { color: #9ca3af; } -a { color: #4b5563; text-decoration: none; border-bottom: 1px dotted #4b5563; } +.debug-link { color: #9ca3af; border-bottom: 1px dotted #9ca3af; cursor: pointer; } +.debug-link:hover { color: #e5e7eb; } +a { color: #9ca3af; text-decoration: none; border-bottom: 1px dotted #9ca3af; } .pulse { animation: pulse 0.5s ease-in-out; } @keyframes pulse { @@ -195,6 +185,17 @@ a { color: #4b5563; text-decoration: none; border-bottom: 1px dotted #4b5563; } background: #222; } +.system-status-bar { + height: 20px; + margin-bottom: 5px; + color: #666; + font-style: italic; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex-shrink: 0; +} + .terminal-output { flex: 1; overflow-y: auto; diff --git a/src/config/constants.js b/src/config/constants.js index aeb6be0..2f1663b 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -1,6 +1,6 @@ const crypto = require("crypto"); -const TOPIC_NAME = "hypermind-lklynet-v1"; +const TOPIC_NAME = process.env.TOPIC_NAME || "hypermind-lklynet-v1"; const TOPIC = crypto.createHash("sha256").update(TOPIC_NAME).digest(); /** diff --git a/src/p2p/messaging.js b/src/p2p/messaging.js index f901ee4..5597b71 100644 --- a/src/p2p/messaging.js +++ b/src/p2p/messaging.js @@ -147,29 +147,60 @@ class MessageHandler { } handleChat(msg, sourceSocket) { - // Identity Verification: Ensure the sender matches the authenticated socket - if (!sourceSocket.peerId || sourceSocket.peerId !== msg.sender) { - return; - } + const { scope, sender, id, sig, hops } = msg; - // Rate Limiting: Prevent flooding (5 messages per 10 seconds per peer) + // Rate Limiting (apply to all chat messages) const now = Date.now(); - let rateData = this.chatRateLimits.get(msg.sender); - + let rateData = this.chatRateLimits.get(sender); + if (!rateData || now - rateData.windowStart > 10000) { - // Reset window - rateData = { count: 0, windowStart: now }; + // Reset window + rateData = { count: 0, windowStart: now }; } if (rateData.count >= 5) { - return; // Drop message + return; // Drop message } - rateData.count++; - this.chatRateLimits.set(msg.sender, rateData); + if (!scope || scope === 'LOCAL') { + // Identity Verification: Ensure the sender matches the authenticated socket + if (!sourceSocket.peerId || sourceSocket.peerId !== sender) { + return; + } - if (this.chatCallback) { - this.chatCallback(msg); + rateData.count++; + this.chatRateLimits.set(sender, rateData); + + if (this.chatCallback) { + this.chatCallback(msg); + } + } else if (scope === 'GLOBAL') { + if (!sig || !id) return; + + // Check signature + const key = createPublicKey(sender); + if (!verifySignature(`chat:${id}`, sig, key)) { + this.diagnostics.increment("invalidSig"); + return; + } + + // Deduplication + if (this.bloomFilter.hasRelayed(id, "chat")) { + return; + } + this.bloomFilter.markRelayed(id, "chat"); + + rateData.count++; + this.chatRateLimits.set(sender, rateData); + + if (this.chatCallback) { + this.chatCallback(msg); + } + + // Relay + if (hops < MAX_RELAY_HOPS) { + this.relayCallback({ ...msg, hops: hops + 1 }, sourceSocket); + } } } } @@ -206,16 +237,12 @@ const validateMessage = (msg) => { } if (msg.type === "CHAT") { - const allowedFields = ["type", "sender", "content", "timestamp"]; + const allowedFields = ['type', 'sender', 'content', 'timestamp', 'scope', 'id', 'sig', 'hops']; const fields = Object.keys(msg); - return ( - fields.every((f) => allowedFields.includes(f)) && - msg.sender && - msg.content && - typeof msg.content === "string" && - msg.content.length <= 140 && - typeof msg.timestamp === "number" - ); + return fields.every(f => allowedFields.includes(f)) && + msg.sender && + msg.content && typeof msg.content === 'string' && msg.content.length <= 140 && + typeof msg.timestamp === 'number'; } return false; diff --git a/src/web/routes.js b/src/web/routes.js index 8d4a077..7258c2b 100644 --- a/src/web/routes.js +++ b/src/web/routes.js @@ -1,6 +1,8 @@ const express = require("express"); const fs = require("fs"); const path = require("path"); +const crypto = require("crypto"); +const { signMessage } = require("../core/security"); const { ENABLE_CHAT, ENABLE_MAP, CHAT_RATE_LIMIT, VISUAL_LIMIT } = require("../config/constants"); const HTML_TEMPLATE = fs.readFileSync( @@ -78,18 +80,34 @@ const setupRoutes = (app, identity, peerManager, swarm, sseManager, diagnostics) chatHistory.push(now); - const { content } = req.body; + const { content, scope = 'LOCAL' } = req.body; if (!content || typeof content !== 'string' || content.length > 140) { return res.status(400).json({ error: "Invalid content" }); } + + if (scope !== 'LOCAL' && scope !== 'GLOBAL') { + return res.status(400).json({ error: "Invalid scope" }); + } + + const timestamp = Date.now(); + // Create a unique ID that depends on content to prevent replay/duplicates + const idBase = identity.id + content + timestamp; + const msgId = crypto.createHash('sha256').update(idBase).digest('hex'); const msg = { type: "CHAT", + id: msgId, sender: identity.id, content: content, - timestamp: Date.now() + timestamp: timestamp, + scope: scope, + hops: 0 }; + if (scope === 'GLOBAL') { + msg.sig = signMessage(`chat:${msgId}`, identity.privateKey); + } + swarm.broadcastChat(msg); sseManager.broadcast(msg);