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 type { IdentityDrizzleDatabase } from "../src/identity/database.js"; import { createIdentityRuntime, IdentityRuntimeError, TRUSTED_CLIENT_ADDRESS_HEADER, } from "../src/identity/runtime.js"; import { identityAdapterSchema } from "../src/identity/schema.js"; function validConfig(overrides: Partial = {}): IdentityConfig { return Object.freeze({ baseUrl: "https://identity.example.test", dashboardOrigin: "https://identity.example.test", publicSignupEnabled: false, secret: "correct-horse-battery-staple-0123456789", trustedOrigins: Object.freeze(["https://identity.example.test"]), ...overrides, }); } interface FakeDbOptions { /** Makes the session-store readiness read reject, standing in for a missing relation, a dropped * column, a denied `SELECT` privilege, an unavailable connection, or a query timeout — every one * of which surfaces to this seam identically, as a rejected query. */ readonly sessionProbeError?: Error; } /** * A fake Identity Drizzle view that records every query actually issued, so a test can prove the * *absence* of identity database work — not merely that a particular helper was not called. Both * the legacy `execute` seam and the `select(...).from(...).where(...)` chain the session-store * readiness check uses are recorded into one ordered list. */ function fakeDb(options: FakeDbOptions = {}) { const queries: string[] = []; const execute = vi.fn(() => { queries.push("execute"); return Promise.resolve(undefined); }); // Drizzle's query builder is awaited directly, so the terminal `where(...)` returns a real // Promise rather than a hand-rolled thenable object. const select = vi.fn((columns: Record) => { queries.push(`select:${Object.keys(columns).sort().join(",")}`); const builder = { from: vi.fn(() => builder), where: vi.fn(() => options.sessionProbeError ? Promise.reject(options.sessionProbeError) : Promise.resolve([] as unknown[]), ), }; return builder; }); const db = { __fakeDb: true, execute, queries, select }; // The runtime only ever uses the narrow surface faked above; the cast keeps the fake honest // about that instead of stubbing all of `NodePgDatabase`, while `queries` stays observable. return db as unknown as IdentityDrizzleDatabase & { readonly queries: string[] }; } describe("Core identity runtime construction boundary", () => { beforeEach(() => { betterAuthMock.mockReset(); drizzleAdapterMock.mockClear(); }); it("does not construct Better Auth or the Drizzle adapter merely because the module is imported", () => { expect(betterAuthMock).not.toHaveBeenCalled(); expect(drizzleAdapterMock).not.toHaveBeenCalled(); }); it("constructs explicitly and returns only the two narrow transport capabilities", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb() }); expect(Object.keys(runtime).sort()).toEqual(["handleAuthRequest", "resolvePrincipal"]); expect(typeof runtime.handleAuthRequest).toBe("function"); expect(typeof runtime.resolvePrincipal).toBe("function"); expect(Object.isFrozen(runtime)).toBe(true); }); it("does not expose the raw database, adapter, or Better Auth instance on the returned value", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); const db = fakeDb(); const runtime = createIdentityRuntime({ config: validConfig(), db }); expect(JSON.stringify(runtime)).toBe("{}"); }); it("passes the explicit ekklesia_core_* schema mapping to the Drizzle adapter", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); const db = fakeDb(); createIdentityRuntime({ config: validConfig(), db }); expect(drizzleAdapterMock).toHaveBeenCalledTimes(1); const [passedDb, options] = drizzleAdapterMock.mock.calls[0] as [unknown, unknown]; expect(passedDb).toBe(db); expect(options).toEqual({ provider: "pg", schema: identityAdapterSchema }); }); it("enables exactly email/password, verification, and rate limiting, nothing else", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); createIdentityRuntime({ config: validConfig(), db: fakeDb() }); expect(betterAuthMock).toHaveBeenCalledTimes(1); const [options] = betterAuthMock.mock.calls[0] as [Record]; // Exhaustive on purpose: a new top-level Better Auth feature has to be added here deliberately, // never by inheriting a default. expect(Object.keys(options).sort()).toEqual([ "advanced", "baseURL", "database", "emailAndPassword", "emailVerification", "logger", "onAPIError", "rateLimit", "secret", "trustedOrigins", ]); expect(options.emailAndPassword).toEqual({ autoSignIn: false, disableSignUp: true, enabled: true, maxPasswordLength: 128, minPasswordLength: 8, // A verified address is a precondition for holding a session (ADR 0038). requireEmailVerification: true, resetPasswordTokenExpiresIn: 3600, // Password reset revokes that human's other sessions, so recovering from a compromise is not // cosmetic. revokeSessionsOnPasswordReset: true, }); expect(options.emailVerification).toEqual({ // Following an emailed link never mints a session: mail infrastructure dereferences links. autoSignInAfterVerification: false, expiresIn: 3600, sendOnSignIn: true, sendOnSignUp: true, }); // No `customRules`: production inherits Better Auth's own stricter per-path defaults. expect(options.rateLimit).toEqual({ enabled: true, max: 100, window: 10 }); expect(options.baseURL).toBe(validConfig().baseUrl); expect(options.secret).toBe(validConfig().secret); expect(options.trustedOrigins).toEqual([...validConfig().trustedOrigins]); }); it("wires the injected notifier into both delivery callbacks, and neither without it", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); const sendResetPassword = vi.fn(async () => undefined); const sendVerificationEmail = vi.fn(async () => undefined); createIdentityRuntime({ config: validConfig(), db: fakeDb(), notifications: { sendResetPassword, sendVerificationEmail }, }); const [options] = betterAuthMock.mock.calls[0] as [Record]; expect((options.emailAndPassword as Record).sendResetPassword).toBe( sendResetPassword, ); expect((options.emailVerification as Record).sendVerificationEmail).toBe( sendVerificationEmail, ); betterAuthMock.mockClear(); createIdentityRuntime({ config: validConfig(), db: fakeDb() }); const [withoutMail] = betterAuthMock.mock.calls[0] as [Record]; // An installation with no mail configures the identical policy and simply cannot deliver. // Crucially it does **not** relax `requireEmailVerification`. expect(withoutMail.emailAndPassword).toMatchObject({ requireEmailVerification: true }); expect( (withoutMail.emailAndPassword as Record).sendResetPassword, ).toBeUndefined(); expect( (withoutMail.emailVerification as Record).sendVerificationEmail, ).toBeUndefined(); }); it("enables sign-up (disableSignUp: false) only when publicSignupEnabled is explicitly true", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); createIdentityRuntime({ config: validConfig({ publicSignupEnabled: true }), db: fakeDb() }); const [options] = betterAuthMock.mock.calls[0] as [Record]; expect(options.emailAndPassword).toMatchObject({ disableSignUp: false }); }); it("explicitly disables Better Auth's default test-environment origin/CSRF skip", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); createIdentityRuntime({ config: validConfig(), db: fakeDb() }); const [options] = betterAuthMock.mock.calls[0] as [Record]; expect(options.advanced).toEqual({ disableCSRFCheck: false, disableOriginCheck: false, ipAddress: { ipAddressHeaders: [TRUSTED_CLIENT_ADDRESS_HEADER] }, }); }); it("trusts exactly the one platform-owned client-address header, never Better Auth's own default", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); createIdentityRuntime({ config: validConfig(), db: fakeDb() }); const [options] = betterAuthMock.mock.calls[0] as [Record]; const advanced = options.advanced as { ipAddress?: { ipAddressHeaders?: string[] } }; expect(advanced.ipAddress?.ipAddressHeaders).toEqual([TRUSTED_CLIENT_ADDRESS_HEADER]); expect(advanced.ipAddress?.ipAddressHeaders).not.toContain("x-forwarded-for"); }); it("configures onAPIError.throw so an unexpected failure escapes Better Auth's own unsanitized fallback logger instead of being swallowed internally", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); createIdentityRuntime({ config: validConfig(), db: fakeDb() }); const [options] = betterAuthMock.mock.calls[0] as [Record]; expect(options.onAPIError).toEqual({ throw: true }); }); it("configures an explicit, error-only Better Auth logger that never forwards Better Auth's own message or arguments", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); createIdentityRuntime({ config: validConfig(), db: fakeDb() }); const [options] = betterAuthMock.mock.calls[0] as [Record]; const loggerOption = options.logger as { disabled?: boolean; level?: string; log?: (...args: unknown[]) => void; }; expect(loggerOption.disabled).toBe(false); expect(loggerOption.level).toBe("error"); expect(typeof loggerOption.log).toBe("function"); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); try { const hostileException = new Error( "password=hunter2 host=db.internal.example port=5432 statement_timeout at /var/app/src/secret.ts:10:5", ); loggerOption.log?.( "error", "Invalid origin: https://evil.example.test", hostileException, "select * from ekklesia_core_session", ); expect(errorSpy).toHaveBeenCalledTimes(1); const emitted = errorSpy.mock.calls .flat() .map((value) => value instanceof Error ? `${value.message}\n${value.stack ?? ""}` : String(value), ) .join("\n"); expect(emitted).not.toContain("hunter2"); expect(emitted).not.toContain("db.internal.example"); expect(emitted).not.toContain("5432"); expect(emitted).not.toContain("/var/app"); expect(emitted).not.toContain("evil.example.test"); expect(emitted.toLowerCase()).not.toContain("select"); expect(emitted.toLowerCase()).not.toContain("stack"); } finally { errorSpy.mockRestore(); } }); it("never publishes a Better Auth log at or below warn level", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); createIdentityRuntime({ config: validConfig(), db: fakeDb() }); const [options] = betterAuthMock.mock.calls[0] as [Record]; const loggerOption = options.logger as { log?: (...args: unknown[]) => void }; const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined); const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); try { loggerOption.log?.("warn", "a warning", "detail"); loggerOption.log?.("info", "some info", "detail"); loggerOption.log?.("debug", "some debug", "detail"); expect(errorSpy).not.toHaveBeenCalled(); expect(warnSpy).not.toHaveBeenCalled(); expect(logSpy).not.toHaveBeenCalled(); } finally { errorSpy.mockRestore(); warnSpy.mockRestore(); logSpy.mockRestore(); } }); it("only widens the four strict rate-limit windows when a narrow override is explicitly injected", () => { betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler: vi.fn() }); createIdentityRuntime({ config: validConfig(), db: fakeDb(), rateLimitOverride: { max: 1000, window: 1 }, }); const [options] = betterAuthMock.mock.calls[0] as [Record]; expect(options.rateLimit).toEqual({ customRules: { // The two paths that trigger an email are included, so a test suite driving many recovery // requests is not throttled by Better Auth's 3-per-60-seconds default for them. "/request-password-reset": { max: 1000, window: 1 }, "/send-verification-email": { max: 1000, window: 1 }, "/sign-in/email": { max: 1000, window: 1 }, "/sign-up/email": { max: 1000, window: 1 }, }, enabled: true, max: 100, window: 10, }); }); 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 { createIdentityRuntime({ config: validConfig(), db: fakeDb() }); } catch (error) { captured = error; } expect(captured).toBeInstanceOf(IdentityRuntimeError); 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"); }); }); describe("Core identity runtime — handleAuthRequest", () => { beforeEach(() => { betterAuthMock.mockReset(); drizzleAdapterMock.mockClear(); }); it("delegates directly to Better Auth's Fetch handler and passes the response through unmodified", async () => { const expectedResponse = new Response("ok", { status: 200 }); const handler = vi.fn().mockResolvedValue(expectedResponse); betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler }); const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb() }); const request = new Request("https://identity.example.test/api/auth/sign-in/email"); const response = await runtime.handleAuthRequest(request); expect(handler).toHaveBeenCalledWith(request); expect(response).toBe(expectedResponse); }); it("passes through Better Auth's own error responses (e.g. 401, 403, 422) unmodified", async () => { const errorResponse = new Response(JSON.stringify({ code: "INVALID_EMAIL_OR_PASSWORD" }), { status: 401, }); const handler = vi.fn().mockResolvedValue(errorResponse); betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler }); const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb() }); const response = await runtime.handleAuthRequest( new Request("https://identity.example.test/x"), ); expect(response.status).toBe(401); }); it("sanitizes a genuinely unexpected handler failure", async () => { const handler = vi.fn().mockRejectedValue(new Error("postgresql://admin:secret@db.internal/x")); betterAuthMock.mockReturnValue({ api: { getSession: vi.fn() }, handler }); const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb() }); let captured: unknown; try { await runtime.handleAuthRequest(new Request("https://identity.example.test/x")); } catch (error) { captured = error; } expect(captured).toBeInstanceOf(IdentityRuntimeError); expect(captured).toMatchObject({ code: "AUTH_REQUEST_FAILED" }); const publicText = `${String(captured)}\n${ captured instanceof Error ? (captured.stack ?? "") : "" }`; expect(publicText).not.toContain("db.internal"); expect(publicText).not.toContain("admin"); }); }); describe("Core identity runtime — resolvePrincipal", () => { beforeEach(() => { betterAuthMock.mockReset(); drizzleAdapterMock.mockClear(); }); /** The configured production (https) session-cookie name, derived by Better Auth's own * `getCookies` helper from the exact same `baseUrl` this suite's `validConfig` uses. */ const productionSessionCookie = "__Secure-better-auth.session_token"; it("maps a valid session to an authenticated principal containing only the opaque user id, without any follow-up probe", async () => { const getSession = vi.fn().mockResolvedValue({ session: { id: "sess_1", token: "tok" }, user: { email: "person@example.test", id: "user_123", name: "Person" }, }); betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() }); const db = fakeDb(); const runtime = createIdentityRuntime({ config: validConfig(), db }); const principal = await runtime.resolvePrincipal( new Headers({ cookie: `${productionSessionCookie}=tok` }), ); expect(principal).toEqual({ kind: "authenticated", userId: "user_123" }); expect(Object.keys(principal)).toEqual(["kind", "userId"]); expect(Object.isFrozen(principal)).toBe(true); // A resolved session needs no readiness confirmation — the store demonstrably worked. expect(db.queries).toEqual([]); }); it("performs zero identity database work for no cookie, unrelated cookies, or session-cookie lookalikes", async () => { const getSession = vi.fn().mockResolvedValue(null); betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() }); for (const cookie of [ undefined, "x=y", "a=1; b=2; c=3", // Lookalikes: a prefix, a suffix, a differently-cased spelling, a percent-encoded name, and // the unprefixed development name (wrong for this https configuration). `${productionSessionCookie}_extra=v`, `extra_${productionSessionCookie}=v`, `${productionSessionCookie.toUpperCase()}=v`, "__Secure-better-auth%2Esession_token=v", "better-auth.session_token=v", ]) { const db = fakeDb(); const runtime = createIdentityRuntime({ config: validConfig(), db }); const principal = await runtime.resolvePrincipal( cookie === undefined ? new Headers() : new Headers({ cookie }), ); expect(principal).toEqual({ kind: "anonymous" }); expect(db.queries).toEqual([]); } }); it("reads the real session relation and its expected columns only when the configured session cookie was presented", async () => { const getSession = vi.fn().mockResolvedValue(null); betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() }); const db = fakeDb(); const runtime = createIdentityRuntime({ config: validConfig(), db }); const principal = await runtime.resolvePrincipal( new Headers({ cookie: `${productionSessionCookie}=stale-or-garbage` }), ); // A healthy session store plus an unresolvable token is a genuinely anonymous caller. expect(principal).toEqual({ kind: "anonymous" }); // The readiness check is a real read of the session relation's expected columns — never // `select 1`, and never a generic pool ping. expect(db.queries).toEqual(["select:expiresAt,id,token,userId"]); }); it("detects the session cookie among several cookies and with irregular whitespace", async () => { const getSession = vi.fn().mockResolvedValue(null); betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() }); for (const cookie of [ `a=1; ${productionSessionCookie}=tok; b=2`, ` ${productionSessionCookie}=tok `, `a=1;${productionSessionCookie}=tok`, // A duplicated session cookie is ambiguous and must fail safe *towards* checking the store. `${productionSessionCookie}=one; ${productionSessionCookie}=two`, // A valueless session cookie still means Better Auth saw the name. `${productionSessionCookie}`, ]) { const db = fakeDb(); const runtime = createIdentityRuntime({ config: validConfig(), db }); await runtime.resolvePrincipal(new Headers({ cookie })); expect(db.queries).toEqual(["select:expiresAt,id,token,userId"]); } }); it("uses the unprefixed development cookie name when the configured base URL is http", async () => { const getSession = vi.fn().mockResolvedValue(null); betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() }); const developmentConfig = validConfig({ baseUrl: "http://localhost:3000", trustedOrigins: Object.freeze(["http://localhost:3000"]), }); const unprefixed = fakeDb(); await createIdentityRuntime({ config: developmentConfig, db: unprefixed }).resolvePrincipal( new Headers({ cookie: "better-auth.session_token=tok" }), ); expect(unprefixed.queries).toEqual(["select:expiresAt,id,token,userId"]); // The production `__Secure-` name is *not* what an http deployment reads. const prefixed = fakeDb(); await createIdentityRuntime({ config: developmentConfig, db: prefixed }).resolvePrincipal( new Headers({ cookie: `${productionSessionCookie}=tok` }), ); expect(prefixed.queries).toEqual([]); }); it("never downgrades a broken session store to anonymous, and leaks no database detail", async () => { const getSession = vi.fn().mockResolvedValue(null); betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() }); // Every distinguishable session-store failure reaches this seam as a rejected read: a missing // relation, a dropped column, a denied privilege, a dead connection, and a timeout alike. const hostileFailures = [ new Error('relation "ekklesia_core_session" does not exist'), new Error('column "token" does not exist'), new Error("permission denied for table ekklesia_core_session at 10.1.2.3:5432"), new Error("connection terminated unexpectedly"), new Error("timed out"), ]; for (const sessionProbeError of hostileFailures) { const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb({ sessionProbeError }), }); let captured: unknown; try { await runtime.resolvePrincipal(new Headers({ cookie: `${productionSessionCookie}=x` })); } catch (error) { captured = error; } expect(captured).toBeInstanceOf(IdentityRuntimeError); expect(captured).toMatchObject({ code: "PRINCIPAL_RESOLUTION_FAILED" }); const publicText = `${String(captured)}\n${ captured instanceof Error ? (captured.stack ?? "") : "" }\n${JSON.stringify(captured)}`; expect(publicText).not.toContain("ekklesia_core_session"); expect(publicText).not.toContain("permission denied"); expect(publicText).not.toContain("10.1.2.3"); expect(publicText).not.toContain("does not exist"); } }); it("treats an unexpected throw from getSession itself as a sanitized infrastructure failure", async () => { const hostile = "password=hunter2 host=db.internal.example port=5432"; const getSession = vi.fn().mockRejectedValue(new Error(hostile)); betterAuthMock.mockReturnValue({ api: { getSession }, handler: vi.fn() }); const runtime = createIdentityRuntime({ config: validConfig(), db: fakeDb() }); let captured: unknown; try { await runtime.resolvePrincipal(new Headers({ cookie: "better-auth.session_token=x" })); } catch (error) { captured = error; } expect(captured).toBeInstanceOf(IdentityRuntimeError); expect(captured).toMatchObject({ code: "PRINCIPAL_RESOLUTION_FAILED" }); const publicText = `${String(captured)}\n${ captured instanceof Error ? (captured.stack ?? "") : "" }`; expect(publicText).not.toContain("hunter2"); expect(publicText).not.toContain("db.internal.example"); }); });