import { createReadOnlyRegistry, type ReadOnlyRegistry, } from "./read-only-registry.ts"; export type FetchCredentialsMode = "omit" | "same-origin" | "include"; export type RestProviderProfile = Readonly<{ providerId: string; baseUrl: string; allowedCredentialsModes: readonly FetchCredentialsMode[]; redirect: "error"; referrerPolicy: "no-referrer"; }>; /** * VD-23. The complete closed set of headers a credential owner may contribute. * Transport-owned headers (`accept`, `content-type`, `idempotency-key`) and * every forbidden request header are deliberately absent. */ export const CREDENTIAL_HEADER_NAMES = Object.freeze([ "authorization", "x-csrf-token", "x-tenant-context", ] as const); export type CredentialHeaderName = (typeof CREDENTIAL_HEADER_NAMES)[number]; export type RestAuthProfile = Readonly<{ authProfileId: string; transport: "ANONYMOUS" | "BEARER_HEADER" | "SAME_ORIGIN_COOKIE"; credentials: FetchCredentialsMode; allowedCredentialHeaders: readonly CredentialHeaderName[]; /** * Proof headers the transport must observe before dispatch. A missing entry * fails closed with zero `fetch()` calls rather than sending an anonymous * request under an authenticated profile. */ requiredCredentialHeaders: readonly CredentialHeaderName[]; }>; /** * LIVE-02. A read facade over a private store, never a `Map`. The executor * resolves a profile on every request, so a post-installation `clear()` would * otherwise turn every authenticated call into `UNKNOWN_AUTH_PROFILE`. */ export type InstalledRestAuthProfiles = ReadOnlyRegistry< string, RestAuthProfile >; export type RestCsrfProfile = Readonly<{ csrfProfileId: string; mode: "NONE" | "HEADER"; headerName: "x-csrf-token" | null; }>; export const REST_AUTH_PROFILES = Object.freeze({ REFERENCE_EXTERNAL_BEARER: Object.freeze({ authProfileId: "REFERENCE_EXTERNAL_BEARER", transport: "BEARER_HEADER", credentials: "omit", allowedCredentialHeaders: Object.freeze(["authorization"] as const), requiredCredentialHeaders: Object.freeze(["authorization"] as const), }), ANONYMOUS: Object.freeze({ authProfileId: "ANONYMOUS", transport: "ANONYMOUS", credentials: "omit", allowedCredentialHeaders: Object.freeze([]), requiredCredentialHeaders: Object.freeze([]), }), /** * TechLog Studio session profile. Canonical mandates a session cookie plus * `X-CSRF-TOKEN` on every mutating Studio operation; the platform already * has this combination first-class as `SAME_ORIGIN_COOKIE` credentials with * an `x-csrf-token` credential header. */ TECH_LOG_STUDIO_SESSION: Object.freeze({ authProfileId: "TECH_LOG_STUDIO_SESSION", transport: "SAME_ORIGIN_COOKIE", credentials: "include", allowedCredentialHeaders: Object.freeze(["x-csrf-token"] as const), requiredCredentialHeaders: Object.freeze(["x-csrf-token"] as const), }), } satisfies Readonly>); function isCredentialHeaderName(value: unknown): value is CredentialHeaderName { return ( typeof value === "string" && (CREDENTIAL_HEADER_NAMES as readonly string[]).includes(value) ); } function exactHeaderSet( names: unknown, label: string, ): readonly CredentialHeaderName[] { if (!Array.isArray(names)) { throw new TypeError(`REST auth profile ${label} must be an array.`); } const seen = new Set(); for (const name of names) { if (!isCredentialHeaderName(name) || seen.has(name)) { throw new TypeError(`REST auth profile ${label} is not an exact set.`); } seen.add(name); } return Object.freeze([...(names as readonly CredentialHeaderName[])]); } /** * §7.7 / VD-23. Installs the composition-owned auth profile registry once. * * The registry — not a credential collaborator — owns Fetch `credentials` and * the exact allowed/required credential-header sets. An incoherent profile is a * composition failure, never a runtime downgrade. */ export function installRestAuthProfileRegistry( profiles: Readonly> = REST_AUTH_PROFILES, ): InstalledRestAuthProfiles { const installed = new Map(); for (const [key, candidate] of Object.entries(profiles)) { if (!candidate || typeof candidate !== "object") { throw new TypeError(`REST auth profile ${key} is not an object.`); } const authProfileId = candidate.authProfileId; if ( typeof authProfileId !== "string" || authProfileId.length === 0 || authProfileId !== key ) { throw new TypeError(`REST auth profile ${key} has a mismatched identity.`); } const allowed = exactHeaderSet( candidate.allowedCredentialHeaders, "allowedCredentialHeaders", ); const required = exactHeaderSet( candidate.requiredCredentialHeaders, "requiredCredentialHeaders", ); if (!required.every((name) => allowed.includes(name))) { throw new TypeError( `REST auth profile ${key} requires a header it does not allow.`, ); } const credentials = candidate.credentials; if (!["omit", "same-origin", "include"].includes(credentials)) { throw new TypeError(`REST auth profile ${key} has invalid credentials.`); } switch (candidate.transport) { case "ANONYMOUS": if ( credentials !== "omit" || allowed.length > 0 || required.length > 0 ) { throw new TypeError( `Anonymous REST auth profile ${key} cannot carry credentials.`, ); } break; case "BEARER_HEADER": if (credentials !== "omit" || !required.includes("authorization")) { throw new TypeError( `Bearer REST auth profile ${key} must require authorization with omitted credentials.`, ); } break; case "SAME_ORIGIN_COOKIE": if (credentials === "omit" || allowed.includes("authorization")) { throw new TypeError( `Cookie REST auth profile ${key} must send ambient credentials without a bearer header.`, ); } break; default: throw new TypeError(`REST auth profile ${key} has unknown transport.`); } installed.set( authProfileId, Object.freeze({ authProfileId, transport: candidate.transport, credentials, allowedCredentialHeaders: allowed, requiredCredentialHeaders: required, }), ); } if (installed.size === 0) { throw new TypeError("REST auth profile registry cannot be empty."); } return createReadOnlyRegistry(installed); } /** The single installed registry every composition root shares. */ export const INSTALLED_REST_AUTH_PROFILES: InstalledRestAuthProfiles = installRestAuthProfileRegistry(); export const REST_CSRF_PROFILES = Object.freeze({ NO_CSRF_BEARER: Object.freeze({ csrfProfileId: "NO_CSRF_BEARER", mode: "NONE", headerName: null, }), } satisfies Readonly>); export function createRestProviderProfile( providerId: string, baseUrl: string, allowedCredentialsModes: readonly FetchCredentialsMode[] = ["omit"], ): RestProviderProfile { const parsed = new URL(baseUrl); const localHttp = parsed.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname); if ( !providerId || (parsed.protocol !== "https:" && !localHttp) || parsed.username || parsed.password || parsed.search || parsed.hash || allowedCredentialsModes.length === 0 || new Set(allowedCredentialsModes).size !== allowedCredentialsModes.length ) { throw new TypeError("Invalid REST provider profile."); } return Object.freeze({ providerId, baseUrl: parsed.href, allowedCredentialsModes: Object.freeze([...allowedCredentialsModes]), redirect: "error", referrerPolicy: "no-referrer", }); } export function resolveRestSecurityProfiles( operation: Readonly<{ method: string; auth: "none" | "external-session"; authProfileId?: string; csrfProfileId?: string; }>, provider: RestProviderProfile, authProfiles: Readonly> = REST_AUTH_PROFILES, csrfProfiles: Readonly> = REST_CSRF_PROFILES, ): Readonly<{ auth: RestAuthProfile; csrf: RestCsrfProfile }> { const auth = authProfiles[operation.authProfileId ?? ""]; const csrf = csrfProfiles[operation.csrfProfileId ?? ""]; const unsafe = !["GET", "HEAD", "OPTIONS"].includes(operation.method); if ( !auth || !csrf || !provider.allowedCredentialsModes.includes(auth.credentials) || (operation.auth === "none" && auth.transport !== "ANONYMOUS") || (operation.auth === "external-session" && auth.transport === "ANONYMOUS") || (auth.transport === "BEARER_HEADER" && csrf.mode !== "NONE") || (unsafe && auth.transport === "SAME_ORIGIN_COOKIE" && csrf.mode !== "HEADER") ) { throw new TypeError("REST security profiles are incoherent."); } return Object.freeze({ auth, csrf }); } export function validateRestProfileBindings( operations: Readonly< Record< string, Readonly<{ contractVersion?: number; operationId: string; method: string; auth: "none" | "external-session"; providerId?: string; authProfileId?: string; csrfProfileId?: string; }> > >, providerCredentialModes: Readonly< Record >, authProfiles: Readonly> = REST_AUTH_PROFILES, csrfProfiles: Readonly> = REST_CSRF_PROFILES, ): true { for (const operation of Object.values(operations)) { if (operation.contractVersion !== 2) continue; const allowed = providerCredentialModes[operation.providerId ?? ""]; if (!allowed) { throw new TypeError( `Unregistered REST provider binding: ${operation.operationId}`, ); } resolveRestSecurityProfiles( operation, Object.freeze({ providerId: operation.providerId ?? "", baseUrl: "https://contract.invalid/", allowedCredentialsModes: allowed, redirect: "error", referrerPolicy: "no-referrer", }), authProfiles, csrfProfiles, ); } return true; }