Adds Core-owned identity contracts, a Core-owned Better Auth database schema (ekklesia_core_user/session/account/verification), a first-class Core-owned migration source that coexists with module-owned migrations under the exact existing migration system, and an explicit Better Auth construction boundary. No authentication endpoint or sign-in UI is exposed yet (ADR 0010). Co-Authored-By: Claude Sonnet 5 <[email protected]>
70 lines
2.4 KiB
TypeScript
70 lines
2.4 KiB
TypeScript
import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
import { Pool, type PoolConfig } from "pg";
|
|
import { identityAdapterSchema } from "./schema.js";
|
|
|
|
export type IdentityDrizzleDatabase = NodePgDatabase<typeof identityAdapterSchema>;
|
|
|
|
export type IdentityDatabaseErrorCode = "IDENTITY_DATABASE_CONFIGURATION_INVALID";
|
|
|
|
/** Never carries the offending connection string, host, or underlying driver error. */
|
|
export class IdentityDatabaseError extends Error {
|
|
readonly code: IdentityDatabaseErrorCode;
|
|
|
|
constructor(code: IdentityDatabaseErrorCode) {
|
|
super("Identity database configuration is invalid.");
|
|
this.name = "IdentityDatabaseError";
|
|
this.code = code;
|
|
this.stack = `${this.name}: ${this.message}`;
|
|
Object.freeze(this);
|
|
}
|
|
}
|
|
|
|
export interface IdentityDatabaseOptions {
|
|
readonly connectionTimeoutMs?: number;
|
|
}
|
|
|
|
export interface IdentityDatabaseHandle {
|
|
readonly close: () => Promise<void>;
|
|
readonly db: IdentityDrizzleDatabase;
|
|
}
|
|
|
|
function validateConnectionString(connectionString: string): string {
|
|
if (connectionString.trim().length === 0) {
|
|
throw new IdentityDatabaseError("IDENTITY_DATABASE_CONFIGURATION_INVALID");
|
|
}
|
|
let protocol: string;
|
|
try {
|
|
protocol = new URL(connectionString).protocol;
|
|
} catch {
|
|
throw new IdentityDatabaseError("IDENTITY_DATABASE_CONFIGURATION_INVALID");
|
|
}
|
|
if (protocol !== "postgres:" && protocol !== "postgresql:") {
|
|
throw new IdentityDatabaseError("IDENTITY_DATABASE_CONFIGURATION_INVALID");
|
|
}
|
|
return connectionString;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* only ever invoked by explicit caller code — never during module import.
|
|
*/
|
|
export function createIdentityDrizzleDatabase(
|
|
connectionString: string,
|
|
options: IdentityDatabaseOptions = {},
|
|
): IdentityDatabaseHandle {
|
|
const validatedConnectionString = validateConnectionString(connectionString);
|
|
const poolConfig: PoolConfig = { connectionString: validatedConnectionString };
|
|
if (options.connectionTimeoutMs !== undefined) {
|
|
poolConfig.connectionTimeoutMillis = options.connectionTimeoutMs;
|
|
}
|
|
|
|
const pool = new Pool(poolConfig);
|
|
const db = drizzle(pool, { schema: identityAdapterSchema });
|
|
|
|
return Object.freeze({
|
|
close: () => pool.end(),
|
|
db,
|
|
});
|
|
}
|