const crypto = require('crypto'); const http = require('http'); const fs = require('fs'); const path = require('path'); // ========== WebSocket Protocol (RFC 6455) ========== const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A }; const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; const MAX_FRAME_PAYLOAD_BYTES = 10 * 1024 * 1024; function computeAcceptKey(clientKey) { return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64'); } function encodeFrame(opcode, payload) { const fin = 0x80; const len = payload.length; let header; if (len < 126) { header = Buffer.alloc(2); header[0] = fin | opcode; header[1] = len; } else if (len < 65536) { header = Buffer.alloc(4); header[0] = fin | opcode; header[1] = 126; header.writeUInt16BE(len, 2); } else { header = Buffer.alloc(10); header[0] = fin | opcode; header[1] = 127; header.writeBigUInt64BE(BigInt(len), 2); } return Buffer.concat([header, payload]); } function decodeFrame(buffer) { if (buffer.length < 2) return null; const secondByte = buffer[1]; const opcode = buffer[0] & 0x0F; const masked = (secondByte & 0x80) !== 0; let payloadLen = secondByte & 0x7F; let offset = 2; if (!masked) throw new Error('Client frames must be masked'); if (payloadLen === 126) { if (buffer.length < 4) return null; payloadLen = buffer.readUInt16BE(2); offset = 4; } else if (payloadLen === 127) { if (buffer.length < 10) return null; const extendedLen = buffer.readBigUInt64BE(2); if (extendedLen > BigInt(MAX_FRAME_PAYLOAD_BYTES)) { throw new Error('WebSocket frame payload exceeds maximum allowed size'); } payloadLen = Number(extendedLen); offset = 10; } if (payloadLen > MAX_FRAME_PAYLOAD_BYTES) { throw new Error('WebSocket frame payload exceeds maximum allowed size'); } const maskOffset = offset; const dataOffset = offset + 4; const totalLen = dataOffset + payloadLen; if (buffer.length < totalLen) return null; const mask = buffer.slice(maskOffset, dataOffset); const data = Buffer.alloc(payloadLen); for (let i = 0; i < payloadLen; i++) { data[i] = buffer[dataOffset + i] ^ mask[i % 4]; } return { opcode, payload: data, bytesConsumed: totalLen }; } // ========== Configuration ========== const PORT_FILE = process.env.BRAINSTORM_PORT_FILE || null; const randomPort = () => 49152 + Math.floor(Math.random() * 16383); // Prefer an explicit port, else the port this session last bound (so a restart // reuses it and an already-open browser tab reconnects), else a random high port. function preferredPort() { if (process.env.BRAINSTORM_PORT) return Number(process.env.BRAINSTORM_PORT); if (PORT_FILE) { try { const p = Number(fs.readFileSync(PORT_FILE, 'utf-8').trim()); if (Number.isInteger(p) && p > 1023 && p < 65536) return p; } catch (e) { /* no prior port recorded */ } } return randomPort(); } let PORT = preferredPort(); const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1'; const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST); const SESSION_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm'; const CONTENT_DIR = path.join(SESSION_DIR, 'content'); const STATE_DIR = path.join(SESSION_DIR, 'state'); let ownerPid = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null; // Per-session secret key. The companion is reachable by any local browser tab // and, when bound to a non-loopback host, by any host that can route to it. // The key authenticates the real client uniformly across loopback, tunnel, and // remote binds — and defeats DNS rebinding — where a Host/Origin allowlist // cannot. It rides the served URL as ?key= and is mirrored into a cookie on // first load so same-origin subresources and the WebSocket carry it for free. const TOKEN = process.env.BRAINSTORM_TOKEN || crypto.randomBytes(32).toString('hex'); const COOKIE_NAME = 'brainstorm-key-' + PORT; const MIME_TYPES = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml' }; // ========== Templates and Constants ========== const WAITING_PAGE = `
Waiting for the agent to push a screen...
`; const FORBIDDEN_PAGE = `This page needs the full URL your coding agent gave you, including the
?key=… part. Copy the complete URL and open it again.