New relay-run/ package boots the REAL term-relay relay-node between a browser WSS
listener and an agent mTLS listener, delegating every authz verdict to the real
relay-auth onUpgrade (Origin/CSWSH + capability verify + DPoP PoP + single-use jti),
with the real P4 E2E crypto. No audited package modified.
- P1 (done): in-process integration test round-trips a sealed payload both ways
through the real createRelayNode splice + real createMuxSession; INV2 asserted
(relay sees only ciphertext); negative controls (foreign Origin / missing DPoP -> 401).
- P2 (boots): main.ts brings up self-signed TLS + 3 listeners + in-memory control-plane.
- P3 (not landed): no agent dial / node-pty / relay-web serve yet.
- 4 tests green, tsc clean. node_modules via symlinks (gitignored).
Two real seam drifts surfaced (documented in PLAN_RELAY_RUN_PHASE0, not hacked):
(1) term-relay authz-port DpopContext shape vs relay-auth — reconciled (server-derived htu/htm);
(2) control-plane CapabilityVerifier.verify is sync but relay-auth verify is async — blocks
HTTP account/pairing seeding until a 1-line async widen.
78 lines
3.0 KiB
TypeScript
78 lines
3.0 KiB
TypeScript
/**
|
|
* 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<string, string> {
|
|
const out: Record<string, string> = {}
|
|
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<string>) =>
|
|
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
|
|
}
|