/** * Browser-facing WSS server for `main.ts`. Terminates HTTPS on the tenant origin and, on WS * upgrade, builds the P1 `UpgradeRequest` from the raw request headers and hands it to the REAL * relay-node (→ real `authorizeUpgrade` → P5 `onUpgrade`: Origin/CSWSH + capability verify + DPoP). * The handshake echoes ONLY `APP_SUBPROTOCOL` — never the token entry (INV15). */ import { readFileSync } from 'node:fs' import { createServer, type Server } from 'node:https' import type { IncomingMessage, ServerResponse } from 'node:http' import { WebSocketServer, type WebSocket as WsWebSocket } from 'ws' import { APP_SUBPROTOCOL } from 'relay-contracts' import type { UpgradeRequest } from 'term-relay/data-plane/upgrade.js' import type { RelayNode } from 'term-relay/data-plane/relay-node.js' import { wsToWebSocketLike } from '../wiring/socket-pipe.js' export interface BrowserServerOptions { readonly certPath: string readonly keyPath: string readonly bindHost: string readonly bindPort: number readonly node: RelayNode readonly landingHtml: string readonly onListening?: () => void readonly onError?: (e: unknown) => void } function parseCookies(header: string | undefined): Record { const out: Record = {} if (!header) return out for (const part of header.split(';')) { const idx = part.indexOf('=') if (idx === -1) continue out[part.slice(0, idx).trim()] = part.slice(idx + 1).trim() } return out } function buildUpgradeRequest(req: IncomingMessage): UpgradeRequest { const proto = req.headers['sec-websocket-protocol'] const subprotocols = (typeof proto === 'string' ? proto.split(',') : []) .map((v) => v.trim()) .filter((v) => v.length > 0) const dpopHeader = req.headers['dpop'] return { host: req.headers.host ?? '', origin: typeof req.headers.origin === 'string' ? req.headers.origin : undefined, url: req.url ?? '/', subprotocols, cookies: parseCookies(req.headers.cookie), remoteAddr: req.socket.remoteAddress ?? '', dpop: { proof: typeof dpopHeader === 'string' ? dpopHeader : null, publicKeyThumbprint: null }, activeSessionCount: 0, } } export function startBrowserServer(opts: BrowserServerOptions): Server { const server = createServer( { cert: readFileSync(opts.certPath), key: readFileSync(opts.keyPath) }, (_req: IncomingMessage, res: ServerResponse) => { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }) res.end(opts.landingHtml) }, ) const wss = new WebSocketServer({ server, handleProtocols: (protocols: Set) => protocols.has(APP_SUBPROTOCOL) ? APP_SUBPROTOCOL : false, }) wss.on('connection', (ws: WsWebSocket, req: IncomingMessage) => { opts.node .handleBrowserUpgrade(buildUpgradeRequest(req), wsToWebSocketLike(ws)) .catch((e) => opts.onError?.(e)) }) server.on('error', (e) => opts.onError?.(e)) server.listen(opts.bindPort, opts.bindHost, () => opts.onListening?.()) return server }