fix: harden identity HTTP transport
CI / verify (pull_request) Successful in 25s
CI / infrastructure-smoke (pull_request) Successful in 20s
CI / migration-smoke (pull_request) Successful in 25s

This commit is contained in:
ekklesia
2026-07-21 23:50:41 -05:00
parent c91f0c8c6d
commit d9b73fd81a
15 changed files with 967 additions and 72 deletions
+39 -4
View File
@@ -6,6 +6,8 @@ export type IdentityDrizzleDatabase = NodePgDatabase<typeof identityAdapterSchem
export type IdentityDatabaseErrorCode = "IDENTITY_DATABASE_CONFIGURATION_INVALID";
const maximumPostgresTimeoutMs = 2_147_483_647;
/** Never carries the offending connection string, host, or underlying driver error. */
export class IdentityDatabaseError extends Error {
readonly code: IdentityDatabaseErrorCode;
@@ -21,6 +23,11 @@ export class IdentityDatabaseError extends Error {
export interface IdentityDatabaseOptions {
readonly connectionTimeoutMs?: number;
/** Bounds both `query_timeout` and `statement_timeout`: a hung or unexpectedly slow query on
* the identity connection is aborted rather than holding the pool connection (and the caller)
* indefinitely. `connectionTimeoutMs` alone only bounds establishing a connection — it does
* nothing once a query is already running on one. */
readonly operationTimeoutMs?: number;
}
export interface IdentityDatabaseHandle {
@@ -44,6 +51,24 @@ function validateConnectionString(connectionString: string): string {
return connectionString;
}
/** Mirrors `core/src/migrations/postgres-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 IdentityDatabaseError("IDENTITY_DATABASE_CONFIGURATION_INVALID");
}
return value;
}
/**
* Explicitly constructs the Core-owned Drizzle database instance used by the identity factory.
* Building the `pg.Pool` performs no network I/O until a query is issued, and this function is
@@ -54,10 +79,20 @@ export function createIdentityDrizzleDatabase(
options: IdentityDatabaseOptions = {},
): IdentityDatabaseHandle {
const validatedConnectionString = validateConnectionString(connectionString);
const poolConfig: PoolConfig = { connectionString: validatedConnectionString };
if (options.connectionTimeoutMs !== undefined) {
poolConfig.connectionTimeoutMillis = options.connectionTimeoutMs;
}
const validatedConnectionTimeoutMs = validateTimeout(options.connectionTimeoutMs);
const validatedOperationTimeoutMs = validateTimeout(options.operationTimeoutMs);
const poolConfig: PoolConfig = {
connectionString: validatedConnectionString,
...(validatedConnectionTimeoutMs === undefined
? {}
: { connectionTimeoutMillis: validatedConnectionTimeoutMs }),
...(validatedOperationTimeoutMs === undefined
? {}
: {
query_timeout: validatedOperationTimeoutMs,
statement_timeout: validatedOperationTimeoutMs,
}),
};
const pool = new Pool(poolConfig);
// pg's own documentation requires an error listener on the pool: an idle client can be