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.
This commit is contained in:
Yaojia Wang
2026-07-04 14:13:04 +02:00
parent 3d17bed322
commit ba85871227
17 changed files with 1239 additions and 0 deletions

View File

@@ -46,6 +46,33 @@ Postgres/Redis (in-memory only), real KMS (dev in-process signer), wildcard TLS/
routing, horizontal scaling, F6 replay-epoch transport (preview stays inert), metering. Those are
Phase 1/2 in DEPLOY_RELAY.md.
## Build outcome (2026-07-04) + seam drifts found
Built as `relay-run/`. **P1 achieved + verified** (4 tests green, tsc clean): an in-process
integration test round-trips a sealed payload both directions through the REAL `createRelayNode`
splice + REAL `createMuxSession`, authorized by the REAL relay-auth `onUpgrade` (Origin/CSWSH +
capability verify + DPoP PoP + single-use jti) with REAL P4 E2E; **INV2 asserted** (relay sees only
ciphertext); negative controls (foreign Origin → 401, missing DPoP → 401). **P2 achieved** (`npm start`
boots self-signed TLS + browser WSS `:8443` + agent mTLS `:8444` + control-plane `:8080`, prints a
minted capability token). **P3 not landed** (no agent dial / node-pty / relay-web serve). No audited
package modified.
**Two real integration blockers surfaced by actually wiring it (reported, not hacked):**
1. **DpopContext shape drift** — term-relay's `authz-port` `DpopContext` is `{proof, publicKeyThumbprint}`
but relay-auth needs `{proofJws, htu, htm}`. Reconciled in `relay-run/src/wiring/authorizer.ts` by
deriving `htu`/`htm` from the relay's own authority (`expectedAud`) — spec-faithful (RFC 9449), the
DPoP binding stays fully enforced. Consider aligning the port type upstream.
2. **control-plane `CapabilityVerifier.verify` is SYNC, relay-auth `verifyCapabilityToken` is ASYNC**
(WebCrypto). The real P5 verifier therefore cannot be injected into the control-plane admin routes
**programmatic account+pairing seeding is blocked in Phase 0** (control-plane boots on its default
fail-closed verifier). Fix: widen `CapabilityVerifier.verify` to return `Promise<CapabilityToken>`
(1-line additive in `control-plane/src/api/authz.ts`), then inject the real async verifier.
**Next steps to a full browser click-through:** (a) the async-verify widen above; (b) a browser-usable
DPoP carrier on the WS upgrade (subprotocol/cookie/preflight — browsers can't set a `dpop` header);
(c) run the agent (enroll → mTLS dial → E2E host endpoint → node-pty); (d) swap the Phase-0
`mtls.verifyPeer` stub for the registry-gated `verifyAgentCert` and serve the built `relay-web` bundle.
## Security notes (keep even in dev)
- Real Origin/CSWSH check on the WSS upgrade (dont stub it off for localhost — set allowedOrigins to
the dev origin). Real capability-token verify + DPoP (no bypass). Self-signed certs are dev-only;