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).
41 lines
2.1 KiB
TypeScript
41 lines
2.1 KiB
TypeScript
/**
|
|
* B3 · Store-backed RouteResolver — the PRODUCTION replacement for the one-entry in-RAM
|
|
* `memoryRouteResolver` (wiring/data-plane.ts:110). It implements term-relay's `RouteResolver`
|
|
* interface EXACTLY (`resolveSubdomain(subdomain) -> ResolvedHost | null`) by looking the tenant
|
|
* label up in the control-plane Postgres `hosts` store via `hosts.getBySubdomain(subdomain)`.
|
|
*
|
|
* Fail-closed (returns null, → 403 at the T8 upgrade edge) on:
|
|
* - unknown subdomain: `getBySubdomain` returns null.
|
|
* - revoked host: the row still exists (status is versioned, not deleted — INV8/INV12), so we
|
|
* must reject `status === 'revoked'` here; a revoked host must never resolve to a route.
|
|
*
|
|
* The resolver is only a HINT for candidate lookup (subdomain-router.ts): the returned `hostId`
|
|
* is what `authorizeUpgrade` feeds to P5 as `requestedHostId`, and P5 gates the signed token's
|
|
* `host` against it (INV1). Identity (`accountId`/`hostId`) here comes solely from the CP store —
|
|
* the ownership source of truth — never from the caller (INV3).
|
|
*/
|
|
import type { HostStore } from 'control-plane/src/store/ports.js'
|
|
import type { RouteResolver, ResolvedHost } from 'term-relay/data-plane/subdomain-router.js'
|
|
|
|
/**
|
|
* The single HostStore capability this resolver needs (interface segregation): a subdomain lookup.
|
|
* A full `createPgStores().hosts` (`HostStore`) satisfies it structurally.
|
|
*/
|
|
export type HostSubdomainLookup = Pick<HostStore, 'getBySubdomain'>
|
|
|
|
export interface StoreRouteResolverDeps {
|
|
readonly hosts: HostSubdomainLookup
|
|
}
|
|
|
|
/** Build a RouteResolver that resolves subdomain → host from the control-plane `hosts` store. */
|
|
export function createStoreRouteResolver({ hosts }: StoreRouteResolverDeps): RouteResolver {
|
|
return {
|
|
async resolveSubdomain(subdomain: string): Promise<ResolvedHost | null> {
|
|
const host = await hosts.getBySubdomain(subdomain)
|
|
if (host === null) return null // unknown subdomain — fail closed
|
|
if (host.status === 'revoked') return null // revoked host — fail closed (INV12)
|
|
return { hostId: host.hostId, accountId: host.accountId, subdomain: host.subdomain }
|
|
},
|
|
}
|
|
}
|