feat: establish core identity foundation
CI / verify (pull_request) Successful in 25s
CI / infrastructure-smoke (pull_request) Successful in 12s
CI / migration-smoke (pull_request) Successful in 22s

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]>
This commit is contained in:
ekklesia
2026-07-21 16:39:15 -05:00
co-authored by Claude Sonnet 5
parent df0f20d8af
commit 679a5d225c
43 changed files with 4259 additions and 64 deletions
+81 -2
View File
@@ -1,6 +1,15 @@
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,
@@ -10,7 +19,6 @@ import {
} from "../migrations/index.js";
import {
type DiscoveredModule,
discoverModuleMigrations,
discoverModuleSchemas,
discoverModules,
type ModuleDiscoveryOptions,
@@ -26,6 +34,7 @@ export type DoctorCheckId =
| "migrations.discovery"
| "postgresql.connection"
| "postgresql.plan"
| "identity.configuration"
| "object-storage.integration";
export type DoctorCheckStatus = "failure" | "pass" | "skipped" | "warning";
@@ -43,8 +52,16 @@ export interface DoctorReport {
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;
@@ -74,9 +91,18 @@ 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<ModuleMigrationAggregate> {
return discoverAllMigrations({
coreMigrationsDirectory: path.join(path.dirname(options.modulesDirectory), "core", "drizzle"),
moduleDiscovery: options,
});
}
const productionDependencies: DoctorDependencies = Object.freeze({
createMigrationDatabase: createPostgresMigrationDatabase,
discoverMigrations: discoverModuleMigrations,
discoverMigrations: discoverAllMigrationsForRuntime,
discoverModules,
discoverSchemas: discoverModuleSchemas,
planMigrations: planModuleMigrations,
@@ -254,6 +280,56 @@ function migrationPlanCheck(plan: MigrationPlanResult): DoctorCheck {
);
}
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]),
@@ -380,6 +456,8 @@ export async function diagnoseEkklesia(
}
}
const identityCheck = identityConfigurationCheck(options.identityConfiguration);
const storageCheck = freezeCheck("object-storage.integration", "skipped", "Check unavailable", [
"Core storage integration has not been implemented yet.",
]);
@@ -392,6 +470,7 @@ export async function diagnoseEkklesia(
migrationCheck,
postgresConnectionCheck,
postgresPlanCheck,
identityCheck,
storageCheck,
]);
}