mirror of
https://github.com/lklynet/hypermind.git
synced 2026-05-03 09:30:36 +00:00
Merge PR #43 and resolve conflicts
This commit is contained in:
@@ -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
@@ -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
@@ -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">></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
@@ -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,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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user