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,123 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const betterAuthMock = vi.hoisted(() => vi.fn());
|
||||
const drizzleAdapterMock = vi.hoisted(() =>
|
||||
vi.fn((db: unknown, options: unknown) => ({ __adapterOptions: options, db })),
|
||||
);
|
||||
|
||||
vi.mock("better-auth", () => ({ betterAuth: betterAuthMock }));
|
||||
vi.mock("better-auth/adapters/drizzle", () => ({ drizzleAdapter: drizzleAdapterMock }));
|
||||
|
||||
import type { IdentityConfig } from "../src/identity/config.js";
|
||||
import { createIdentityAuth, IdentityConstructionError } from "../src/identity/factory.js";
|
||||
import { identityAdapterSchema } from "../src/identity/schema.js";
|
||||
|
||||
function validConfig(): IdentityConfig {
|
||||
return Object.freeze({
|
||||
baseUrl: "https://identity.example.test",
|
||||
secret: "correct-horse-battery-staple-0123456789",
|
||||
trustedOrigins: Object.freeze(["https://identity.example.test"]),
|
||||
});
|
||||
}
|
||||
|
||||
describe("Core identity Better Auth construction boundary", () => {
|
||||
beforeEach(() => {
|
||||
betterAuthMock.mockReset();
|
||||
drizzleAdapterMock.mockClear();
|
||||
});
|
||||
|
||||
it("constructs explicitly with narrow injected dependencies and returns only a success marker", () => {
|
||||
betterAuthMock.mockReturnValue({ api: {}, handler: () => undefined });
|
||||
const fakeDb = { __fakeDb: true };
|
||||
|
||||
const result = createIdentityAuth({ config: validConfig(), db: fakeDb as never });
|
||||
|
||||
expect(result).toEqual({ constructed: true });
|
||||
expect(Object.keys(result)).toEqual(["constructed"]);
|
||||
expect(Object.isFrozen(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not expose the raw database, adapter, or Better Auth instance on the returned value", () => {
|
||||
betterAuthMock.mockReturnValue({ api: {}, handler: () => undefined });
|
||||
const fakeDb = { __fakeDb: true };
|
||||
|
||||
const result = createIdentityAuth({ config: validConfig(), db: fakeDb as never });
|
||||
|
||||
expect(JSON.stringify(result)).not.toContain("__fakeDb");
|
||||
expect(JSON.stringify(result)).not.toContain("handler");
|
||||
});
|
||||
|
||||
it("passes the explicit ekklesia_core_* schema mapping to the Drizzle adapter", () => {
|
||||
betterAuthMock.mockReturnValue({});
|
||||
const fakeDb = { __fakeDb: true };
|
||||
|
||||
createIdentityAuth({ config: validConfig(), db: fakeDb as never });
|
||||
|
||||
expect(drizzleAdapterMock).toHaveBeenCalledTimes(1);
|
||||
const [db, options] = drizzleAdapterMock.mock.calls[0] as [unknown, unknown];
|
||||
expect(db).toBe(fakeDb);
|
||||
expect(options).toEqual({ provider: "pg", schema: identityAdapterSchema });
|
||||
});
|
||||
|
||||
it("enables no provider or plugin beyond the four core models", () => {
|
||||
betterAuthMock.mockReturnValue({});
|
||||
|
||||
createIdentityAuth({ config: validConfig(), db: {} as never });
|
||||
|
||||
expect(betterAuthMock).toHaveBeenCalledTimes(1);
|
||||
const [options] = betterAuthMock.mock.calls[0] as [Record<string, unknown>];
|
||||
expect(Object.keys(options).sort()).toEqual([
|
||||
"baseURL",
|
||||
"database",
|
||||
"secret",
|
||||
"trustedOrigins",
|
||||
]);
|
||||
expect(options.baseURL).toBe(validConfig().baseUrl);
|
||||
expect(options.secret).toBe(validConfig().secret);
|
||||
expect(options.trustedOrigins).toEqual([...validConfig().trustedOrigins]);
|
||||
});
|
||||
|
||||
it("sanitizes a construction failure without leaking the underlying error, secret, or URL", () => {
|
||||
const hostileSecret = validConfig().secret;
|
||||
betterAuthMock.mockImplementation(() => {
|
||||
throw new Error(
|
||||
`Better Auth failed for secret ${hostileSecret} at https://internal.example.test`,
|
||||
);
|
||||
});
|
||||
|
||||
let captured: unknown;
|
||||
try {
|
||||
createIdentityAuth({ config: validConfig(), db: {} as never });
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
|
||||
expect(captured).toBeInstanceOf(IdentityConstructionError);
|
||||
expect(captured).toMatchObject({ code: "IDENTITY_CONSTRUCTION_FAILED" });
|
||||
const publicText = `${String(captured)}\n${
|
||||
captured instanceof Error ? (captured.stack ?? "") : ""
|
||||
}`;
|
||||
expect(publicText).not.toContain(hostileSecret);
|
||||
expect(publicText).not.toContain("internal.example.test");
|
||||
});
|
||||
|
||||
it("sanitizes an adapter construction failure the same way", () => {
|
||||
drizzleAdapterMock.mockImplementation(() => {
|
||||
throw new Error("adapter exploded with postgresql://admin:[email protected]/ekklesia");
|
||||
});
|
||||
|
||||
let captured: unknown;
|
||||
try {
|
||||
createIdentityAuth({ config: validConfig(), db: {} as never });
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
|
||||
expect(captured).toBeInstanceOf(IdentityConstructionError);
|
||||
const publicText = `${String(captured)}\n${
|
||||
captured instanceof Error ? (captured.stack ?? "") : ""
|
||||
}`;
|
||||
expect(publicText).not.toContain("db.internal");
|
||||
expect(publicText).not.toContain("admin");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user