export type IdentityEnvironment = "development" | "production"; export type IdentityConfigErrorCode = | "BASE_URL_INSECURE_IN_PRODUCTION" | "BASE_URL_INVALID" | "BASE_URL_LOOPBACK_IN_PRODUCTION" | "BASE_URL_MISSING" | "PUBLIC_SIGNUP_ENABLED_INVALID" | "PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION" | "SECRET_LOW_ENTROPY" | "SECRET_MISSING" | "SECRET_TOO_SHORT" | "TRUSTED_ORIGIN_INSECURE_IN_PRODUCTION" | "TRUSTED_ORIGIN_INVALID" | "TRUSTED_ORIGIN_LOOPBACK_IN_PRODUCTION" | "TRUSTED_ORIGIN_WILDCARD_UNSUPPORTED" | "TRUSTED_ORIGINS_EMPTY" | "TRUSTED_ORIGINS_MISSING"; const MIN_SECRET_LENGTH = 32; const LOOPBACK_HOSTNAMES: ReadonlySet = new Set([ "localhost", "127.0.0.1", "::1", "0.0.0.0", ]); const errorMessages: Readonly> = Object.freeze({ BASE_URL_INSECURE_IN_PRODUCTION: "The identity base URL must use https in production.", BASE_URL_INVALID: "The identity base URL must be an absolute http or https URL.", BASE_URL_LOOPBACK_IN_PRODUCTION: "The identity base URL must not resolve to a loopback host in production.", BASE_URL_MISSING: "The identity base URL is required.", PUBLIC_SIGNUP_ENABLED_INVALID: 'Public signup enablement must be an explicit boolean or the exact string "true"/"false".', PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION: "Public signup cannot be enabled in production until durable signup-abuse controls exist.", SECRET_LOW_ENTROPY: "The identity secret does not have sufficient entropy.", SECRET_MISSING: "The identity secret is required.", SECRET_TOO_SHORT: `The identity secret must be at least ${MIN_SECRET_LENGTH} characters.`, TRUSTED_ORIGIN_INSECURE_IN_PRODUCTION: "Trusted origins must use https in production.", TRUSTED_ORIGIN_INVALID: "Every trusted origin must be an absolute http or https origin with no path, query, or fragment.", TRUSTED_ORIGIN_LOOPBACK_IN_PRODUCTION: "Trusted origins must not resolve to a loopback host in production.", TRUSTED_ORIGIN_WILDCARD_UNSUPPORTED: "Wildcard trusted origins are not supported in this release.", TRUSTED_ORIGINS_EMPTY: "At least one trusted origin is required.", TRUSTED_ORIGINS_MISSING: "Trusted origins are required.", }); /** * Never carries the offending secret, URL, origin, hostname, or any other caller-supplied value. * Only a stable code and a fixed, generic message are ever exposed. */ export class IdentityConfigError extends Error { readonly code: IdentityConfigErrorCode; constructor(code: IdentityConfigErrorCode) { super(errorMessages[code]); this.name = "IdentityConfigError"; this.code = code; this.stack = `${this.name}: ${this.message}`; Object.freeze(this); } } export interface IdentityConfig { readonly baseUrl: string; /** Whether `POST /api/auth/sign-up/email` accepts new accounts. Defaults to `false` (disabled) — * public signup never becomes reachable merely because this input was omitted, misspelled, or * loosely parsed. Development and test composition may enable it explicitly; production * currently cannot enable it at all (`PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION`), because the * durable signup-abuse prerequisites do not exist yet — see `validatePublicSignupEnabled`. In * production this is therefore always `false`. */ readonly publicSignupEnabled: boolean; readonly secret: string; readonly trustedOrigins: readonly string[]; } export interface IdentityConfigInput { readonly baseUrl?: unknown; readonly publicSignupEnabled?: unknown; readonly secret?: unknown; readonly trustedOrigins?: unknown; } export interface CreateIdentityConfigOptions { /** Defaults to "production" so an omitted environment never silently relaxes validation. */ readonly environment?: IdentityEnvironment; } function isLoopbackHostname(hostname: string): boolean { return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase()); } function parseAbsoluteHttpUrl(value: unknown): URL | undefined { if (typeof value !== "string" || value.length === 0) { return undefined; } try { const url = new URL(value); return url.protocol === "http:" || url.protocol === "https:" ? url : undefined; } catch { return undefined; } } function validateSecret(secret: unknown): string { if (typeof secret !== "string" || secret.length === 0) { throw new IdentityConfigError("SECRET_MISSING"); } if (secret.length < MIN_SECRET_LENGTH) { throw new IdentityConfigError("SECRET_TOO_SHORT"); } // A narrow, deterministic guard against trivially degenerate secrets (e.g. one repeated // character). It cannot measure true entropy; operators remain responsible for generating a // high-entropy value (for example `openssl rand -base64 32`). if (/^(.)\1*$/u.test(secret)) { throw new IdentityConfigError("SECRET_LOW_ENTROPY"); } return secret; } function validateBaseUrl(baseUrl: unknown, environment: IdentityEnvironment): string { if (typeof baseUrl !== "string" || baseUrl.length === 0) { throw new IdentityConfigError("BASE_URL_MISSING"); } const url = parseAbsoluteHttpUrl(baseUrl); if (!url) { throw new IdentityConfigError("BASE_URL_INVALID"); } if (environment === "production") { if (url.protocol !== "https:") { throw new IdentityConfigError("BASE_URL_INSECURE_IN_PRODUCTION"); } if (isLoopbackHostname(url.hostname)) { throw new IdentityConfigError("BASE_URL_LOOPBACK_IN_PRODUCTION"); } } return baseUrl; } function validateTrustedOrigin(origin: unknown, environment: IdentityEnvironment): string { if (typeof origin !== "string" || origin.length === 0) { throw new IdentityConfigError("TRUSTED_ORIGIN_INVALID"); } // Better Auth itself supports glob-style wildcard trusted origins. Phase 4A deliberately does // not: cross-subdomain cookies are not enabled yet (see ADR 0010), so a wildcard origin would // widen the trusted-origin surface without a matching cookie-scope decision. Revisit only // alongside a future ADR that enables cross-subdomain cookies. if (origin.includes("*") || origin.includes("?")) { throw new IdentityConfigError("TRUSTED_ORIGIN_WILDCARD_UNSUPPORTED"); } const url = parseAbsoluteHttpUrl(origin); if (!url || url.origin !== origin) { throw new IdentityConfigError("TRUSTED_ORIGIN_INVALID"); } if (environment === "production") { if (url.protocol !== "https:") { throw new IdentityConfigError("TRUSTED_ORIGIN_INSECURE_IN_PRODUCTION"); } if (isLoopbackHostname(url.hostname)) { throw new IdentityConfigError("TRUSTED_ORIGIN_LOOPBACK_IN_PRODUCTION"); } } return origin; } /** * Strict, explicit-only parsing: `undefined` is the one safe default (disabled), a genuine boolean * passes straight through for programmatic callers, and the exact lowercase strings `"true"`/ * `"false"` are accepted so an environment variable (always a string) can opt in — anything else * (a misspelling, `"1"`, `"yes"`, mixed case, an empty string) is rejected outright rather than * silently coerced to `false` or `true`. There is deliberately no "loosely truthy" branch. * * Parsing is deliberately separated from the production policy below: a malformed value is * reported as malformed in *every* environment (`PUBLIC_SIGNUP_ENABLED_INVALID`), so an operator * who typo'd the flag is never told the misleading "unsupported in production" story instead. * * Enabling public signup is then refused outright in production * (`PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION`). This is not a preference — the durable * signup-abuse prerequisites a public registration endpoint needs (verified email or an * invitation requirement, plus durable shared/edge throttling rather than this release's * per-process in-memory limiter) do not exist in this repository yet, so there is no safe * production configuration in which the flag could be honored. Development and test composition * may still enable it explicitly through this same typed path. When those prerequisites are * actually implemented and proven, this check is the single place that changes. */ function validatePublicSignupEnabled(value: unknown, environment: IdentityEnvironment): boolean { let enabled: boolean; if (value === undefined) { enabled = false; } else if (typeof value === "boolean") { enabled = value; } else if (value === "true") { enabled = true; } else if (value === "false") { enabled = false; } else { throw new IdentityConfigError("PUBLIC_SIGNUP_ENABLED_INVALID"); } if (enabled && environment === "production") { throw new IdentityConfigError("PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION"); } return enabled; } function validateTrustedOrigins( trustedOrigins: unknown, environment: IdentityEnvironment, ): readonly string[] { if (trustedOrigins === undefined || trustedOrigins === null) { throw new IdentityConfigError("TRUSTED_ORIGINS_MISSING"); } if (!Array.isArray(trustedOrigins) || trustedOrigins.length === 0) { throw new IdentityConfigError("TRUSTED_ORIGINS_EMPTY"); } return Object.freeze(trustedOrigins.map((origin) => validateTrustedOrigin(origin, environment))); } /** * Validates explicit, typed identity configuration. Performs no environment reads, network * calls, or database access; callers are responsible for sourcing `input` (for example from * environment variables) before calling this function. */ export function createIdentityConfig( input: IdentityConfigInput, options: CreateIdentityConfigOptions = {}, ): IdentityConfig { const environment = options.environment ?? "production"; const secret = validateSecret(input.secret); const baseUrl = validateBaseUrl(input.baseUrl, environment); const trustedOrigins = validateTrustedOrigins(input.trustedOrigins, environment); const publicSignupEnabled = validatePublicSignupEnabled(input.publicSignupEnabled, environment); return Object.freeze({ baseUrl, publicSignupEnabled, secret, trustedOrigins }); }