/** * Base-app WebSocket protocol shapes, as the browser bundle SPEAKS them end-to-end. * * The agent (P2) forwards each logical stream to the UNCHANGED web-terminal at `127.0.0.1:3000`, * which speaks this exact JSON protocol (TECH_DOC §4 / src/protocol.ts). relay-web is a legitimate * consumer of that contract, so it declares the minimal shapes locally — it does NOT import the * base `src/` (that stays byte-for-byte untouched; verification greps `src` clean). * * These frames are the PLAINTEXT that the E2E layer (T8) later seals; in v0.8 they ride the * passthrough transport unencrypted. */ /** client → server (agent → base app). `attach` MUST be the first message. */ export type ClientMessage = | { readonly type: 'attach'; readonly sessionId: string | null; readonly cwd?: string } | { readonly type: 'input'; readonly data: string } | { readonly type: 'resize'; readonly cols: number; readonly rows: number } /** server → client. */ export type ServerMessage = | { readonly type: 'attached'; readonly sessionId: string } | { readonly type: 'output'; readonly data: string } | { readonly type: 'exit'; readonly code: number; readonly reason?: string } const encoder = new TextEncoder() const decoder = new TextDecoder() /** Encode a client message as UTF-8 JSON bytes for `TerminalTransport.send`. */ export function encodeClientMessage(msg: ClientMessage): Uint8Array { return encoder.encode(JSON.stringify(msg)) } /** * Decode UTF-8 JSON bytes into a ServerMessage. NEVER throws (mirrors base invariant #3): an * unparseable / unknown frame yields `null` so the caller drops it rather than crashing the view. */ export function decodeServerMessage(bytes: Uint8Array): ServerMessage | null { let parsed: unknown try { parsed = JSON.parse(decoder.decode(bytes)) } catch { return null } if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return null const obj = parsed as Record const type = obj['type'] if (type === 'attached' && typeof obj['sessionId'] === 'string') { return { type: 'attached', sessionId: obj['sessionId'] } } if (type === 'output' && typeof obj['data'] === 'string') { return { type: 'output', data: obj['data'] } } if (type === 'exit' && typeof obj['code'] === 'number') { const reason = obj['reason'] return typeof reason === 'string' ? { type: 'exit', code: obj['code'], reason } : { type: 'exit', code: obj['code'] } } return null }