feat: establish core identity foundation
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:
co-authored by
Claude Sonnet 5
parent
df0f20d8af
commit
679a5d225c
@@ -0,0 +1,243 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
CORE_MIGRATION_SOURCE_ID,
|
||||
discoverAllMigrations,
|
||||
discoverCoreMigrations,
|
||||
} from "../src/migrations/index.js";
|
||||
import { ModuleMigrationDiscoveryError } from "../src/modules/index.js";
|
||||
|
||||
const originSnapshotId = "00000000-0000-0000-0000-000000000000";
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryDirectories
|
||||
.splice(0)
|
||||
.map((directory) => rm(directory, { force: true, recursive: true })),
|
||||
);
|
||||
});
|
||||
|
||||
async function createTemporaryDirectory(prefix: string): Promise<string> {
|
||||
const directory = await mkdtemp(path.join(tmpdir(), prefix));
|
||||
temporaryDirectories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
/** Writes a single-migration Drizzle directory (journal + snapshot + policy + SQL). */
|
||||
async function writeMigrationsDirectory(
|
||||
migrationsDirectory: string,
|
||||
moduleId: string,
|
||||
tag: string,
|
||||
sql: string,
|
||||
): Promise<string> {
|
||||
await mkdir(path.join(migrationsDirectory, "meta"), { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
path.join(migrationsDirectory, "meta/_journal.json"),
|
||||
JSON.stringify({
|
||||
version: "7",
|
||||
dialect: "postgresql",
|
||||
entries: [{ idx: 0, version: "7", when: 1_700_000_000_000, tag, breakpoints: true }],
|
||||
}),
|
||||
),
|
||||
writeFile(
|
||||
path.join(migrationsDirectory, "meta/_ekklesia.json"),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
moduleId,
|
||||
entries: [{ tag, destructive: { mode: "none" } }],
|
||||
}),
|
||||
),
|
||||
writeFile(
|
||||
path.join(migrationsDirectory, "meta/0000_snapshot.json"),
|
||||
JSON.stringify({
|
||||
version: "7",
|
||||
dialect: "postgresql",
|
||||
id: "10000000-0000-4000-8000-000000000001",
|
||||
prevId: originSnapshotId,
|
||||
}),
|
||||
),
|
||||
writeFile(path.join(migrationsDirectory, `${tag}.sql`), sql),
|
||||
]);
|
||||
return sql;
|
||||
}
|
||||
|
||||
async function createModule(
|
||||
modulesDirectory: string,
|
||||
id: string,
|
||||
options: { readonly dependsOn?: readonly string[] } = {},
|
||||
): Promise<void> {
|
||||
const moduleDirectory = path.join(modulesDirectory, id);
|
||||
const migrationsDirectory = path.join(moduleDirectory, "drizzle");
|
||||
await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true });
|
||||
const requiredDependencies = Object.fromEntries(
|
||||
(options.dependsOn ?? []).map((dependencyId) => [dependencyId, "^0.1.0"]),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
path.join(moduleDirectory, "package.json"),
|
||||
JSON.stringify({
|
||||
name: `@test/module-${id}`,
|
||||
version: "0.1.0",
|
||||
type: "module",
|
||||
ekklesia: { layer: "module" },
|
||||
}),
|
||||
),
|
||||
writeFile(
|
||||
path.join(moduleDirectory, "ekklesia.module.json"),
|
||||
JSON.stringify({
|
||||
manifestVersion: 2,
|
||||
id,
|
||||
name: `${id} module`,
|
||||
version: "0.1.0",
|
||||
compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0" },
|
||||
dependencies: {
|
||||
modules: { required: requiredDependencies, optional: {} },
|
||||
capabilities: { required: {}, optional: {} },
|
||||
},
|
||||
capabilities: [],
|
||||
permissions: [],
|
||||
entrypoints: { server: "./dist/server/index.js", migrations: "./drizzle" },
|
||||
}),
|
||||
),
|
||||
writeFile(
|
||||
path.join(moduleDirectory, "dist/server/index.js"),
|
||||
`export default { id: ${JSON.stringify(id)}, version: "0.1.0" };`,
|
||||
),
|
||||
]);
|
||||
|
||||
await writeMigrationsDirectory(
|
||||
migrationsDirectory,
|
||||
id,
|
||||
"0000_initial",
|
||||
`CREATE TABLE "${id.replaceAll("-", "_")}_records" ("id" integer);\n`,
|
||||
);
|
||||
}
|
||||
|
||||
function sha256(contents: string): string {
|
||||
return createHash("sha256").update(contents).digest("hex");
|
||||
}
|
||||
|
||||
describe("Core-owned migration discovery", () => {
|
||||
it("discovers Core's own migrations with the reserved core identity and an exact checksum", async () => {
|
||||
const coreMigrationsDirectory = await createTemporaryDirectory("ekklesia-core-migrations-");
|
||||
const sql = await writeMigrationsDirectory(
|
||||
coreMigrationsDirectory,
|
||||
"core",
|
||||
"0000_identity_core_tables",
|
||||
'CREATE TABLE "ekklesia_core_user" ("id" text PRIMARY KEY);\n',
|
||||
);
|
||||
|
||||
const migrations = await discoverCoreMigrations({ coreMigrationsDirectory });
|
||||
|
||||
expect(CORE_MIGRATION_SOURCE_ID).toBe("core");
|
||||
expect(migrations).toHaveLength(1);
|
||||
expect(migrations[0]).toMatchObject({
|
||||
approvalRequired: false,
|
||||
checksum: sha256(sql),
|
||||
identity: "core:0000_identity_core_tables",
|
||||
index: 0,
|
||||
moduleId: "core",
|
||||
tag: "0000_identity_core_tables",
|
||||
});
|
||||
});
|
||||
|
||||
it("is deterministic across repeated discovery calls", async () => {
|
||||
const coreMigrationsDirectory = await createTemporaryDirectory("ekklesia-core-migrations-");
|
||||
await writeMigrationsDirectory(
|
||||
coreMigrationsDirectory,
|
||||
"core",
|
||||
"0000_identity_core_tables",
|
||||
'CREATE TABLE "ekklesia_core_user" ("id" text PRIMARY KEY);\n',
|
||||
);
|
||||
|
||||
const first = await discoverCoreMigrations({ coreMigrationsDirectory });
|
||||
const second = await discoverCoreMigrations({ coreMigrationsDirectory });
|
||||
|
||||
expect(first).toEqual(second);
|
||||
});
|
||||
|
||||
it("orders Core migrations before every module migration while preserving dependency-first module order", async () => {
|
||||
const modulesDirectory = await createTemporaryDirectory("ekklesia-modules-");
|
||||
const coreMigrationsDirectory = await createTemporaryDirectory("ekklesia-core-migrations-");
|
||||
await writeMigrationsDirectory(
|
||||
coreMigrationsDirectory,
|
||||
"core",
|
||||
"0000_identity_core_tables",
|
||||
'CREATE TABLE "ekklesia_core_user" ("id" text PRIMARY KEY);\n',
|
||||
);
|
||||
// "calendar" depends on "people"; created on disk in the opposite order so only the
|
||||
// dependency graph (not directory order) can explain the resulting sequence.
|
||||
await createModule(modulesDirectory, "calendar", { dependsOn: ["people"] });
|
||||
await createModule(modulesDirectory, "people");
|
||||
|
||||
const aggregate = await discoverAllMigrations({
|
||||
coreMigrationsDirectory,
|
||||
moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0" },
|
||||
});
|
||||
|
||||
expect(aggregate.modules.map((module) => module.moduleId)).toEqual([
|
||||
"core",
|
||||
"people",
|
||||
"calendar",
|
||||
]);
|
||||
expect(aggregate.migrations.map((migration) => migration.identity)).toEqual([
|
||||
"core:0000_identity_core_tables",
|
||||
"people:0000_initial",
|
||||
"calendar:0000_initial",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects an installed module that reuses the reserved core migration identity", async () => {
|
||||
const modulesDirectory = await createTemporaryDirectory("ekklesia-modules-");
|
||||
const coreMigrationsDirectory = await createTemporaryDirectory("ekklesia-core-migrations-");
|
||||
await writeMigrationsDirectory(
|
||||
coreMigrationsDirectory,
|
||||
"core",
|
||||
"0000_identity_core_tables",
|
||||
'CREATE TABLE "ekklesia_core_user" ("id" text PRIMARY KEY);\n',
|
||||
);
|
||||
await createModule(modulesDirectory, "core");
|
||||
|
||||
let captured: unknown;
|
||||
try {
|
||||
await discoverAllMigrations({
|
||||
coreMigrationsDirectory,
|
||||
moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0" },
|
||||
});
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
|
||||
expect(captured).toBeInstanceOf(ModuleMigrationDiscoveryError);
|
||||
expect((captured as ModuleMigrationDiscoveryError).issues).toEqual([
|
||||
expect.objectContaining({ code: "MODULE_MIGRATION_ID_DUPLICATE", moduleId: "core" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps a fresh, module-free repository honestly reporting only Core's migration", async () => {
|
||||
const modulesDirectory = await createTemporaryDirectory("ekklesia-modules-");
|
||||
const coreMigrationsDirectory = await createTemporaryDirectory("ekklesia-core-migrations-");
|
||||
await writeMigrationsDirectory(
|
||||
coreMigrationsDirectory,
|
||||
"core",
|
||||
"0000_identity_core_tables",
|
||||
'CREATE TABLE "ekklesia_core_user" ("id" text PRIMARY KEY);\n',
|
||||
);
|
||||
|
||||
const aggregate = await discoverAllMigrations({
|
||||
coreMigrationsDirectory,
|
||||
moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0" },
|
||||
});
|
||||
|
||||
expect(aggregate.discoveredModules).toEqual([]);
|
||||
expect(aggregate.modules).toHaveLength(1);
|
||||
expect(aggregate.modules[0]?.moduleId).toBe("core");
|
||||
expect(aggregate.migrations).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user