Merge PR #43 and resolve conflicts

This commit is contained in:
lklynet
2026-01-06 19:19:04 -05:00
7 changed files with 126 additions and 50 deletions
+2
View File
@@ -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": {
+35 -8
View File
@@ -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;
}
+2 -1
View File
@@ -77,10 +77,11 @@
<div id="terminal" class="terminal hidden">
<button id="terminal-toggle" class="terminal-toggle" title="Toggle Chat"></button>
<div id="system-status-bar" class="system-status-bar"></div>
<div id="terminal-output" class="terminal-output"></div>
<div class="terminal-input-line">
<span class="prompt">&gt;</span>
<input type="text" id="terminal-input" maxlength="140" placeholder="Broadcast to direct peers..." autocomplete="off">
<input type="text" id="terminal-input" maxlength="140" placeholder="Broadcast..." autocomplete="off">
</div>
</div>
+16 -15
View File
@@ -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;
+1 -1
View File
@@ -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();
/**
+50 -23
View File
@@ -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;
+20 -2
View File
@@ -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);