feat(relay): Phase1 waves A2-E — server entry, shared-store data plane, agent runtime, deploy artifacts

RELAY-PHASE1 Wave A2/B/C/D/E (12-agent workflow, all tsc-clean, 314/314 tests pass):
- A2: control-plane server.ts entry + boot/redis.ts revocation-bus wiring + start script.
- B1: relay-run shared-store EnforceDeps (relay-auth ports over the SAME Postgres+Redis as P3).
- B2: registry-backed MtlsVerifier (verifyAgentCert, fail-closed, INV14).
- B3: store-backed RouteResolver (subdomain->hostId).
- B4: Redis relay:revocations subscriber -> tunnel teardown (INV12).
- B5: main-phase1.ts production entry (public bind, real TLS, async-mTLS prefetch bridge) + staging /auth/mint.
- B6 (PARTIAL): relay-web operator login + browser DPoP; proof offered via term.dpop.<b64u> subprotocol.
- C: agent dist/cli.js build (esbuild) + runTunnel run-loop + CliDeps.
- D1: same-origin static serve of relay-web/public from the browser WSS.
- E: systemd units + gen-ca/gen-capability-key/issue-tls-cert scripts + deploy/RUNBOOK.md.
Adversarial review: all hard invariants PASS. Follow-ups (B7): close DPoP-subprotocol read on
browser-server (blocks browser connect); rate-limit /auth/mint (F1); wire activeSessionCount (F2);
scrub error logs (F5). Excludes unrelated public/style.css (concurrent iOS job).
This commit is contained in:
Yaojia Wang
2026-07-06 16:13:34 +02:00
parent 95b9cccf07
commit aa1912b962
40 changed files with 4783 additions and 19 deletions

View File

@@ -14,6 +14,7 @@
*/
import { APP_SUBPROTOCOL, encodeTokenSubprotocol } from 'relay-contracts'
import type { RelayWebConfig } from './config'
import { encodeDpopSubprotocol } from './dpop'
/** The seam every transport implements — plaintext in the browser, encoded by the impl. */
export interface TerminalTransport {
@@ -41,6 +42,14 @@ export type WebSocketCtor = new (url: string, protocols?: string | readonly stri
export interface PassthroughOpts {
/** v0.9+ populates this; ABSENT in v0.8 (cookie-only auth). */
readonly capabilityToken?: string
/**
* Phase-1 STAGING (B6): a DPoP proof-of-possession JWS bound to the token's `cnf.jkt`. Browsers
* can't set the `dpop` request header on a native WS upgrade, so — mirroring the §4.3 token — it
* rides an extra `term.dpop.<b64u>` subprotocol entry (see dpop.ts `encodeDpopSubprotocol`). The
* current relay ignores it (its `handleProtocols` echoes only `APP_SUBPROTOCOL`), so the handshake
* is unaffected; true DPoP enforcement needs the relay to read this entry (server-lane, VPS-gated).
*/
readonly dpopProof?: string
/** DI seam: inject a mock WebSocket constructor in tests; defaults to global `WebSocket`. */
readonly wsCtor?: WebSocketCtor
}
@@ -71,15 +80,21 @@ export function createPassthroughTransport(
): TerminalTransport {
const Ctor: WebSocketCtor = opts.wsCtor ?? (globalThis.WebSocket as unknown as WebSocketCtor)
const token = opts.capabilityToken
const dpopProof = opts.dpopProof
const hasBearer = token !== undefined || dpopProof !== undefined
const url = cfg.wsUrl('/term') // SAME-ORIGIN, scheme-following; NEVER carries a token (T4 §)
let ws: WebSocketLike | null = null
let messageCb: ((bytes: Uint8Array) => void) | null = null
let closeCb: ((reason: string) => void) | null = null
// App subprotocol FIRST; token entry (v0.9+) second — the frozen §4.3 order.
const protocols: readonly string[] =
token === undefined ? [APP_SUBPROTOCOL] : [APP_SUBPROTOCOL, encodeTokenSubprotocol(token)]
// App subprotocol FIRST; token entry (v0.9+) second; DPoP proof entry (Phase-1 B6) last — a
// bearer NEVER touches the URL/query (would leak it to proxy/access logs, history, and Referer).
const protocols: readonly string[] = [
APP_SUBPROTOCOL,
...(token === undefined ? [] : [encodeTokenSubprotocol(token)]),
...(dpopProof === undefined ? [] : [encodeDpopSubprotocol(dpopProof)]),
]
function open(): Promise<void> {
return new Promise((resolve, reject) => {
@@ -88,9 +103,10 @@ export function createPassthroughTransport(
ws = socket
socket.onopen = () => {
// Echo rule (v0.9+): with a token attached, accept ONLY the app subprotocol back — a relay
// echoing the token entry, an empty value, or a foreign one is rejected (bearer-leak guard).
if (token !== undefined && socket.protocol !== APP_SUBPROTOCOL) {
// Echo rule (v0.9+): with a bearer attached (token and/or DPoP), accept ONLY the app
// subprotocol back — a relay echoing a bearer entry, an empty value, or a foreign one is
// rejected (bearer-leak guard).
if (hasBearer && socket.protocol !== APP_SUBPROTOCOL) {
socket.close(4400, 'bad-subprotocol')
reject(new Error(`unexpected echoed subprotocol: '${socket.protocol}'`))
return