import path from "node:path"; import { isVersionCompatible, moduleIdSchema, semanticVersionSchema } from "@ekklesia/contracts"; import { getTableName } from "drizzle-orm"; import { type CreateIdentityConfigOptions, createIdentityConfig, IdentityConfigError, } from "../identity/config.js"; import { identityAdapterSchema } from "../identity/schema.js"; import { createPostgresMigrationDatabase, discoverAllMigrations, type MigrationDatabase, type MigrationPlanResult, MigrationRunnerError, PostgresMigrationDatabaseError, type PostgresMigrationDatabaseOptions, planModuleMigrations, } from "../migrations/index.js"; import { type DiscoveredModule, discoverModuleSchemas, discoverModules, type ModuleDiscoveryOptions, type ModuleMigrationAggregate, type ModuleSchemaAggregate, } from "../modules/index.js"; export type DoctorCheckId = | "platform.runtime" | "modules.discovery" | "modules.dependencies" | "schemas.discovery" | "migrations.discovery" | "postgresql.connection" | "postgresql.plan" | "identity.configuration" | "object-storage.integration"; export type DoctorCheckStatus = "failure" | "pass" | "skipped" | "warning"; export type DoctorResult = "not-ready" | "ready" | "ready-with-warnings"; export interface DoctorCheck { readonly details: readonly string[]; readonly id: DoctorCheckId; readonly status: DoctorCheckStatus; readonly summary: string; } export interface DoctorReport { readonly checks: readonly DoctorCheck[]; readonly result: DoctorResult; } export interface DoctorIdentityConfigurationOptions { readonly baseUrl?: string | undefined; readonly environment?: CreateIdentityConfigOptions["environment"]; readonly secret?: string | undefined; readonly trustedOrigins?: readonly string[] | undefined; } export interface DoctorOptions { readonly databaseUrl?: string | undefined; readonly identityConfiguration?: DoctorIdentityConfigurationOptions | undefined; readonly moduleDiscovery: ModuleDiscoveryOptions; readonly runtimeRange: string; readonly runtimeVersion: string; } export interface DoctorDependencies { readonly createMigrationDatabase: ( connectionString: string, options: PostgresMigrationDatabaseOptions, ) => MigrationDatabase; readonly discoverMigrations: ( options: ModuleDiscoveryOptions, ) => Promise; readonly discoverModules: ( options: ModuleDiscoveryOptions, ) => Promise; readonly discoverSchemas: (options: ModuleDiscoveryOptions) => Promise; readonly planMigrations: (options: { readonly aggregate: ModuleMigrationAggregate; readonly database: MigrationDatabase; }) => Promise; readonly runtimeSatisfies: (version: string, range: string) => boolean; } const doctorPostgresTimeoutMs = 10_000; const checksumPattern = /^[a-f0-9]{64}$/u; const migrationIdentityPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*:\d{4,}_[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/u; async function discoverAllMigrationsForRuntime( options: ModuleDiscoveryOptions, ): Promise { return discoverAllMigrations({ coreMigrationsDirectory: path.join(path.dirname(options.modulesDirectory), "core", "drizzle"), moduleDiscovery: options, }); } const productionDependencies: DoctorDependencies = Object.freeze({ createMigrationDatabase: createPostgresMigrationDatabase, discoverMigrations: discoverAllMigrationsForRuntime, discoverModules, discoverSchemas: discoverModuleSchemas, planMigrations: planModuleMigrations, runtimeSatisfies: isVersionCompatible, }); function plural(count: number, singular: string): string { return count === 1 ? singular : `${singular}s`; } function freezeCheck( id: DoctorCheckId, status: DoctorCheckStatus, summary: string, details: readonly string[] = [], ): DoctorCheck { return Object.freeze({ details: Object.freeze([...details]), id, status, summary, }); } export function deriveDoctorResult(checks: readonly DoctorCheck[]): DoctorResult { if (checks.some((check) => check.status === "failure")) { return "not-ready"; } if (checks.some((check) => check.status === "warning" || check.status === "skipped")) { return "ready-with-warnings"; } return "ready"; } function publishReport(checks: readonly DoctorCheck[]): DoctorReport { const frozenChecks = Object.freeze([...checks]); return Object.freeze({ checks: frozenChecks, result: deriveDoctorResult(frozenChecks) }); } function safeModuleDetails(modules: readonly DiscoveredModule[]): readonly string[] { return modules.map((module) => { const id = module.manifest.id; const version = module.manifest.version; if ( !moduleIdSchema.safeParse(id).success || !semanticVersionSchema.safeParse(version).success ) { throw new TypeError("Unsafe module diagnostic value."); } return `${id} ${version}`; }); } function safeMigrationReference(identity: string, checksum: string): string { if (!migrationIdentityPattern.test(identity) || !checksumPattern.test(checksum)) { throw new TypeError("Unsafe migration diagnostic value."); } return `${identity} · ${checksum.slice(0, 12)}`; } function moduleDiscoveryFailure(error: unknown): DoctorCheck { const details = error instanceof Error && error.name === "ModuleDiscoveryError" ? ["The installed module inventory is invalid."] : ["Static module discovery could not be completed."]; return freezeCheck("modules.discovery", "failure", "Module discovery failed", details); } function schemaDiscoveryFailure(): DoctorCheck { return freezeCheck("schemas.discovery", "failure", "Schema validation failed", [ "Installed module schemas could not be validated.", ]); } function migrationDiscoveryFailure(): DoctorCheck { return freezeCheck("migrations.discovery", "failure", "Migration validation failed", [ "Installed module migrations could not be validated.", ]); } function configuredDatabaseFailure(error: unknown): readonly [DoctorCheck, DoctorCheck] { if (error instanceof MigrationRunnerError) { if (error.code === "MIGRATION_CONNECTION_FAILED") { return [ freezeCheck("postgresql.connection", "failure", "Connection failed", [ "The configured PostgreSQL database could not be reached.", ]), freezeCheck("postgresql.plan", "skipped", "Migration plan skipped", [ "A successful PostgreSQL connection is required.", ]), ]; } return [ freezeCheck("postgresql.connection", "pass", "Connection established"), freezeCheck("postgresql.plan", "failure", "Migration history validation failed", [ migrationPlanFailureDetail(error.code), ]), ]; } if (error instanceof PostgresMigrationDatabaseError) { return [ freezeCheck("postgresql.connection", "failure", "PostgreSQL configuration invalid", [ "The configured database connection could not be prepared safely.", ]), freezeCheck("postgresql.plan", "skipped", "Migration plan skipped", [ "A valid PostgreSQL configuration is required.", ]), ]; } return [ freezeCheck("postgresql.connection", "failure", "PostgreSQL diagnostic failed", [ "The configured database could not be inspected safely.", ]), freezeCheck("postgresql.plan", "skipped", "Migration plan skipped", [ "PostgreSQL connectivity could not be confirmed.", ]), ]; } function migrationPlanFailureDetail(code: MigrationRunnerError["code"]): string { switch (code) { case "MIGRATION_CHECKSUM_MISMATCH": return "An applied migration checksum disagrees with packaged inventory."; case "MIGRATION_HISTORY_INVALID": return "Applied migration history is invalid."; case "MIGRATION_HISTORY_READ_FAILED": return "Applied migration history could not be read safely."; case "MIGRATION_UNKNOWN_APPLIED": return "Applied migration history contains an unknown identity."; case "MIGRATION_CLEANUP_FAILED": return "The read-only PostgreSQL session did not close cleanly."; default: return "The read-only migration plan could not be completed safely."; } } function migrationPlanCheck(plan: MigrationPlanResult): DoctorCheck { const pendingCount = plan.entries.filter((entry) => entry.status === "pending").length; const blockedCount = plan.entries.filter((entry) => entry.status === "approval-blocked").length; const appliedCount = plan.entries.filter((entry) => entry.status === "applied").length; if ( appliedCount !== plan.appliedCount || pendingCount + blockedCount !== plan.pendingCount || blockedCount !== plan.approvalRequiredCount ) { throw new TypeError("Inconsistent migration plan diagnostic value."); } const details = [ `${appliedCount} applied · ${pendingCount} pending · ${blockedCount} approval-blocked`, ...plan.entries.map((entry) => { if (!(["applied", "approval-blocked", "pending"] as const).includes(entry.status)) { throw new TypeError("Unsafe migration plan diagnostic value."); } return `${entry.status} · ${safeMigrationReference(entry.identity, entry.checksum)}`; }), ]; if (pendingCount + blockedCount === 0) { return freezeCheck("postgresql.plan", "pass", "Migration history current", details); } const attentionCount = pendingCount + blockedCount; return freezeCheck( "postgresql.plan", "warning", attentionCount === 1 ? "1 migration requires attention" : `${attentionCount} migrations require attention`, details, ); } function identitySchemaSummary(): string { const tableNames = Object.values(identityAdapterSchema) .map((table) => getTableName(table)) .sort(); return `Core identity schema defines ${tableNames.length} ${plural(tableNames.length, "table")} (${tableNames.join(", ")}).`; } /** * Pure configuration validation only: never constructs Better Auth, never opens a database * connection, and never prints the configured secret, base URL, or trusted origins. Missing * configuration is a skip, not a failure — Doctor does not require identity to be configured yet. */ function identityConfigurationCheck( input: DoctorIdentityConfigurationOptions | undefined, ): DoctorCheck { const schemaDetail = identitySchemaSummary(); const hasAnyValue = input?.secret !== undefined || input?.baseUrl !== undefined || (input?.trustedOrigins?.length ?? 0) > 0; if (!hasAnyValue) { return freezeCheck("identity.configuration", "skipped", "Identity configuration skipped", [ schemaDetail, "Identity runtime configuration (signing key, base URL, trusted origins) is not set.", ]); } try { createIdentityConfig( { baseUrl: input?.baseUrl, secret: input?.secret, trustedOrigins: input?.trustedOrigins, }, { environment: input?.environment ?? "production" }, ); return freezeCheck("identity.configuration", "pass", "Identity configuration valid", [ schemaDetail, ]); } catch (error) { const code = error instanceof IdentityConfigError ? error.code : "IDENTITY_CONFIGURATION_INVALID"; return freezeCheck("identity.configuration", "failure", "Identity configuration invalid", [ schemaDetail, `[${code}] Configured identity values failed validation.`, ]); } } function skippedPostgresChecks(detail: string): readonly [DoctorCheck, DoctorCheck] { return [ freezeCheck("postgresql.connection", "skipped", "Connection check skipped", [detail]), freezeCheck("postgresql.plan", "skipped", "Migration plan skipped", [ "A validated migration inventory and configured PostgreSQL connection are required.", ]), ]; } export function doctorExitCode(report: DoctorReport): 0 | 1 { return report.result === "not-ready" ? 1 : 0; } export async function diagnoseEkklesia( options: DoctorOptions, dependencies: DoctorDependencies = productionDependencies, ): Promise { let runtimeCheck: DoctorCheck; try { runtimeCheck = dependencies.runtimeSatisfies(options.runtimeVersion, options.runtimeRange) ? freezeCheck("platform.runtime", "pass", "Runtime version supported", [ "Active Node.js satisfies the declared supported range.", ]) : freezeCheck("platform.runtime", "failure", "Runtime version unsupported", [ "Install a Node.js version supported by this repository.", ]); } catch { runtimeCheck = freezeCheck("platform.runtime", "failure", "Runtime validation failed", [ "The active Node.js runtime could not be validated safely.", ]); } let discoveredModules: readonly DiscoveredModule[] | undefined; let moduleCheck: DoctorCheck; let dependencyCheck: DoctorCheck; try { discoveredModules = await dependencies.discoverModules(options.moduleDiscovery); const moduleDetails = safeModuleDetails(discoveredModules); moduleCheck = freezeCheck( "modules.discovery", "pass", "Module discovery complete", discoveredModules.length === 0 ? ["0 modules installed"] : moduleDetails, ); dependencyCheck = freezeCheck("modules.dependencies", "pass", "Dependency graph valid", [ discoveredModules.length === 0 ? "Dependency order is empty." : `Order: ${discoveredModules.map((module) => module.manifest.id).join(" → ")}`, ]); } catch (error) { moduleCheck = moduleDiscoveryFailure(error); dependencyCheck = freezeCheck("modules.dependencies", "skipped", "Dependency check skipped", [ "Complete module discovery is required.", ]); } let schemaCheck: DoctorCheck; let migrationCheck: DoctorCheck; let migrations: ModuleMigrationAggregate | undefined; if (!discoveredModules) { schemaCheck = freezeCheck("schemas.discovery", "skipped", "Schema validation skipped", [ "Complete module discovery is required.", ]); migrationCheck = freezeCheck( "migrations.discovery", "skipped", "Migration validation skipped", ["Complete module discovery is required."], ); } else { try { const schemas = await dependencies.discoverSchemas(options.moduleDiscovery); schemaCheck = freezeCheck("schemas.discovery", "pass", "Schemas validated", [ `${schemas.discoveredModules.length} ${plural(schemas.discoveredModules.length, "module")} · ${schemas.tables.length} ${plural(schemas.tables.length, "table")}`, ]); } catch { schemaCheck = schemaDiscoveryFailure(); } try { migrations = await dependencies.discoverMigrations(options.moduleDiscovery); const inventory = migrations.migrations.map((migration) => safeMigrationReference(migration.identity, migration.checksum), ); migrationCheck = freezeCheck("migrations.discovery", "pass", "Migrations validated", [ `${migrations.migrations.length} ${plural(migrations.migrations.length, "migration")} · checksums valid`, ...inventory, ]); } catch { migrations = undefined; migrationCheck = migrationDiscoveryFailure(); } } let postgresConnectionCheck: DoctorCheck; let postgresPlanCheck: DoctorCheck; const databaseUrl = options.databaseUrl; const databaseConfigured = typeof databaseUrl === "string" && databaseUrl.trim() !== ""; if (!migrations) { [postgresConnectionCheck, postgresPlanCheck] = skippedPostgresChecks( "Complete migration discovery is required.", ); } else if (!databaseConfigured) { [postgresConnectionCheck, postgresPlanCheck] = skippedPostgresChecks( "DATABASE_URL is not configured.", ); } else { try { const database = dependencies.createMigrationDatabase(databaseUrl, { connectionTimeoutMs: doctorPostgresTimeoutMs, operationTimeoutMs: doctorPostgresTimeoutMs, }); const plan = await dependencies.planMigrations({ aggregate: migrations, database }); postgresConnectionCheck = freezeCheck( "postgresql.connection", "pass", "Connection established", ); postgresPlanCheck = migrationPlanCheck(plan); } catch (error) { [postgresConnectionCheck, postgresPlanCheck] = configuredDatabaseFailure(error); } } const identityCheck = identityConfigurationCheck(options.identityConfiguration); const storageCheck = freezeCheck("object-storage.integration", "skipped", "Check unavailable", [ "Core storage integration has not been implemented yet.", ]); return publishReport([ runtimeCheck, moduleCheck, dependencyCheck, schemaCheck, migrationCheck, postgresConnectionCheck, postgresPlanCheck, identityCheck, storageCheck, ]); }