/** * T1 — runtime config derived from `location` (no host/IP knob exists in the client, so the * browser can never be pointed at another tenant's origin — supports INV1 upstream). * * The tenant origin is `.term.`. `wsUrl` is ALWAYS same-origin and * scheme-following (`wss:` on HTTPS, else `ws:`) — the M6 guarantee that avoids mixed-content * blocking on TLS/Tailscale deploys. */ import { ConfigError } from './errors' /** The label that marks the tenant zone: `.term.`. */ const TENANT_ZONE_LABEL = 'term' export interface RelayWebConfig { /** left-most hostname label, e.g. 'alice' in alice.term.example.com */ readonly subdomain: string /** scheme-following, SAME-ORIGIN ws URL builder (M6); a path cannot inject a foreign host */ readonly wsUrl: (path: string) => string /** same-origin API base ('' — every control-plane call is relative) */ readonly apiBase: string } /** Parse the tenant subdomain from a hostname, or throw (fail-fast, never default to a tenant). */ function parseSubdomain(hostname: string): string { const labels = hostname.split('.') const first = labels[0] // Require the `.term.<...>` shape: a non-empty left label with `term` as the 2nd label. if (labels.length < 3 || labels[1] !== TENANT_ZONE_LABEL || first === undefined || first === '') { throw new ConfigError( `hostname '${hostname}' has no tenant subdomain (expected '.${TENANT_ZONE_LABEL}.')`, ) } return first } /** * Derive the client runtime config from `location`. Throws {@link ConfigError} on a host that is * not a valid tenant subdomain — it never silently falls back to another tenant's subdomain. */ export function readConfig(loc: Pick): RelayWebConfig { const subdomain = parseSubdomain(loc.hostname) const wsScheme = loc.protocol === 'https:' ? 'wss' : 'ws' const host = loc.host const wsUrl = (path: string): string => { // Always anchor to our own host; normalize to a leading '/' so a caller-supplied // `//evil.com` becomes a PATH on our host, never a protocol-relative foreign origin. const normalized = path.startsWith('/') ? path : `/${path}` return `${wsScheme}://${host}${normalized}` } return Object.freeze({ subdomain, wsUrl, apiBase: '' }) }