From 0b1481bbec5fb0a41a9b1bc21405eac4352c8615 Mon Sep 17 00:00:00 2001 From: lklynet Date: Mon, 5 Jan 2026 22:17:31 -0500 Subject: [PATCH] chore: update UI styles and include WIP global chat changes --- package.json | 2 ++ public/app.js | 43 ++++++++++++++++++++++++++------- public/index.html | 3 ++- public/style.css | 31 ++++++++++++------------ src/config/constants.js | 2 +- src/p2p/messaging.js | 53 ++++++++++++++++++++++++++++++++--------- src/web/routes.js | 22 +++++++++++++++-- 7 files changed, 118 insertions(+), 38 deletions(-) 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 3878f37..5f5ee45 100644 --- a/public/app.js +++ b/public/app.js @@ -243,11 +243,13 @@ 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 promptEl = document.querySelector('.prompt'); let myId = null; let myChatHistory = []; +let globalChatEnabled = true; terminalToggle.addEventListener('click', (e) => { e.stopPropagation(); @@ -313,17 +315,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'; @@ -339,16 +339,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) { @@ -373,7 +392,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 2aaae41..b366e01 100644 --- a/public/index.html +++ b/public/index.html @@ -77,10 +77,11 @@ diff --git a/public/style.css b/public/style.css index dae9a2c..546f551 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 { @@ -191,6 +181,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 56aa5f4..71445ce 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 6154e02..46499e4 100644 --- a/src/p2p/messaging.js +++ b/src/p2p/messaging.js @@ -133,14 +133,11 @@ 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 @@ -151,11 +148,45 @@ class MessageHandler { 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); + } } } } @@ -183,7 +214,7 @@ 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 && diff --git a/src/web/routes.js b/src/web/routes.js index 39b57a1..c712cbb 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, CHAT_RATE_LIMIT } = require("../config/constants"); const HTML_TEMPLATE = fs.readFileSync( @@ -76,18 +78,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);