import { assertProductionDatabaseTransport, type DatabaseTransportPolicy, type DatabaseTransportPolicyInput, createDatabaseTransportPolicy, } from "@ekklesia/core/database"; import { type CreateIdentityConfigOptions, createIdentityConfig, type IdentityConfig, type IdentityConfigInput, } from "@ekklesia/core/identity"; import { type ClientAddressConfig, type ClientAddressConfigInput, createClientAddressConfig, } from "./client-address.js"; export type ApiConfigErrorCode = | "DATABASE_URL_INVALID" | "DATABASE_URL_MISSING" | "HOST_INVALID" | "PORT_INVALID"; /** Never carries the offending database URL, host, port, or any other caller-supplied value. */ export class ApiConfigError extends Error { readonly code: ApiConfigErrorCode; constructor(code: ApiConfigErrorCode) { super(apiConfigErrorMessages[code]); this.name = "ApiConfigError"; this.code = code; this.stack = `${this.name}: ${this.message}`; Object.freeze(this); } } const apiConfigErrorMessages: Readonly> = Object.freeze({ DATABASE_URL_INVALID: "The API database URL must be an absolute postgres:// or postgresql:// URL.", DATABASE_URL_MISSING: "The API database URL is required.", HOST_INVALID: "The API listen address must be a non-empty host.", PORT_INVALID: "The API listen port must be an integer between 0 and 65535 (0 requests an OS-assigned port).", }); const defaultHost = "127.0.0.1"; const defaultPort = 3000; export interface ApiRuntimeConfig { readonly clientAddress: ClientAddressConfig; readonly databaseUrl: string; /** The one validated PostgreSQL transport policy this runtime's single pool uses. */ readonly databaseTransport: DatabaseTransportPolicy; readonly host: string; readonly identity: IdentityConfig; readonly port: number; } export interface ApiRuntimeConfigInput { /** Optional. Defaults to direct mode (trusts only the actual immediate socket peer) — no proxy * trust unless explicitly configured. See `./client-address.js`. */ readonly clientAddress?: ClientAddressConfigInput; readonly databaseUrl?: unknown; /** Optional. Defaults to certificate-verified TLS in production and plaintext in development; * production can never select plaintext. See `@ekklesia/core/database`'s transport policy. */ readonly databaseTransport?: DatabaseTransportPolicyInput; readonly host?: unknown; readonly identity: IdentityConfigInput; readonly port?: unknown; } export interface CreateApiRuntimeConfigOptions { readonly environment?: CreateIdentityConfigOptions["environment"]; } function validateDatabaseUrl(databaseUrl: unknown): string { if (typeof databaseUrl !== "string" || databaseUrl.trim().length === 0) { throw new ApiConfigError("DATABASE_URL_MISSING"); } let protocol: string; try { protocol = new URL(databaseUrl).protocol; } catch { throw new ApiConfigError("DATABASE_URL_INVALID"); } if (protocol !== "postgres:" && protocol !== "postgresql:") { throw new ApiConfigError("DATABASE_URL_INVALID"); } return databaseUrl; } function validateHost(host: unknown): string { if (host === undefined) { return defaultHost; } if (typeof host !== "string" || host.trim().length === 0) { throw new ApiConfigError("HOST_INVALID"); } return host; } function validatePort(port: unknown): number { if (port === undefined) { return defaultPort; } const numericPort = typeof port === "string" ? Number(port) : port; if ( typeof numericPort !== "number" || !Number.isInteger(numericPort) || numericPort < 0 || numericPort > 65_535 ) { throw new ApiConfigError("PORT_INVALID"); } return numericPort; } /** * Validates explicit, typed API runtime configuration, delegating identity fields to Core's * existing `createIdentityConfig` rather than defining a competing identity configuration * contract. Performs no environment reads, network calls, or database access. */ export function createApiRuntimeConfig( input: ApiRuntimeConfigInput, options: CreateApiRuntimeConfigOptions = {}, ): ApiRuntimeConfig { const databaseUrl = validateDatabaseUrl(input.databaseUrl); const host = validateHost(input.host); const port = validatePort(input.port); const identity = createIdentityConfig( input.identity, options.environment === undefined ? {} : { environment: options.environment }, ); const clientAddress = createClientAddressConfig(input.clientAddress); const environment = options.environment ?? "production"; const databaseTransport = createDatabaseTransportPolicy(input.databaseTransport, { environment, }); // Fails here — before `startApi` constructs a pool, Better Auth, the module runtime, or a // listener — if a production deployment would connect without certificate-verified TLS, or if // the URL carries TLS parameters that could override the policy. assertProductionDatabaseTransport(databaseUrl, databaseTransport, environment); return Object.freeze({ clientAddress, databaseTransport, databaseUrl, host, identity, port, }); }