/** * 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 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 { 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 } }, } }