/** * T7 · Subdomain router (§4.2 routing). `extractSubdomain` returns the SINGLE leftmost label * of `host` under `baseDomain`, or null for a bare/nested/confusing Host (INV1 Host-confusion * guard). It is a HINT for candidate lookup only — NEVER the authority: T8 requires the * capability token's `aud` to equal this subdomain and P5 gates `token.host` against the * resolver's `hostId`. `RouteResolver` is IMPLEMENTED BY P3; P1 owns only the interface. */ export interface ResolvedHost { readonly hostId: string readonly accountId: string readonly subdomain: string } export interface RouteResolver { resolveSubdomain(subdomain: string): Promise } const LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/ /** Strip a single trailing dot and lowercase; strip a `:port` suffix if present. */ function normalizeHost(host: string): string { const noPort = host.split(':', 1)[0] ?? host const trimmed = noPort.endsWith('.') ? noPort.slice(0, -1) : noPort return trimmed.toLowerCase() } /** * Extract the single tenant label: `'alice.term.example.com'` under `'term.example.com'` → `'alice'`. * Returns null when: host equals the base domain (bare), the suffix does not match, or the prefix * is multi-label (`'a.b.term.example.com'` → null — nested subdomains are rejected, INV1). */ export function extractSubdomain(hostHeader: string, baseDomain: string): string | null { const host = normalizeHost(hostHeader) const base = normalizeHost(baseDomain) if (host === base) return null // bare base domain — no tenant const suffix = '.' + base if (!host.endsWith(suffix)) return null // confusion attempt (e.g. '...example.com.evil.com') const label = host.slice(0, host.length - suffix.length) if (label.length === 0) return null if (label.includes('.')) return null // multi-label prefix rejected (single-label tenant only) if (!LABEL_RE.test(label)) return null // invalid DNS label return label }