Files
web-terminal/relay-run/src/servers/control-plane-boot.ts
Yaojia Wang ba85871227 feat(relay-run): Phase 0 single-host relay — real data-plane wiring + integration test
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.
2026-07-04 14:13:04 +02:00

42 lines
1.8 KiB
TypeScript

/**
* Best-effort control-plane (P3) boot for `main.ts`: in-memory stores + the dev in-process CA
* signer, listening for admin/enroll HTTP. Wrapped so a failure degrades gracefully (the data-plane
* is the security-critical surface and boots independently).
*
* KNOWN SEAM (reported, not hacked): P3's injected `CapabilityVerifier.verify` is SYNCHRONOUS, but
* relay-auth's real `verifyCapabilityToken` is ASYNC (WebCrypto). The real P5 verifier therefore
* cannot be injected without an additive change to control-plane, so the admin routes run on the
* default fail-closed verifier here (deny-by-default, never bypassed). Programmatic account/pairing
* seeding through those routes is consequently BLOCKED in Phase 0 — see the report.
*/
import { buildControlPlane } from 'control-plane'
import { loadEnv } from 'control-plane/src/env.js'
import { createMemoryStores } from 'control-plane/src/store/memory.js'
export interface ControlPlaneBootOptions {
readonly publicRaw: Uint8Array
readonly baseDomain: string
readonly caCertPath: string
readonly bindHost: string
readonly bindPort: number
}
export interface ControlPlaneHandle {
close(): Promise<void>
}
export async function bootControlPlane(opts: ControlPlaneBootOptions): Promise<ControlPlaneHandle> {
const env = loadEnv({
PG_URL: 'postgres://localhost:5432/relay_run_dev',
REDIS_URL: 'redis://localhost:6379',
CAPABILITY_SIGN_PUBKEY_B64: Buffer.from(opts.publicRaw).toString('base64'),
CA_INTERMEDIATE_KMS_KEY_REF: 'dev-inprocess-signer',
CA_INTERMEDIATE_CERT_PATH: opts.caCertPath,
NODE_MTLS_TRUST_BUNDLE_PATH: opts.caCertPath,
BASE_DOMAIN: opts.baseDomain,
})
const { app } = await buildControlPlane(env, { stores: createMemoryStores() })
await app.listen({ port: opts.bindPort, host: opts.bindHost })
return { close: () => app.close() }
}