import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres"; import { Pool, type PoolConfig } from "pg"; import { type DatabaseTransportPolicy, databaseTlsConnectionOptions, } from "../database/transport.js"; import { authorizationSchema } from "./schema.js"; export type AuthorizationDrizzleDatabase = NodePgDatabase; export type AuthorizationDatabaseErrorCode = "AUTHORIZATION_DATABASE_CONFIGURATION_INVALID"; const maximumPostgresTimeoutMs = 2_147_483_647; /** Never carries the offending connection string, host, or underlying driver error. */ export class AuthorizationDatabaseError extends Error { readonly code: AuthorizationDatabaseErrorCode; constructor(code: AuthorizationDatabaseErrorCode) { super("Authorization database configuration is invalid."); this.name = "AuthorizationDatabaseError"; this.code = code; this.stack = `${this.name}: ${this.message}`; Object.freeze(this); } } export interface AuthorizationDatabaseOptions { readonly connectionTimeoutMs?: number; /** Bounds both `query_timeout` and `statement_timeout`, mirroring * `core/src/identity/database.ts`: `connectionTimeoutMs` alone only bounds establishing a * connection, not a hung query already running on one. */ readonly operationTimeoutMs?: number; /** The validated transport policy (`core/src/database/transport.js`). Passed explicitly by * every production executable; omitted by single-domain unit/integration factories that use * the disposable loopback database, which keeps the pre-existing plaintext behavior. */ readonly transport?: DatabaseTransportPolicy; } export interface AuthorizationDatabaseHandle { readonly close: () => Promise; readonly db: AuthorizationDrizzleDatabase; } function validateConnectionString(connectionString: string): string { if (connectionString.trim().length === 0) { throw new AuthorizationDatabaseError("AUTHORIZATION_DATABASE_CONFIGURATION_INVALID"); } let protocol: string; try { protocol = new URL(connectionString).protocol; } catch { throw new AuthorizationDatabaseError("AUTHORIZATION_DATABASE_CONFIGURATION_INVALID"); } if (protocol !== "postgres:" && protocol !== "postgresql:") { throw new AuthorizationDatabaseError("AUTHORIZATION_DATABASE_CONFIGURATION_INVALID"); } return connectionString; } /** Mirrors `core/src/identity/database.ts`'s timeout validation exactly: an explicit timeout must * be a positive safe integer no larger than PostgreSQL's own maximum, or it is rejected as invalid * configuration rather than silently passed through to `pg`. */ function validateTimeout(value: unknown): number | undefined { if (value === undefined) { return undefined; } if ( typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0 || value > maximumPostgresTimeoutMs ) { throw new AuthorizationDatabaseError("AUTHORIZATION_DATABASE_CONFIGURATION_INVALID"); } return value; } /** * Explicitly constructs the Core-owned Drizzle database instance backing the authorization store. * Building the `pg.Pool` performs no network I/O until a query is issued, and this function is * only ever invoked by explicit caller code — never during module import. */ export function createAuthorizationDrizzleDatabase( connectionString: string, options: AuthorizationDatabaseOptions = {}, ): AuthorizationDatabaseHandle { const validatedConnectionString = validateConnectionString(connectionString); const validatedConnectionTimeoutMs = validateTimeout(options.connectionTimeoutMs); const validatedOperationTimeoutMs = validateTimeout(options.operationTimeoutMs); const ssl = options.transport === undefined ? undefined : databaseTlsConnectionOptions(validatedConnectionString, options.transport); const poolConfig: PoolConfig = { connectionString: validatedConnectionString, ...(ssl === undefined ? {} : { ssl }), ...(validatedConnectionTimeoutMs === undefined ? {} : { connectionTimeoutMillis: validatedConnectionTimeoutMs }), ...(validatedOperationTimeoutMs === undefined ? {} : { query_timeout: validatedOperationTimeoutMs, statement_timeout: validatedOperationTimeoutMs, }), }; const pool = new Pool(poolConfig); // Same rationale as core/src/identity/database.ts: pg's own documentation requires an error // listener on the pool, or a server-initiated termination of an idle client becomes an // unhandled Node.js error and can crash the process. pool.on("error", () => undefined); const db = drizzle(pool, { schema: authorizationSchema }); return Object.freeze({ close: () => pool.end(), db, }); } /** * A typed Drizzle view over an externally owned pool — for example one shared with the identity * schema via `createValidatedPostgresPool` (`core/src/database/pool.ts`). Unlike * `createAuthorizationDrizzleDatabase`, this function never constructs, registers an error * listener on, or closes a pool itself: the caller retains that responsibility exactly once, * regardless of how many typed views it creates over the same pool. */ export function createAuthorizationDrizzleView(pool: Pool): AuthorizationDrizzleDatabase { return drizzle(pool, { schema: authorizationSchema }); }