import { randomBytes } from "node:crypto"; import { request as httpRequest } from "node:http"; import net from "node:net"; import { fileURLToPath } from "node:url"; import path from "node:path"; import { parseEkklesiaUserId, parsePermissionId, SYSTEM_ACTOR } from "@ekklesia/contracts"; import { bootstrapPlatformOwner, createAuthorizationDrizzleDatabase, createAuthorizationStore, } from "@ekklesia/core/authorization"; import { diagnoseEkklesia } from "@ekklesia/core/diagnostics"; import { applyModuleMigrations, createPostgresMigrationDatabase, discoverAllMigrations, } from "@ekklesia/core/migrations"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { Client, type QueryResultRow } from "pg"; import { afterAll, describe, expect, it, vi } from "vitest"; import type { ClientAddressConfigInput } from "../src/client-address.js"; import { createApiRuntimeConfig } from "../src/config.js"; import { startApi, type NodeServerTimeouts, type RunningApi } from "../src/server.js"; const integrationDatabaseUrlEnvironment = "EKKLESIA_MIGRATION_TEST_DATABASE_URL"; const coreMigrationsDirectory = path.join( path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "core", "drizzle", ); const trackedDatabases = new Set(); let databaseCounter = 0; class IntegrationHarnessError extends Error { constructor(message: string) { super(message); this.name = "IntegrationHarnessError"; this.stack = `${this.name}: ${this.message}`; } } function harnessError(message: string): IntegrationHarnessError { return new IntegrationHarnessError(message); } function baseDatabaseUrl(): string { const value = process.env[integrationDatabaseUrlEnvironment]; if (typeof value !== "string" || value.trim() === "") { throw harnessError("The API integration database is not configured."); } try { const parsed = new URL(value); if (parsed.protocol !== "postgres:" && parsed.protocol !== "postgresql:") { throw new TypeError("Unsupported protocol."); } } catch { throw harnessError("The API integration database configuration is invalid."); } return value; } function quoteDatabaseName(databaseName: string): string { if (!/^[a-z][a-z0-9_]{0,62}$/u.test(databaseName)) { throw harnessError("The generated integration database name is invalid."); } return `"${databaseName}"`; } function makeDatabaseName(label: string): string { const safeLabel = label .toLowerCase() .replaceAll(/[^a-z0-9]+/gu, "_") .replaceAll(/^_+|_+$/gu, "") .slice(0, 14); databaseCounter += 1; return `ekk_api_${process.pid}_${databaseCounter}_${safeLabel || "scenario"}_${randomBytes(4).toString("hex")}`; } function databaseUrl(databaseName: string): string { const parsed = new URL(baseDatabaseUrl()); parsed.pathname = `/${databaseName}`; parsed.searchParams.delete("application_name"); return parsed.toString(); } async function openAdministrativeClient(connectionString: string): Promise { const client = new Client({ connectionString }); client.on("error", () => undefined); try { await client.connect(); return client; } catch { try { await client.end(); } catch { // The public harness failure remains static when connection cleanup also fails. } throw harnessError("The API integration database connection failed."); } } async function safeQuery( client: Client, text: string, values: unknown[] = [], ): Promise { try { const result = await client.query(text, values); return result.rows; } catch { throw harnessError("An integration database administration operation failed."); } } async function safeEnd(client: Client): Promise { try { await client.end(); } catch { throw harnessError("An integration database connection could not be closed."); } } async function createDatabase(databaseName: string): Promise { const admin = await openAdministrativeClient(baseDatabaseUrl()); try { await safeQuery(admin, `CREATE DATABASE ${quoteDatabaseName(databaseName)} TEMPLATE template0`); } finally { await safeEnd(admin); } } async function dropDatabase(databaseName: string): Promise { const admin = await openAdministrativeClient(baseDatabaseUrl()); try { await safeQuery( admin, "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()", [databaseName], ); await safeQuery( admin, `DROP DATABASE IF EXISTS ${quoteDatabaseName(databaseName)} WITH (FORCE)`, ); trackedDatabases.delete(databaseName); } finally { await safeEnd(admin); } } async function activeConnectionCount(databaseName: string): Promise { const admin = await openAdministrativeClient(baseDatabaseUrl()); try { const rows = await safeQuery<{ count: string }>( admin, "SELECT count(*)::text AS count FROM pg_stat_activity WHERE datname = $1", [databaseName], ); return Number(rows[0]?.count ?? "0"); } finally { await safeEnd(admin); } } async function createDisposableDatabase(label: string): Promise<{ name: string; url: string }> { const name = makeDatabaseName(label); await createDatabase(name); trackedDatabases.add(name); return { name, url: databaseUrl(name) }; } async function applyCoreMigrations(url: string): Promise { const emptyModulesDirectory = await mkdtemp(path.join(tmpdir(), "ekklesia-api-no-modules-")); try { const aggregate = await discoverAllMigrations({ coreMigrationsDirectory, moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory: emptyModulesDirectory, platformVersion: "0.1.0", }, }); // Phase 4A's and Phase 4C pass 1's committed migrations must not have drifted while building // this transport slice. expect(aggregate.migrations.map((entry) => entry.identity)).toEqual([ "core:0000_identity_core_tables", "core:0001_authorization_core_tables", ]); expect(aggregate.migrations[0]?.checksum).toBe( "f8f2086ef06f7a6943e8ffc4350bb010b37aed04c0a72030d2b153c986b1325f", ); expect(aggregate.migrations[1]?.checksum).toBe( "830e1fbc26d05af08df8d8710c3d10e1f3016d32c41b8363cb661c49afcbe81e", ); await applyModuleMigrations({ aggregate, database: createPostgresMigrationDatabase(url) }); } finally { await rm(emptyModulesDirectory, { recursive: true, force: true }); } } const trackedModulesDirectories: string[] = []; const eventSymbols: symbol[] = []; async function createFixtureModulesDirectory(): Promise { const directory = await mkdtemp(path.join(tmpdir(), "ekklesia-api-installed-modules-")); trackedModulesDirectories.push(directory); return directory; } /** A dynamically imported module fixture cannot close over this test file's local variables, so * lifecycle markers cross that boundary through a `globalThis` symbol instead (same technique as * `core/tests/module-runtime.test.ts`). */ function createEventRecorder(): { events: string[]; expression: string } { const key = `ekklesia.api.integration.test.${eventSymbols.length}.${Date.now()}.${Math.random()}`; const symbol = Symbol.for(key); const events: string[] = []; (globalThis as unknown as Record)[symbol] = events; eventSymbols.push(symbol); return { events, expression: `globalThis[Symbol.for(${JSON.stringify(key)})]` }; } /** Writes a minimal, structurally valid module fixture straight to disk — real discovery, * composition, and (where a scenario actually starts the API) real module runtime execution all * run against it, nothing hand-built. Register/start/stop push markers onto `recorder.events` * instead of throwing, so both "Doctor/composition never executed this" (an empty recorder) and * "startApi actually imported, registered, and started this" (a populated recorder) can be proven * against the exact same fixture. When declared with the `calendar.events.read` permission, the * module also registers a restricted `GET /events/:eventId` route and a restricted, state-changing * `POST /events` route against that same permission, so the full HTTP mounting/enforcement chain * can be exercised live. */ async function createFixtureModule( modulesDirectory: string, id: string, recorder: { expression: string }, manifestOverrides: Record = {}, ): Promise { const moduleDirectory = path.join(modulesDirectory, id); const manifest = { manifestVersion: 3, id, name: `${id} module`, version: "0.1.0", compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0" }, permissions: [], entrypoints: { server: "./dist/server/index.js" }, ...manifestOverrides, }; const declaresCalendarRead = (manifest.permissions as ReadonlyArray<{ id: string }>).some( (declaration) => declaration.id === "calendar.events.read", ); await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true }); await Promise.all([ writeFile( path.join(moduleDirectory, "package.json"), JSON.stringify({ ekklesia: { layer: "module" }, name: `@test/module-${id}`, type: "module", version: "0.1.0", }), ), writeFile(path.join(moduleDirectory, "ekklesia.module.json"), JSON.stringify(manifest)), writeFile( path.join(moduleDirectory, "dist/server/index.js"), `export default { id: ${JSON.stringify(id)}, version: "0.1.0", register({ http }) { ${recorder.expression}.push(${JSON.stringify(`${id}:register`)}); ${ declaresCalendarRead ? `http.route({ method: "GET", path: "/events/:eventId", permission: "calendar.events.read", maxBodyBytes: 0, async handler(request, context) { return Response.json({ eventId: context.params.eventId, principal: context.principal }); }, }); http.route({ method: "POST", path: "/events", permission: "calendar.events.read", maxBodyBytes: 1024, async handler(request, context) { return Response.json({ created: true, principal: context.principal }, { status: 201 }); }, });` : "" } }, start() { ${recorder.expression}.push(${JSON.stringify(`${id}:start`)}); }, stop() { ${recorder.expression}.push(${JSON.stringify(`${id}:stop`)}); }, };`, ), ]); } afterAll(async () => { for (const databaseName of [...trackedDatabases]) { await dropDatabase(databaseName).catch(() => undefined); } for (const directory of trackedModulesDirectories.splice(0)) { await rm(directory, { recursive: true, force: true }).catch(() => undefined); } const globalValues = globalThis as unknown as Record; for (const symbol of eventSymbols.splice(0)) { delete globalValues[symbol]; } }); const identityBaseUrl = "https://identity.example.test"; const dashboardOrigin = "https://dashboard.example.test"; const untrustedOrigin = "https://evil.example.test"; function apiOrigin(running: RunningApi): string { const address = running.server.address(); if (!address || typeof address !== "object") { throw harnessError("The API integration server has no bound address."); } return `http://127.0.0.1:${address.port}`; } interface StartConfiguredApiOptions { readonly clientAddress?: ClientAddressConfigInput; readonly modulesDirectoryOverride?: string; readonly nodeServerTimeoutOverride?: Partial; /** Defaults to `true`: almost every existing scenario in this suite signs accounts up over HTTP * and needs it enabled. Pass `false` explicitly for the dedicated signup-gating scenarios. */ readonly publicSignupEnabled?: boolean; /** Defaults to a generous override so this suite's own request volume never trips Better Auth's * strict built-in sign-in/sign-up rule. The dedicated rate-limit/client-address scenarios pass * `undefined` explicitly to exercise the real, unmodified production rule instead. */ readonly rateLimitOverride?: { readonly max: number; readonly window: number } | undefined; } async function startConfiguredApi( url: string, options: StartConfiguredApiOptions = {}, ): Promise { const publicSignupEnabled = options.publicSignupEnabled ?? true; // This harness explicitly selects development configuration, for two independent reasons that // both apply to every scenario in this suite: // // 1. It connects to the disposable *plaintext* loopback PostgreSQL. Production configuration // requires certificate-verified TLS with no exception, so a plaintext test database can // only be used by a deployment that has explicitly declared itself non-production. // 2. Scenarios that create accounts over HTTP need public signup, which production refuses to // enable at all until durable abuse controls exist. // // This changes only Ekklesia's own configuration-validation strictness — `environment` never // reaches Better Auth — and the harness keeps the identical https, non-loopback base URL and // trusted origins regardless, so the production-equivalent origin/CSRF behavior ADR 0012 // established is exercised exactly the same either way. const environment = "development" as const; const config = createApiRuntimeConfig( { ...(options.clientAddress === undefined ? {} : { clientAddress: options.clientAddress }), databaseUrl: url, host: "127.0.0.1", identity: { baseUrl: identityBaseUrl, publicSignupEnabled, secret: "integration-test-secret-0123456789abcdefgh", trustedOrigins: [identityBaseUrl, dashboardOrigin], }, port: 0, }, { environment }, ); return startApi(config, { ...("rateLimitOverride" in options ? { ...(options.rateLimitOverride ? { rateLimitOverride: options.rateLimitOverride } : {}) } : { rateLimitOverride: { max: 1000, window: 1 } }), ...(options.modulesDirectoryOverride === undefined ? {} : { modulesDirectoryOverride: options.modulesDirectoryOverride }), ...(options.nodeServerTimeoutOverride === undefined ? {} : { nodeServerTimeoutOverride: options.nodeServerTimeoutOverride }), }); } async function startMigratedApi( url: string, modulesDirectoryOverride?: string, ): Promise { // Narrow, explicit test-only overrides: production composition (server.ts's `main`) never sets // either. Better Auth's built-in sign-in/sign-up rate limit (3 requests / 10s) would otherwise // reject this suite's many sequential requests; `modulesDirectoryOverride` lets a scenario point // permission composition at a real, disposable module fixture instead of this repository's own // (empty) `modules/` directory. return startConfiguredApi(url, { ...(modulesDirectoryOverride === undefined ? {} : { modulesDirectoryOverride }), }); } function setCookieValue(response: Response): string | undefined { return response.headers.get("set-cookie") ?? undefined; } function firstCookiePair(setCookieHeader: string): string { return setCookieHeader.split(",")[0]?.split(";")[0]?.trim() ?? ""; } interface SignUpResult { readonly response: Response; readonly body: { token: string | null; user: { id: string; email: string } }; } async function signUp( origin: string, email: string, password: string, headers: Record = {}, ): Promise { const response = await fetch(`${origin}/api/auth/sign-up/email`, { body: JSON.stringify({ email, name: "Integration Test User", password }), // A trusted Origin by default, matching signIn below: Node's own fetch() (and every browser) // automatically sends `Sec-Fetch-Mode: cors`, which Better Auth's first-login CSRF protection // treats as "this is a Fetch-API call" and requires a trusted Origin/Referer for regardless of // whether a cookie is present — see the dedicated no-Origin scenario below, which uses a raw // `http.request` instead of `fetch()` to exercise a genuinely Fetch-Metadata-free caller. headers: { "content-type": "application/json", origin: dashboardOrigin, ...headers }, method: "POST", }); return { body: (await response.clone().json()) as SignUpResult["body"], response }; } interface SignInResult { readonly cookiePair: string | undefined; readonly response: Response; } async function signIn( origin: string, email: string, password: string, headers: Record = {}, ): Promise { const response = await fetch(`${origin}/api/auth/sign-in/email`, { body: JSON.stringify({ email, password }), headers: { "content-type": "application/json", origin: dashboardOrigin, ...headers, }, method: "POST", }); const setCookie = setCookieValue(response); return { cookiePair: setCookie ? firstCookiePair(setCookie) : undefined, response }; } async function fetchPrincipal( origin: string, headers: Record = {}, ): Promise<{ body: unknown; response: Response }> { const response = await fetch(`${origin}/api/v1/principal`, { headers }); return { body: await response.clone().json(), response }; } /** * A raw `node:http` POST, deliberately not `fetch()`. Node's own `fetch()` (undici) — like every * browser — automatically sends `Sec-Fetch-Mode: cors` on every request, which Better Auth's * first-login CSRF protection (`formCsrfMiddleware`) treats as "this came from a Fetch-API * client" and requires a trusted Origin/Referer for regardless of cookie presence. A genuine * non-browser, non-Fetch server-to-server integration (curl, most backend HTTP client libraries, * or this raw socket) sends none of the Fetch Metadata headers, so it is unaffected by that * specific protection and needs no Origin header at all. */ function rawPostRequest( origin: string, requestPath: string, body: unknown, ): Promise<{ body: string; status: number }> { return new Promise((resolve, reject) => { const url = new URL(requestPath, origin); const payload = JSON.stringify(body); const req = httpRequest( url, { headers: { "content-length": Buffer.byteLength(payload), "content-type": "application/json", }, method: "POST", }, (res) => { const chunks: Buffer[] = []; res.on("data", (chunk) => chunks.push(chunk as Buffer)); res.on("end", () => { resolve({ body: Buffer.concat(chunks).toString("utf8"), status: res.statusCode ?? 0 }); }); }, ); req.on("error", reject); req.end(payload); }); } interface RawSignInAttemptOptions { /** Originates the outbound connection from this local address — real, distinct loopback * addresses (127.0.0.2, 127.0.0.3, ...) are all independently routable on the loopback interface, * so this proves genuinely different real socket peers, not merely different header claims. */ readonly localAddress?: string; /** A raw, unvalidated `X-Forwarded-For` header value to send — deliberately not parsed or * validated here, since these tests are proving how the *server* handles it. */ readonly forwardedFor?: string; } interface RawResponse { readonly body: string; readonly headers: Record; readonly status: number; } /** * A raw `node:http` sign-in attempt against a deliberately wrong password. Better Auth's rate * limiter keys and counts every `/sign-in*` request before ever validating credentials, so this * never needs a real account and always resolves to either `401` (accepted by the limiter, wrong * credentials) or `429` (rejected by the limiter) — the only two outcomes these tests distinguish. * Raw `node:http`, not `fetch()`, for the same Fetch-Metadata reason `rawPostRequest` documents. */ function rawSignInAttempt( origin: string, options: RawSignInAttemptOptions = {}, ): Promise { return new Promise((resolve, reject) => { const url = new URL("/api/auth/sign-in/email", origin); const payload = JSON.stringify({ email: "nobody-in-particular@example.test", password: "definitely-the-wrong-password", }); const req = httpRequest( url, { headers: { "content-length": Buffer.byteLength(payload), "content-type": "application/json", ...(options.forwardedFor === undefined ? {} : { "x-forwarded-for": options.forwardedFor }), }, ...(options.localAddress === undefined ? {} : { localAddress: options.localAddress }), method: "POST", }, (res) => { const chunks: Buffer[] = []; res.on("data", (chunk) => chunks.push(chunk as Buffer)); res.on("end", () => { resolve({ body: Buffer.concat(chunks).toString("utf8"), headers: res.headers, status: res.statusCode ?? 0, }); }); }, ); req.on("error", reject); req.end(payload); }); } describe.sequential("live API — signup, sign-in, principal, sign-out", () => { let database: { name: string; url: string }; let running: RunningApi; let origin: string; const emailOne = "alice@example.test"; const emailTwo = "bob@example.test"; const password = "correct horse battery staple"; it("applies migrations and starts the API against a freshly migrated database", async () => { database = await createDisposableDatabase("signup_flow"); await applyCoreMigrations(database.url); running = await startMigratedApi(database.url); origin = apiOrigin(running); expect(running.server.listening).toBe(true); }); it("returns the anonymous principal shape before any sign-in", async () => { const { body, response } = await fetchPrincipal(origin); expect(response.status).toBe(200); expect(body).toEqual({ kind: "anonymous" }); }); it("publicly creates the first ordinary identity via sign-up, with no automatic session (autoSignIn: false)", async () => { const { body, response } = await signUp(origin, emailOne, password); expect(response.status).toBe(200); expect(body.token).toBeNull(); expect(body.user.email).toBe(emailOne); expect(setCookieValue(response)).toBeUndefined(); }); it("does not sign in automatically after sign-up", async () => { const { body } = await fetchPrincipal(origin); expect(body).toEqual({ kind: "anonymous" }); }); it("rejects wrong credentials without leaking raw internal information", async () => { const { response } = await signIn(origin, emailOne, "wrong password entirely"); const text = await response.text(); expect(response.status).toBe(401); expect(text).not.toContain(database.url); expect(text).not.toContain("postgres"); expect(text.toLowerCase()).not.toContain("stack"); }); let sessionCookie: string; it("signs in explicitly and returns a valid, host-only, HttpOnly, SameSite=Lax, secure session cookie", async () => { const { cookiePair, response } = await signIn(origin, emailOne, password); const setCookie = setCookieValue(response); expect(response.status).toBe(200); expect(setCookie).toBeDefined(); expect(cookiePair).toBeDefined(); sessionCookie = cookiePair as string; const cookieText = setCookie as string; expect(cookieText).toContain("HttpOnly"); expect(cookieText).toContain("SameSite=Lax"); expect(cookieText).toContain("Secure"); expect(cookieText).not.toMatch(/domain=/iu); }); it("resolves the authenticated principal containing only kind and the opaque user id", async () => { const { body, response } = await fetchPrincipal(origin, { cookie: sessionCookie }); expect(response.status).toBe(200); expect(body).toMatchObject({ kind: "authenticated" }); const record = body as Record; expect(Object.keys(record).sort()).toEqual(["kind", "userId"]); expect(typeof record.userId).toBe("string"); }); it("resolves a malformed cookie as anonymous rather than failing", async () => { const { body, response } = await fetchPrincipal(origin, { cookie: "better-auth.session_token=not-a-real-token", }); expect(response.status).toBe(200); expect(body).toEqual({ kind: "anonymous" }); }); it("resolves a nonexistent cookie name as anonymous", async () => { const { body, response } = await fetchPrincipal(origin, { cookie: "unrelated=value" }); expect(response.status).toBe(200); expect(body).toEqual({ kind: "anonymous" }); }); it("does not create a second identity on duplicate sign-up, and does not leak unstable internal details", async () => { const { body, response } = await signUp(origin, emailOne, "a totally different password"); expect(response.status).toBe(200); expect(body.token).toBeNull(); expect(typeof body.user.id).toBe("string"); }); it("keeps exactly one user row for the duplicated email after the duplicate sign-up attempt", async () => { const admin = await openAdministrativeClient(database.url); try { const rows = await safeQuery<{ count: string }>( admin, 'select count(*)::text as count from "ekklesia_core_user" where email = $1', [emailOne], ); expect(rows[0]?.count).toBe("1"); } finally { await safeEnd(admin); } }); it("signs out and invalidates the session", async () => { const response = await fetch(`${origin}/api/auth/sign-out`, { body: "{}", headers: { "content-type": "application/json", cookie: sessionCookie, origin: dashboardOrigin, }, method: "POST", }); expect(response.status).toBe(200); expect(setCookieValue(response)).toBeDefined(); }); it("resolves the old cookie as anonymous after sign-out", async () => { const { body, response } = await fetchPrincipal(origin, { cookie: sessionCookie }); expect(response.status).toBe(200); expect(body).toEqual({ kind: "anonymous" }); }); let secondUserId: string; it("registers and signs in a second, distinct public account", async () => { const signUpResult = await signUp(origin, emailTwo, password); expect(signUpResult.response.status).toBe(200); const signInResult = await signIn(origin, emailTwo, password); expect(signInResult.response.status).toBe(200); expect(signInResult.cookiePair).toBeDefined(); const { body } = await fetchPrincipal(origin, { cookie: signInResult.cookiePair as string, }); const record = body as { kind: string; userId: string }; expect(record.kind).toBe("authenticated"); secondUserId = record.userId; expect(secondUserId).toBeTruthy(); }); it("gives the first and second account identical principal shapes with no elevation", async () => { const first = await signIn(origin, emailOne, password); const { body: firstBody } = await fetchPrincipal(origin, { cookie: first.cookiePair as string, }); const second = await signIn(origin, emailTwo, password); const { body: secondBody } = await fetchPrincipal(origin, { cookie: second.cookiePair as string, }); expect(Object.keys(firstBody as object).sort()).toEqual( Object.keys(secondBody as object).sort(), ); expect((firstBody as { kind: string }).kind).toBe((secondBody as { kind: string }).kind); }); it("gives neither account owner, role, permission, membership, or volunteer data at the database level", async () => { const admin = await openAdministrativeClient(database.url); try { const columns = await safeQuery<{ column_name: string }>( admin, "select column_name from information_schema.columns where table_schema = 'public' and table_name = 'ekklesia_core_user'", ); const columnNames = columns.map((row) => row.column_name); for (const forbidden of [ "role", "is_admin", "is_owner", "is_staff", "is_volunteer", "is_member", "permissions", "grants", ]) { expect(columnNames).not.toContain(forbidden); } } finally { await safeEnd(admin); } }); it("never stores a plaintext password", async () => { const admin = await openAdministrativeClient(database.url); try { const rows = await safeQuery<{ password: string | null }>( admin, `select a.password from "ekklesia_core_account" a inner join "ekklesia_core_user" u on u.id = a.user_id where u.email = $1`, [emailOne], ); const stored = rows[0]?.password; expect(stored).toBeTruthy(); expect(stored).not.toBe(password); expect(stored?.length ?? 0).toBeGreaterThan(password.length); } finally { await safeEnd(admin); } }); it("supports a genuine server-to-server sign-up request that carries no Origin header and no Fetch Metadata headers at all", async () => { const { status } = await rawPostRequest(origin, "/api/auth/sign-up/email", { email: "carol@example.test", name: "Integration Test User", password, }); expect(status).toBe(200); }); it("allows a trusted browser origin for the principal route with credentials", async () => { const response = await fetch(`${origin}/api/v1/principal`, { headers: { cookie: sessionCookie, origin: dashboardOrigin }, }); expect(response.status).toBe(200); expect(response.headers.get("access-control-allow-origin")).toBe(dashboardOrigin); expect(response.headers.get("access-control-allow-credentials")).toBe("true"); }); it("never returns Access-Control-Allow-Origin for an untrusted browser origin", async () => { const principalResponse = await fetch(`${origin}/api/v1/principal`, { headers: { origin: untrustedOrigin }, }); expect(principalResponse.headers.get("access-control-allow-origin")).toBeNull(); }); // Production-equivalent Better Auth origin/CSRF protection (ADR 0012). Explicitly setting // `advanced: { disableCSRFCheck: false, disableOriginCheck: false }` (`core/src/identity/runtime.ts`) // removes Better Auth's own `isTest()`-driven default that otherwise silently skips origin/CSRF // validation under a test runner, so the same behavior verified below is also what production // enforces. it("accepts a no-cookie first authentication attempt from a trusted browser origin", async () => { const response = await fetch(`${origin}/api/auth/sign-in/email`, { body: JSON.stringify({ email: emailOne, password }), headers: { "content-type": "application/json", origin: dashboardOrigin }, method: "POST", }); expect(response.status).toBe(200); expect(response.headers.get("access-control-allow-origin")).toBe(dashboardOrigin); }); it("rejects a no-cookie first authentication attempt from an untrusted browser origin", async () => { const response = await fetch(`${origin}/api/auth/sign-in/email`, { body: JSON.stringify({ email: emailOne, password }), headers: { "content-type": "application/json", origin: untrustedOrigin }, method: "POST", }); const text = await response.text(); expect(response.status).toBe(403); expect(response.headers.get("access-control-allow-origin")).toBeNull(); expect(text).not.toContain(database.url); expect(text.toLowerCase()).not.toContain("stack"); }); it("rejects a cookie-bearing state-changing request from an untrusted browser origin, and does not invalidate the session", async () => { const { cookiePair } = await signIn(origin, emailOne, password); expect(cookiePair).toBeDefined(); const response = await fetch(`${origin}/api/auth/sign-out`, { body: "{}", headers: { "content-type": "application/json", cookie: cookiePair as string, origin: untrustedOrigin, }, method: "POST", }); const text = await response.text(); expect(response.status).toBe(403); expect(response.headers.get("access-control-allow-origin")).toBeNull(); expect(text).not.toContain(database.url); expect(text.toLowerCase()).not.toContain("stack"); // The forged/rejected sign-out must never have reached sign-out logic: the session presented // alongside it must still be valid. const { body } = await fetchPrincipal(origin, { cookie: cookiePair as string }); expect(body).toMatchObject({ kind: "authenticated" }); }); it("rejects a 7-character password as too short", async () => { const { response } = await signUp(origin, "pw-short@example.test", "1234567"); expect(response.status).toBe(400); }); it("accepts an 8-character password", async () => { const { response, body } = await signUp(origin, "pw-min@example.test", "12345678"); expect(response.status).toBe(200); expect(body.user.email).toBe("pw-min@example.test"); }); it("accepts a 128-character password", async () => { const { response } = await signUp(origin, "pw-max@example.test", "a".repeat(128)); expect(response.status).toBe(200); }); it("rejects a 129-character password as too long", async () => { const { response } = await signUp(origin, "pw-over@example.test", "a".repeat(129)); expect(response.status).toBe(400); }); it("leaves Better Auth's handler unavailable outside /api/auth/*", async () => { const response = await fetch(`${origin}/sign-in/email`, { method: "POST" }); expect(response.status).toBe(404); }); it("shuts down cleanly, closing the HTTP listener and the PostgreSQL pool", async () => { await running.shutdown(); await expect(fetch(origin, { signal: AbortSignal.timeout(1_000) })).rejects.toBeTruthy(); const remainingConnections = await activeConnectionCount(database.name); expect(remainingConnections).toBe(0); await dropDatabase(database.name); }); }); describe.sequential("live API — startup and migration guarantees", () => { it("does not create any tables or migration history when started against an unmigrated database", async () => { const database = await createDisposableDatabase("unmigrated"); try { const config = createApiRuntimeConfig( { databaseUrl: database.url, host: "127.0.0.1", identity: { baseUrl: identityBaseUrl, publicSignupEnabled: true, secret: "integration-test-secret-0123456789abcdefgh", trustedOrigins: [identityBaseUrl], }, port: 0, }, // Development validation, for the same reason `startConfiguredApi` documents: this scenario // drives a real sign-up request, which production configuration refuses to enable at all. { environment: "development" }, ); const running = await startApi(config, { rateLimitOverride: { max: 1000, window: 1 } }); const origin = apiOrigin(running); try { // A request that touches the (nonexistent) identity tables fails, but must not repair // or create anything. The exact failure body is not asserted here (Better Auth's // internal-error shape for this case is not a stable public contract); only that it is // not a successful sign-up. const response = await fetch(`${origin}/api/auth/sign-up/email`, { body: JSON.stringify({ email: "dana@example.test", name: "Integration Test User", password: "correct horse battery staple", }), headers: { "content-type": "application/json" }, method: "POST", }); expect(response.status).not.toBe(200); } finally { await running.shutdown(); } const admin = await openAdministrativeClient(database.url); try { const rows = await safeQuery<{ count: string }>( admin, "select count(*)::text as count from information_schema.tables where table_schema = 'public'", ); expect(rows[0]?.count).toBe("0"); } finally { await safeEnd(admin); } } finally { await dropDatabase(database.name); } }); }); describe.sequential("live API — principal infrastructure failure is never downgraded to anonymous", () => { it("returns a sanitized failure, not a false anonymous 200, when the database becomes unreachable mid-session", async () => { const database = await createDisposableDatabase("infra_failure"); await applyCoreMigrations(database.url); const running = await startMigratedApi(database.url); const origin = apiOrigin(running); await signUp(origin, "erin@example.test", "correct horse battery staple"); const { cookiePair } = await signIn( origin, "erin@example.test", "correct horse battery staple", ); expect(cookiePair).toBeDefined(); // Force the database itself unreachable out from under the running API pool. await dropDatabase(database.name); const { body, response } = await fetchPrincipal(origin, { cookie: cookiePair as string }); expect(response.status).toBe(503); expect(body).toEqual({ code: "PRINCIPAL_RESOLUTION_FAILED" }); await running.shutdown().catch(() => undefined); }); }); describe.sequential("live API — session-store readiness against real PostgreSQL", () => { /** * Every scenario below drives a real, migrated PostgreSQL database, breaks exactly one property * of the *session store specifically*, and asserts the public contract. The administrative * connection each helper opens (`openAdministrativeClient`) is **test-only provisioning * infrastructure** — it renames a relation, drops a column, or revokes a privilege in a * disposable database, and is closed in `finally`. It is never part of the API runtime, which * continues to own exactly one pool through `startApi`. */ async function withBrokenSessionStore( label: string, breakStore: (admin: Client) => Promise, assertions: (context: { readonly cookie: string; readonly origin: string; readonly logs: string[]; }) => Promise, ): Promise { const database = await createDisposableDatabase(label); await applyCoreMigrations(database.url); const running = await startConfiguredApi(database.url); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); try { const origin = apiOrigin(running); const email = `${label}@example.test`; const password = "correct horse battery staple"; await signUp(origin, email, password); const { cookiePair } = await signIn(origin, email, password); expect(cookiePair).toBeDefined(); // The session resolves normally before anything is broken. const before = await fetchPrincipal(origin, { cookie: cookiePair as string }); expect(before.response.status).toBe(200); expect(before.body).toMatchObject({ kind: "authenticated" }); const admin = await openAdministrativeClient(database.url); try { await breakStore(admin); } finally { await safeEnd(admin); } await assertions({ cookie: cookiePair as string, logs: errorSpy.mock.calls.flat().map((value) => String(value)), origin, }); } finally { errorSpy.mockRestore(); await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } } /** The exact public contract a broken session store must produce — never anonymous, never 403, * and never carrying a relation name, column name, privilege detail, SQL, host, or stack. */ async function expectSanitizedUnavailable(origin: string, cookie: string): Promise { const response = await fetch(`${origin}/api/v1/principal`, { headers: { cookie } }); expect(response.status).toBe(503); expect(response.headers.get("cache-control")).toBe("no-store"); const text = await response.clone().text(); expect(JSON.parse(text)).toEqual({ code: "PRINCIPAL_RESOLUTION_FAILED" }); for (const forbidden of [ "ekklesia_core_session", "permission denied", "does not exist", "relation", "column", "select", "postgres", "5432", ]) { expect(text.toLowerCase()).not.toContain(forbidden.toLowerCase()); } return response; } it("returns a sanitized 503 — not anonymous — when the session table is missing, even though a generic SELECT 1 still succeeds", async () => { await withBrokenSessionStore( "session_missing_table", async (admin) => { // Prove the distinction this pass exists for: the connection is perfectly healthy for a // trivial statement, so the previous `select 1` probe would have reported "reachable" and // the caller would have been reported as genuinely anonymous. const trivial = await safeQuery<{ one: number }>(admin, "SELECT 1 AS one"); expect(trivial[0]?.one).toBe(1); await safeQuery( admin, 'ALTER TABLE "ekklesia_core_session" RENAME TO "ekklesia_core_session_moved"', ); }, async ({ cookie, origin }) => { await expectSanitizedUnavailable(origin, cookie); }, ); }); it("returns a sanitized 503 when an expected session column is missing", async () => { await withBrokenSessionStore( "session_missing_column", async (admin) => { await safeQuery(admin, 'ALTER TABLE "ekklesia_core_session" DROP COLUMN "token" CASCADE'); }, async ({ cookie, origin }) => { await expectSanitizedUnavailable(origin, cookie); }, ); }); it("returns a sanitized 503 when SELECT privilege on the session table is genuinely revoked", async () => { // A real privilege denial needs the API to connect as a role that is *not* the table owner — // an owner's implicit rights cannot be revoked. This provisions a dedicated restricted role // (test-only infrastructure), grants it everything except SELECT on the session table, and // runs the API entirely as that role. const database = await createDisposableDatabase("session_denied"); await applyCoreMigrations(database.url); const restrictedRole = `ekk_restricted_${randomBytes(4).toString("hex")}`; const restrictedPassword = randomBytes(12).toString("hex"); const admin = await openAdministrativeClient(database.url); try { await safeQuery( admin, `CREATE ROLE "${restrictedRole}" LOGIN PASSWORD '${restrictedPassword}'`, ); await safeQuery(admin, `GRANT USAGE ON SCHEMA public TO "${restrictedRole}"`); await safeQuery( admin, `GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "${restrictedRole}"`, ); // The one thing this role may not do: read the session relation. await safeQuery(admin, `REVOKE ALL ON "ekklesia_core_session" FROM "${restrictedRole}"`); } finally { await safeEnd(admin); } const restrictedUrl = (() => { const parsed = new URL(database.url); parsed.username = restrictedRole; parsed.password = restrictedPassword; return parsed.toString(); })(); const running = await startConfiguredApi(restrictedUrl); try { const origin = apiOrigin(running); // The restricted role can still run a trivial statement and can still read non-session // tables, so only the session store specifically is broken. const response = await fetch(`${origin}/api/v1/principal`, { headers: { cookie: "__Secure-better-auth.session_token=any-token-shape" }, }); const text = await response.clone().text(); expect(response.status).toBe(503); expect(response.headers.get("cache-control")).toBe("no-store"); expect(JSON.parse(text)).toEqual({ code: "PRINCIPAL_RESOLUTION_FAILED" }); expect(text).not.toContain("ekklesia_core_session"); expect(text.toLowerCase()).not.toContain("permission denied"); expect(text).not.toContain(restrictedRole); } finally { await running.shutdown().catch(() => undefined); // Drop the disposable database first (it holds the role's dependencies), then the role. await dropDatabase(database.name).catch(() => undefined); const cleanup = await openAdministrativeClient(baseDatabaseUrl()); try { await safeQuery(cleanup, `DROP ROLE IF EXISTS "${restrictedRole}"`); } finally { await safeEnd(cleanup); } } }); it("keeps a genuinely unknown, expired, or revoked session anonymous while the store is healthy", async () => { const database = await createDisposableDatabase("session_healthy"); await applyCoreMigrations(database.url); const running = await startConfiguredApi(database.url); try { const origin = apiOrigin(running); const email = "healthy-store@example.test"; const password = "correct horse battery staple"; await signUp(origin, email, password); const { cookiePair } = await signIn(origin, email, password); expect(cookiePair).toBeDefined(); const cookieName = (cookiePair as string).split("=")[0] as string; // A completely unknown token against a healthy store is genuinely anonymous. const unknown = await fetchPrincipal(origin, { cookie: `${cookieName}=not-a-real-token` }); expect(unknown.response.status).toBe(200); expect(unknown.body).toEqual({ kind: "anonymous" }); // A revoked (signed-out) session is likewise genuinely anonymous, not a 503. const signOut = await fetch(`${origin}/api/auth/sign-out`, { body: "{}", headers: { "content-type": "application/json", cookie: cookiePair as string, origin: dashboardOrigin, }, method: "POST", }); expect(signOut.status).toBe(200); const revoked = await fetchPrincipal(origin, { cookie: cookiePair as string }); expect(revoked.response.status).toBe(200); expect(revoked.body).toEqual({ kind: "anonymous" }); // An expired session (expiry forced into the past) is anonymous, not a 503. const admin = await openAdministrativeClient(database.url); try { await safeQuery( admin, `UPDATE "ekklesia_core_session" SET expires_at = now() - interval '1 day'`, ); } finally { await safeEnd(admin); } const secondSignIn = await signIn(origin, email, password); const expiredCookie = secondSignIn.cookiePair as string; const adminAgain = await openAdministrativeClient(database.url); try { await safeQuery( adminAgain, `UPDATE "ekklesia_core_session" SET expires_at = now() - interval '1 day'`, ); } finally { await safeEnd(adminAgain); } const expired = await fetchPrincipal(origin, { cookie: expiredCookie }); expect(expired.response.status).toBe(200); expect(expired.body).toEqual({ kind: "anonymous" }); } finally { await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); it("performs no identity database work for an unrelated cookie, proven against a database whose session table is missing", async () => { const database = await createDisposableDatabase("session_unrelated_cookie"); await applyCoreMigrations(database.url); const running = await startConfiguredApi(database.url); try { const origin = apiOrigin(running); // Break the session store outright. Any request that actually touched it would now 503. const admin = await openAdministrativeClient(database.url); try { await safeQuery( admin, 'ALTER TABLE "ekklesia_core_session" RENAME TO "ekklesia_core_session_moved"', ); } finally { await safeEnd(admin); } // Unrelated cookies must still resolve as anonymous with a 200: they never triggered // session-store work in the first place. This is the original reproduction, inverted. for (const cookie of ["x=y", "a=1; b=2; c=3", "better_auth_session_token=v"]) { const { body, response } = await fetchPrincipal(origin, { cookie }); expect(response.status).toBe(200); expect(body).toEqual({ kind: "anonymous" }); } // No cookie at all behaves identically. const none = await fetchPrincipal(origin); expect(none.response.status).toBe(200); expect(none.body).toEqual({ kind: "anonymous" }); } finally { await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); }); describe.sequential("live API — Better Auth logging never leaks operational detail", () => { it("emits no raw exception, SQL, connection detail, origin header, or stack trace when a real identity database failure occurs", async () => { const database = await createDisposableDatabase("logging_leak"); await applyCoreMigrations(database.url); const running = await startMigratedApi(database.url); const origin = apiOrigin(running); await signUp(origin, "frank@example.test", "correct horse battery staple"); // Force the database itself unreachable out from under the running API pool, then drive a // request through Better Auth's own database-backed handler (not Ekklesia's own principal // route) so Better Auth's internal logging — configured in `core/src/identity/runtime.ts` — // is the thing actually being exercised here, not Core's own error handling. Without // `onAPIError: { throw: true }`, the pinned release's own `better-call` transport dependency // would catch this internally and print it raw via an unconditional // `console.error("# SERVER_ERROR: ", error)` that no `logger` configuration can reach; with // it, the failure instead propagates out of `auth.handler()` and becomes a sanitized // `IdentityRuntimeError` at `handleAuthRequest`'s own boundary, and nothing is logged at all. await dropDatabase(database.name); 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 { const response = await fetch(`${origin}/api/auth/sign-in/email`, { body: JSON.stringify({ email: "frank@example.test", password: "correct horse battery staple", }), headers: { "content-type": "application/json", origin: dashboardOrigin }, method: "POST", }); const body = await response.clone().json(); expect(response.status).not.toBe(200); expect(body).toEqual({ code: "AUTH_REQUEST_FAILED" }); const emitted = [...errorSpy.mock.calls, ...warnSpy.mock.calls, ...logSpy.mock.calls] .flat() .map((value) => value instanceof Error ? `${value.message}\n${value.stack ?? ""}` : String(value), ) .join("\n"); // The primary guarantee: onAPIError.throw routes this exact failure out to Core's own // sanitized boundary before Better Auth (or its own transport dependency) ever gets a // chance to log anything about it, so nothing is emitted at all. expect(emitted).toBe(""); // Defense in depth, kept even though the assertion above is currently sufficient: if a // future change causes something to be logged after all, it must still never contain any // of this. expect(emitted).not.toContain(database.url); expect(emitted).not.toContain(database.name); expect(emitted.toLowerCase()).not.toContain("password"); expect(emitted.toLowerCase()).not.toContain("stack"); expect(emitted).not.toMatch(/select|insert|update|delete|drizzle/iu); expect(emitted).not.toContain(dashboardOrigin); expect(emitted).not.toContain(identityBaseUrl); } finally { errorSpy.mockRestore(); warnSpy.mockRestore(); logSpy.mockRestore(); await running.shutdown().catch(() => undefined); } }); }); describe.sequential("live API — platform owner and delegated grant management", () => { let database: { name: string; url: string }; let running: RunningApi; let origin: string; let ownerUserId: string; let ownerCookie: string; let managerUserId: string; let managerCookie: string; let createdGrantId: string; const ownerEmail = "authz-owner@example.test"; const managerEmail = "authz-manager@example.test"; const password = "correct horse battery staple"; // This scenario intentionally proves out the explicit-grant lifecycle (create, list, use, // revoke) against Core's own grant-management permission rather than a module's — a module // permission would exercise the exact same route and evaluator code, adding coverage breadth // without coverage depth. The "live API — installed module permission end-to-end" scenario // below is where a real installed module's declared permission is proven end-to-end instead. const testPermission = "core.authorization.grants.manage"; // Well-formed (passes `PermissionId` shape validation) but never registered — used to prove the // live route rejects it even for a caller who otherwise has full grant-management authority. const unregisteredPermission = "calendar.events.read"; it("starts a freshly migrated API and signs up the eventual owner and eventual delegated manager", async () => { database = await createDisposableDatabase("authz_http"); await applyCoreMigrations(database.url); running = await startMigratedApi(database.url); origin = apiOrigin(running); const ownerSignUp = await signUp(origin, ownerEmail, password); ownerUserId = ownerSignUp.body.user.id; const managerSignUp = await signUp(origin, managerEmail, password); managerUserId = managerSignUp.body.user.id; const ownerSignIn = await signIn(origin, ownerEmail, password); expect(ownerSignIn.cookiePair).toBeDefined(); ownerCookie = ownerSignIn.cookiePair as string; }); it("denies grants access to an anonymous caller", async () => { const response = await fetch(`${origin}/api/v1/grants?userId=${ownerUserId}`); expect(response.status).toBe(403); expect(await response.json()).toEqual({ code: "AUTHORIZATION_DENIED" }); }); it("denies grants access to an authenticated caller with no owner authority and no grant", async () => { const managerSignInBefore = await signIn(origin, managerEmail, password); const response = await fetch(`${origin}/api/v1/grants?userId=${ownerUserId}`, { headers: { cookie: managerSignInBefore.cookiePair as string }, }); expect(response.status).toBe(403); }); it("bootstraps the owner via Core's real bootstrap operation, then confirms owner authority over the HTTP grants API", async () => { // Exactly what `pnpm authorization:bootstrap-owner` would do, driven directly against the // same disposable database the running API already uses — no second process needed to prove // the HTTP layer honors real platform-owner state. const { close, db } = createAuthorizationDrizzleDatabase(database.url); try { await bootstrapPlatformOwner(db, { userId: parseEkklesiaUserId(ownerUserId) }); } finally { await close(); } const response = await fetch(`${origin}/api/v1/grants?userId=${ownerUserId}`, { headers: { cookie: ownerCookie }, }); expect(response.status).toBe(200); expect(await response.json()).toEqual({ grants: [] }); }); it("lets the owner delegate grants.manage to another account through the explicit-grant HTTP API", async () => { const response = await fetch(`${origin}/api/v1/grants`, { body: JSON.stringify({ permission: "core.authorization.grants.manage", subjectUserId: managerUserId, }), headers: { "content-type": "application/json", cookie: ownerCookie, origin: dashboardOrigin, }, method: "POST", }); const body = (await response.json()) as { grant: { sourceKind: string; userId: string } }; expect(response.status).toBe(201); expect(body.grant.sourceKind).toBe("explicit"); expect(body.grant.userId).toBe(managerUserId); }); it("lets the delegated manager — who is not a platform owner — manage another user's grants", async () => { const managerSignIn = await signIn(origin, managerEmail, password); expect(managerSignIn.cookiePair).toBeDefined(); managerCookie = managerSignIn.cookiePair as string; const response = await fetch(`${origin}/api/v1/grants?userId=${ownerUserId}`, { headers: { cookie: managerCookie }, }); expect(response.status).toBe(200); }); it("rejects a well-formed but unregistered permission even from a caller with full grant-management authority, and writes no grant or audit row", async () => { const admin = await openAdministrativeClient(database.url); try { const grantCountBefore = await safeQuery<{ count: string }>( admin, `select count(*)::text as count from "ekklesia_core_grant" where permission_id = $1`, [unregisteredPermission], ); const auditCountBefore = await safeQuery<{ count: string }>( admin, `select count(*)::text as count from "ekklesia_core_authorization_audit" where permission_id = $1`, [unregisteredPermission], ); expect(grantCountBefore[0]?.count).toBe("0"); expect(auditCountBefore[0]?.count).toBe("0"); const response = await fetch(`${origin}/api/v1/grants`, { body: JSON.stringify({ permission: unregisteredPermission, subjectUserId: managerUserId, }), headers: { "content-type": "application/json", cookie: managerCookie, origin: dashboardOrigin, }, method: "POST", }); expect(response.status).toBe(403); expect(await response.json()).toEqual({ code: "AUTHORIZATION_DENIED" }); const grantCountAfter = await safeQuery<{ count: string }>( admin, `select count(*)::text as count from "ekklesia_core_grant" where permission_id = $1`, [unregisteredPermission], ); const auditCountAfter = await safeQuery<{ count: string }>( admin, `select count(*)::text as count from "ekklesia_core_authorization_audit" where permission_id = $1`, [unregisteredPermission], ); expect(grantCountAfter[0]?.count).toBe("0"); expect(auditCountAfter[0]?.count).toBe("0"); } finally { await safeEnd(admin); } }); it("creates an explicit grant via HTTP, deriving sourceKind and actor server-side, and records an atomic audit entry", async () => { const response = await fetch(`${origin}/api/v1/grants`, { body: JSON.stringify({ permission: testPermission, scope: "calendar-event:1", subjectUserId: managerUserId, }), headers: { "content-type": "application/json", cookie: managerCookie, origin: dashboardOrigin, }, method: "POST", }); const body = (await response.json()) as { grant: { id: string; sourceKind: string; userId: string }; }; expect(response.status).toBe(201); expect(body.grant.sourceKind).toBe("explicit"); expect(body.grant.userId).toBe(managerUserId); createdGrantId = body.grant.id; const admin = await openAdministrativeClient(database.url); try { const rows = await safeQuery<{ action: string; actor_id: string | null }>( admin, `select action, actor_id from "ekklesia_core_authorization_audit" where grant_id = $1`, [createdGrantId], ); expect(rows).toEqual([{ action: "grant.create", actor_id: managerUserId }]); } finally { await safeEnd(admin); } }); it("lists the explicit grant via HTTP", async () => { const response = await fetch(`${origin}/api/v1/grants?userId=${managerUserId}`, { headers: { cookie: ownerCookie }, }); const body = (await response.json()) as { grants: Array<{ id: string }> }; expect(response.status).toBe(200); expect(body.grants.some((grant) => grant.id === createdGrantId)).toBe(true); }); it("rejects a create-grant request carrying disallowed fields such as actor or sourceKind, even alongside otherwise-valid fields", async () => { const response = await fetch(`${origin}/api/v1/grants`, { body: JSON.stringify({ actor: { type: "system" }, permission: testPermission, sourceKind: "relationship", subjectUserId: managerUserId, }), headers: { "content-type": "application/json", cookie: ownerCookie, origin: dashboardOrigin, }, method: "POST", }); expect(response.status).toBe(400); }); it("rejects a mutation request with no trusted Origin header, and never returns a permissive CORS header", async () => { const response = await fetch(`${origin}/api/v1/grants`, { body: JSON.stringify({ permission: testPermission, subjectUserId: managerUserId }), headers: { "content-type": "application/json", cookie: ownerCookie }, method: "POST", }); expect(response.status).toBe(403); expect(await response.json()).toEqual({ code: "UNTRUSTED_ORIGIN" }); expect(response.headers.get("access-control-allow-origin")).toBeNull(); }); it("rejects a mutation request from an untrusted Origin the same way", async () => { const response = await fetch(`${origin}/api/v1/grants`, { body: JSON.stringify({ permission: testPermission, subjectUserId: managerUserId }), headers: { "content-type": "application/json", cookie: ownerCookie, origin: untrustedOrigin, }, method: "POST", }); expect(response.status).toBe(403); expect(response.headers.get("access-control-allow-origin")).toBeNull(); }); it("revokes the explicit grant via HTTP and records the revocation audit entry", async () => { const response = await fetch(`${origin}/api/v1/grants/${createdGrantId}/revoke`, { headers: { cookie: ownerCookie, origin: dashboardOrigin }, method: "POST", }); expect(response.status).toBe(200); expect(await response.json()).toEqual({ success: true }); const admin = await openAdministrativeClient(database.url); try { const rows = await safeQuery<{ action: string }>( admin, `select action from "ekklesia_core_authorization_audit" where grant_id = $1 order by created_at`, [createdGrantId], ); expect(rows.map((row) => row.action)).toEqual(["grant.create", "grant.revoke"]); } finally { await safeEnd(admin); } }); it("never exposes a relationship-derived grant through the generic list, create, or revoke HTTP API", async () => { // Seeded directly through the store, simulating a module's own business operation — never // through HTTP, which has no way to express sourceKind at all. const { close, db } = createAuthorizationDrizzleDatabase(database.url); let relationshipGrantId: string; try { const store = createAuthorizationStore(db); const relationshipGrant = await store.createGrant({ actor: SYSTEM_ACTOR, permission: parsePermissionId("roster.volunteer.read"), sourceKind: "relationship", sourceRef: "roster:volunteer:99", userId: parseEkklesiaUserId(managerUserId), }); relationshipGrantId = relationshipGrant.id; } finally { await close(); } const listResponse = await fetch(`${origin}/api/v1/grants?userId=${managerUserId}`, { headers: { cookie: ownerCookie }, }); const listBody = (await listResponse.json()) as { grants: Array<{ id: string }> }; expect(listBody.grants.some((grant) => grant.id === relationshipGrantId)).toBe(false); const revokeResponse = await fetch(`${origin}/api/v1/grants/${relationshipGrantId}/revoke`, { headers: { cookie: ownerCookie, origin: dashboardOrigin }, method: "POST", }); expect(revokeResponse.status).toBe(404); expect(await revokeResponse.json()).toEqual({ code: "AUTHORIZATION_GRANT_NOT_FOUND" }); }); it("shuts down cleanly, closing the one shared pool", async () => { await running.shutdown(); const remainingConnections = await activeConnectionCount(database.name); expect(remainingConnections).toBe(0); await dropDatabase(database.name); }); }); describe.sequential("live API — authorization infrastructure failure is never downgraded to a false denial", () => { it("returns a sanitized 503, not a 403, when the database becomes unreachable mid-session", async () => { const database = await createDisposableDatabase("authz_infra_failure"); await applyCoreMigrations(database.url); const running = await startMigratedApi(database.url); const origin = apiOrigin(running); await signUp(origin, "authz-infra@example.test", "correct horse battery staple"); const { cookiePair } = await signIn( origin, "authz-infra@example.test", "correct horse battery staple", ); expect(cookiePair).toBeDefined(); // Force the database itself unreachable out from under the running API pool — the same // technique the identity infra-failure test above uses, applied to a grants route this time. await dropDatabase(database.name); const response = await fetch(`${origin}/api/v1/grants?userId=does-not-matter`, { headers: { cookie: cookiePair as string }, }); expect(response.status).toBe(503); const body = (await response.json()) as { code: string }; expect(["PRINCIPAL_RESOLUTION_FAILED", "AUTHORIZATION_EVALUATION_FAILED"]).toContain(body.code); await running.shutdown().catch(() => undefined); }); }); describe.sequential("live API — installed module permission end-to-end", () => { let database: { name: string; url: string }; let modulesDirectory: string; let running: RunningApi; let origin: string; let ownerUserId: string; let ownerCookie: string; let createdGrantId: string; const ownerEmail = "authz-module-owner@example.test"; const password = "correct horse battery staple"; const modulePermission = "calendar.events.read"; const absentPermission = "scheduler.jobs.read"; const recorder = createEventRecorder(); it("recognizes a real installed module and passes permission composition in Doctor, without executing its server entrypoint", async () => { modulesDirectory = await createFixtureModulesDirectory(); await createFixtureModule(modulesDirectory, "calendar", recorder, { permissions: [ { baseline: "restricted", description: "Read calendar events.", id: modulePermission, }, ], }); const report = await diagnoseEkklesia({ moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }, runtimeRange: ">=0.0.0", runtimeVersion: process.versions.node, }); // Only the module-discovery and permission-composition checks are asserted here — this // fixture's temporary directory has no `core/drizzle` sibling for migrations discovery to // find, which is unrelated to what this scenario proves and would otherwise make the check // fail for a reason that has nothing to do with permission composition. const moduleCheck = report.checks.find((entry) => entry.id === "modules.discovery"); const permissionsCheck = report.checks.find((entry) => entry.id === "modules.permissions"); expect(moduleCheck).toMatchObject({ status: "pass" }); expect(moduleCheck?.details).toEqual(["calendar 0.1.0"]); expect(permissionsCheck).toMatchObject({ details: ["2 permissions registered"], status: "pass", }); // Doctor and permission composition read only the manifest — the fixture's register/start // markers must remain entirely unset. expect(recorder.events).toEqual([]); }); it("starts a freshly migrated API that discovers the fixture module, imports and starts its runtime exactly once, and mounts its route", async () => { database = await createDisposableDatabase("authz_module"); await applyCoreMigrations(database.url); running = await startMigratedApi(database.url, modulesDirectory); origin = apiOrigin(running); // Unlike Doctor above, a successful production startup does deliberately import, register, // and start the module's runtime — exactly once each. expect(recorder.events).toEqual(["calendar:register", "calendar:start"]); const ownerSignUp = await signUp(origin, ownerEmail, password); ownerUserId = ownerSignUp.body.user.id; const ownerSignIn = await signIn(origin, ownerEmail, password); expect(ownerSignIn.cookiePair).toBeDefined(); ownerCookie = ownerSignIn.cookiePair as string; // Existing identity HTTP behavior — sign-up, sign-in, principal resolution — is unaffected by // module permission composition running before it. const { body: principal } = await fetchPrincipal(origin, { cookie: ownerCookie }); expect(principal).toEqual({ kind: "authenticated", userId: ownerUserId }); }); it("lets an authorized platform owner create an explicit grant for the installed module's permission, with an atomic audit entry using its declared id", async () => { const { close, db } = createAuthorizationDrizzleDatabase(database.url); try { await bootstrapPlatformOwner(db, { userId: parseEkklesiaUserId(ownerUserId) }); } finally { await close(); } const response = await fetch(`${origin}/api/v1/grants`, { body: JSON.stringify({ permission: modulePermission, subjectUserId: ownerUserId }), headers: { "content-type": "application/json", cookie: ownerCookie, origin: dashboardOrigin, }, method: "POST", }); const body = (await response.json()) as { grant: { id: string; permission: string; sourceKind: string }; }; expect(response.status).toBe(201); expect(body.grant.permission).toBe(modulePermission); expect(body.grant.sourceKind).toBe("explicit"); createdGrantId = body.grant.id; const admin = await openAdministrativeClient(database.url); try { const rows = await safeQuery<{ action: string; permission_id: string }>( admin, `select action, permission_id from "ekklesia_core_authorization_audit" where grant_id = $1`, [createdGrantId], ); expect(rows).toEqual([{ action: "grant.create", permission_id: modulePermission }]); } finally { await safeEnd(admin); } }); it("denies the module's own mounted route to an anonymous caller, then serves it to the newly granted owner with the decoded path parameter and authenticated principal", async () => { const anonymousResponse = await fetch(`${origin}/api/v1/modules/calendar/events/evt-42`); expect(anonymousResponse.status).toBe(403); expect(await anonymousResponse.json()).toEqual({ code: "AUTHORIZATION_DENIED" }); const grantedResponse = await fetch(`${origin}/api/v1/modules/calendar/events/evt-42`, { headers: { cookie: ownerCookie }, }); expect(grantedResponse.status).toBe(200); const body = (await grantedResponse.json()) as { eventId: string; principal: { kind: string; userId: string }; }; expect(body.eventId).toBe("evt-42"); expect(body.principal).toEqual({ kind: "authenticated", userId: ownerUserId }); }); it("requires a trusted Origin for the module's own state-changing route, exactly like the platform's own mutation routes", async () => { const noOriginResponse = await fetch(`${origin}/api/v1/modules/calendar/events`, { body: JSON.stringify({}), headers: { "content-type": "application/json", cookie: ownerCookie }, method: "POST", }); expect(noOriginResponse.status).toBe(403); expect(await noOriginResponse.json()).toEqual({ code: "UNTRUSTED_ORIGIN" }); const untrustedResponse = await fetch(`${origin}/api/v1/modules/calendar/events`, { body: JSON.stringify({}), headers: { "content-type": "application/json", cookie: ownerCookie, origin: untrustedOrigin, }, method: "POST", }); expect(untrustedResponse.status).toBe(403); const trustedResponse = await fetch(`${origin}/api/v1/modules/calendar/events`, { body: JSON.stringify({}), headers: { "content-type": "application/json", cookie: ownerCookie, origin: dashboardOrigin, }, method: "POST", }); expect(trustedResponse.status).toBe(201); expect(await trustedResponse.json()).toMatchObject({ created: true }); }); it("rejects a well-formed permission absent from the composed registry, writing no grant or audit row", async () => { const admin = await openAdministrativeClient(database.url); try { const before = await safeQuery<{ count: string }>( admin, `select count(*)::text as count from "ekklesia_core_grant" where permission_id = $1`, [absentPermission], ); expect(before[0]?.count).toBe("0"); const response = await fetch(`${origin}/api/v1/grants`, { body: JSON.stringify({ permission: absentPermission, subjectUserId: ownerUserId }), headers: { "content-type": "application/json", cookie: ownerCookie, origin: dashboardOrigin, }, method: "POST", }); expect(response.status).toBe(403); expect(await response.json()).toEqual({ code: "AUTHORIZATION_DENIED" }); const after = await safeQuery<{ count: string }>( admin, `select count(*)::text as count from "ekklesia_core_grant" where permission_id = $1`, [absentPermission], ); const audit = await safeQuery<{ count: string }>( admin, `select count(*)::text as count from "ekklesia_core_authorization_audit" where permission_id = $1`, [absentPermission], ); expect(after[0]?.count).toBe("0"); expect(audit[0]?.count).toBe("0"); } finally { await safeEnd(admin); } }); it("shuts down cleanly, then restarting with the module absent rejects new grants for its former permission, returns NOT_FOUND for its former route, while the historical grant remains listable", async () => { await running.shutdown(); const remainingConnections = await activeConnectionCount(database.name); expect(remainingConnections).toBe(0); // Exactly one stop, matching the exactly-one register/start already asserted above. expect(recorder.events.filter((event) => event === "calendar:stop")).toHaveLength(1); const emptyModulesDirectory = await createFixtureModulesDirectory(); running = await startMigratedApi(database.url, emptyModulesDirectory); origin = apiOrigin(running); // The module's own route is unmounted along with the module itself. const removedRouteResponse = await fetch(`${origin}/api/v1/modules/calendar/events/evt-42`, { headers: { cookie: ownerCookie }, }); expect(removedRouteResponse.status).toBe(404); expect(await removedRouteResponse.json()).toEqual({ code: "NOT_FOUND" }); // The owner's existing session survives the restart (sessions live in the database, not the // process), so the same cookie proves ordinary identity behavior is unaffected here too. const listResponse = await fetch(`${origin}/api/v1/grants?userId=${ownerUserId}`, { headers: { cookie: ownerCookie }, }); const listBody = (await listResponse.json()) as { grants: Array<{ id: string; permission: string }>; }; expect(listResponse.status).toBe(200); expect(listBody.grants.some((grant) => grant.id === createdGrantId)).toBe(true); const response = await fetch(`${origin}/api/v1/grants`, { body: JSON.stringify({ permission: modulePermission, subjectUserId: ownerUserId }), headers: { "content-type": "application/json", cookie: ownerCookie, origin: dashboardOrigin, }, method: "POST", }); expect(response.status).toBe(403); expect(await response.json()).toEqual({ code: "AUTHORIZATION_DENIED" }); const admin = await openAdministrativeClient(database.url); try { const rows = await safeQuery<{ id: string }>( admin, `select id from "ekklesia_core_grant" where id = $1 and revoked_at is null`, [createdGrantId], ); expect(rows).toHaveLength(1); } finally { await safeEnd(admin); } await running.shutdown(); await dropDatabase(database.name); }); }); describe.sequential("live API — public signup is disabled by default", () => { it("rejects sign-up with a stable sanitized 400 when publicSignupEnabled is not set, creating no user row", async () => { const database = await createDisposableDatabase("signup_disabled"); await applyCoreMigrations(database.url); const running = await startConfiguredApi(database.url, { publicSignupEnabled: false }); try { const origin = apiOrigin(running); const response = await fetch(`${origin}/api/auth/sign-up/email`, { body: JSON.stringify({ email: "blocked@example.test", name: "Blocked", password: "correct horse battery staple", }), headers: { "content-type": "application/json", origin: dashboardOrigin }, method: "POST", }); const text = await response.text(); expect(response.status).toBe(400); expect(response.headers.get("cache-control")).toBe("no-store"); expect(text.toLowerCase()).not.toContain("stack"); expect(text).not.toContain(database.url); const admin = await openAdministrativeClient(database.url); try { const rows = await safeQuery<{ count: string }>( admin, 'select count(*)::text as count from "ekklesia_core_user"', ); expect(rows[0]?.count).toBe("0"); } finally { await safeEnd(admin); } } finally { await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); it("gives an identical rejection whether or not the requested email is already registered", async () => { const database = await createDisposableDatabase("signup_disabled_enum"); await applyCoreMigrations(database.url); const existingEmail = "already-registered@example.test"; // Briefly enable signup on this same database to seed one real, pre-existing account, matching // how a production database might have been seeded before signup was later disabled. const seedRunning = await startConfiguredApi(database.url, { publicSignupEnabled: true }); try { const seedSignUp = await signUp( apiOrigin(seedRunning), existingEmail, "correct horse battery", ); expect(seedSignUp.response.status).toBe(200); } finally { await seedRunning.shutdown().catch(() => undefined); } const running = await startConfiguredApi(database.url, { publicSignupEnabled: false }); try { const origin = apiOrigin(running); const signUpAttempt = (email: string) => fetch(`${origin}/api/auth/sign-up/email`, { body: JSON.stringify({ email, name: "X", password: "correct horse battery staple" }), headers: { "content-type": "application/json", origin: dashboardOrigin }, method: "POST", }); const existingResponse = await signUpAttempt(existingEmail); const newResponse = await signUpAttempt("brand-new-address@example.test"); expect(existingResponse.status).toBe(400); expect(newResponse.status).toBe(400); expect(existingResponse.status).toBe(newResponse.status); expect(await existingResponse.text()).toBe(await newResponse.text()); } finally { await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); it("keeps sign-in and existing-session behavior fully functional for a pre-existing account while signup stays disabled", async () => { const database = await createDisposableDatabase("signup_disabled_signin"); await applyCoreMigrations(database.url); const email = "still-can-sign-in@example.test"; const password = "correct horse battery staple"; const seedRunning = await startConfiguredApi(database.url, { publicSignupEnabled: true }); try { const seedSignUp = await signUp(apiOrigin(seedRunning), email, password); expect(seedSignUp.response.status).toBe(200); } finally { await seedRunning.shutdown().catch(() => undefined); } const running = await startConfiguredApi(database.url, { publicSignupEnabled: false }); try { const origin = apiOrigin(running); const signInResult = await signIn(origin, email, password); expect(signInResult.response.status).toBe(200); expect(signInResult.cookiePair).toBeDefined(); const { body } = await fetchPrincipal(origin, { cookie: signInResult.cookiePair as string }); expect(body).toMatchObject({ kind: "authenticated" }); const blockedSignUp = await fetch(`${origin}/api/auth/sign-up/email`, { body: JSON.stringify({ email: "another-new-address@example.test", name: "X", password }), headers: { "content-type": "application/json", origin: dashboardOrigin }, method: "POST", }); expect(blockedSignUp.status).toBe(400); } finally { await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); }); describe.sequential("live API — client-address resolution and auth rate-limit identity", () => { let database: { name: string; url: string }; let directModeApi: RunningApi; let directOrigin: string; it("starts a direct-mode API (the safe default) against a freshly migrated database, with Better Auth's real unmodified rate-limit rule", async () => { database = await createDisposableDatabase("client_address"); await applyCoreMigrations(database.url); directModeApi = await startConfiguredApi(database.url, { rateLimitOverride: undefined }); directOrigin = apiOrigin(directModeApi); expect(directModeApi.server.listening).toBe(true); }); it("reproduces the original vulnerability's exact shape and proves it is closed: rotating spoofed X-Forwarded-For values never bypass the limiter in direct mode", async () => { // Before this pass, Better Auth trusted a caller-controlled X-Forwarded-For directly: each // distinct spoofed value looked like an unrelated client, so a caller could send an unlimited // number of sign-in attempts merely by rotating the header on every request. Direct mode now // resolves identity from the real socket peer only — the header is never consulted. const peer = "127.0.0.21"; const first = await rawSignInAttempt(directOrigin, { forwardedFor: "1.1.1.1", localAddress: peer, }); const second = await rawSignInAttempt(directOrigin, { forwardedFor: "2.2.2.2", localAddress: peer, }); const third = await rawSignInAttempt(directOrigin, { forwardedFor: "3.3.3.3", localAddress: peer, }); expect([first.status, second.status, third.status]).toEqual([401, 401, 401]); // The fabricated value on this fourth request is new *again* — under the pre-fix behavior this // would still look like a brand-new, never-before-seen client and would succeed. It must not. const fourth = await rawSignInAttempt(directOrigin, { forwardedFor: "4.4.4.4", localAddress: peer, }); expect(fourth.status).toBe(429); // The limiter's own existing response semantics are preserved unmodified. expect(fourth.headers["x-retry-after"]).toBeDefined(); expect(JSON.parse(fourth.body)).toMatchObject({ message: expect.any(String) }); // A request from the identical real peer with *no* forwarded header at all is equally blocked // — proving the header was never what determined the bucket, in either direction. const fifth = await rawSignInAttempt(directOrigin, { localAddress: peer }); expect(fifth.status).toBe(429); }); it("gives distinct real peer addresses independent rate-limit buckets", async () => { const peerA = "127.0.0.22"; const peerB = "127.0.0.23"; expect((await rawSignInAttempt(directOrigin, { localAddress: peerA })).status).toBe(401); expect((await rawSignInAttempt(directOrigin, { localAddress: peerA })).status).toBe(401); expect((await rawSignInAttempt(directOrigin, { localAddress: peerA })).status).toBe(401); expect((await rawSignInAttempt(directOrigin, { localAddress: peerA })).status).toBe(429); // An unrelated real peer's budget is completely untouched by peerA's exhaustion. expect((await rawSignInAttempt(directOrigin, { localAddress: peerB })).status).toBe(401); }); it("shuts down the direct-mode API", async () => { await directModeApi.shutdown(); }); let proxyModeApi: RunningApi; let proxyOrigin: string; const trustedProxyPeer = "127.0.0.24"; it("starts a second API in explicit trusted-proxy mode against the same migrated database", async () => { proxyModeApi = await startConfiguredApi(database.url, { clientAddress: { header: "x-forwarded-for", mode: "trusted-proxy", trustedProxies: [trustedProxyPeer], }, rateLimitOverride: undefined, }); proxyOrigin = apiOrigin(proxyModeApi); }); it("honors a forwarded address only when it comes from the explicitly trusted proxy peer", async () => { expect( ( await rawSignInAttempt(proxyOrigin, { forwardedFor: "203.0.113.50", localAddress: trustedProxyPeer, }) ).status, ).toBe(401); expect( ( await rawSignInAttempt(proxyOrigin, { forwardedFor: "203.0.113.50", localAddress: trustedProxyPeer, }) ).status, ).toBe(401); expect( ( await rawSignInAttempt(proxyOrigin, { forwardedFor: "203.0.113.50", localAddress: trustedProxyPeer, }) ).status, ).toBe(401); expect( ( await rawSignInAttempt(proxyOrigin, { forwardedFor: "203.0.113.50", localAddress: trustedProxyPeer, }) ).status, ).toBe(429); // A *different* forwarded client from the same trusted proxy gets its own independent bucket. expect( ( await rawSignInAttempt(proxyOrigin, { forwardedFor: "203.0.113.51", localAddress: trustedProxyPeer, }) ).status, ).toBe(401); }); it("ignores a forwarded address claimed by an untrusted peer, using that peer's own real address instead", async () => { // 203.0.113.50 is already exhausted above when legitimately forwarded by the trusted proxy — // an untrusted peer merely claiming to *be* that address must not inherit its exhausted bucket, // and must not be able to choose an arbitrary bucket of its own by forging the header either. const untrustedPeer = "127.0.0.25"; const response = await rawSignInAttempt(proxyOrigin, { forwardedFor: "203.0.113.50", localAddress: untrustedPeer, }); expect(response.status).toBe(401); }); it("walks a chained forwarded header right-to-left, skipping the trusted-proxy hop to find the genuine client", async () => { const chained = `203.0.113.60, ${trustedProxyPeer}`; expect( ( await rawSignInAttempt(proxyOrigin, { forwardedFor: chained, localAddress: trustedProxyPeer, }) ).status, ).toBe(401); expect( ( await rawSignInAttempt(proxyOrigin, { forwardedFor: chained, localAddress: trustedProxyPeer, }) ).status, ).toBe(401); expect( ( await rawSignInAttempt(proxyOrigin, { forwardedFor: chained, localAddress: trustedProxyPeer, }) ).status, ).toBe(401); expect( ( await rawSignInAttempt(proxyOrigin, { forwardedFor: chained, localAddress: trustedProxyPeer, }) ).status, ).toBe(429); }); it("fails safely with a sanitized 503 on a malformed forwarded chain from a trusted proxy, never falling back to a shared bucket", async () => { const response = await rawSignInAttempt(proxyOrigin, { forwardedFor: "not-an-ip", localAddress: trustedProxyPeer, }); expect(response.status).toBe(503); expect(JSON.parse(response.body)).toEqual({ code: "CLIENT_ADDRESS_UNRESOLVED" }); expect(response.headers["cache-control"]).toBe("no-store"); }); it("shuts down the trusted-proxy-mode API and drops the shared database", async () => { await proxyModeApi.shutdown(); await dropDatabase(database.name); }); }); describe.sequential("live API — Node header/request timeouts and client errors", () => { /** * Drives one raw, deliberately-malformed or deliberately-slow byte sequence through a real * listening API and returns everything observable at the transport boundary, plus everything the * *real* production logging seam (`console.error`, which `apps/api/src/server.ts` writes its * sanitized operational line through) actually emitted while it happened. * * This is the only shape of proof that counts for the timeout-observability claim: no event is * manually emitted, no logger is called directly, and nothing asserts merely that a listener was * registered — a real socket is opened against a real server, real bytes are written, and the * real Node transport is left to do whatever it genuinely does. */ async function driveRawConnection( label: string, write: (socket: net.Socket) => void, timeouts: Partial = { connectionsCheckingIntervalMs: 100, headersTimeoutMs: 200, keepAliveTimeoutMs: 200, requestTimeoutMs: 300, }, ): Promise<{ destroyed: boolean; logs: string[]; received: string }> { const database = await createDisposableDatabase(label); await applyCoreMigrations(database.url); const config = createApiRuntimeConfig( { databaseUrl: database.url, host: "127.0.0.1", identity: { baseUrl: identityBaseUrl, publicSignupEnabled: false, secret: "integration-test-secret-0123456789abcdefgh", trustedOrigins: [identityBaseUrl, dashboardOrigin], }, port: 0, }, // Development configuration, for the same reason `startConfiguredApi` documents: this suite // connects to the disposable plaintext loopback PostgreSQL, which production configuration // never permits. Nothing about the transport-level timeout behavior under test depends on it. { environment: "development" }, ); // Deliberately tiny, test-only timeouts — production composition (server.ts's `main`) never // sets this override — so a real timeout is observed deterministically in well under a second // rather than by waiting out the production values. const running = await startApi(config, { nodeServerTimeoutOverride: timeouts }); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); try { const address = running.server.address(); if (!address || typeof address !== "object") { throw harnessError("The API integration server has no bound address."); } const socket = net.connect({ host: "127.0.0.1", port: address.port }); await new Promise((resolve, reject) => { socket.once("connect", () => resolve()); socket.once("error", reject); }); const receivedChunks: Buffer[] = []; // A `net.Socket` stays paused (and never surfaces the peer's eventual FIN as `close`) until // something actively consumes its readable side — attaching this listener is what makes the // socket start flowing, not merely a way to capture the response for the assertions below. socket.on("data", (chunk) => receivedChunks.push(chunk as Buffer)); socket.on("error", () => undefined); const closed = new Promise((resolve) => socket.once("close", resolve)); write(socket); await closed; const logs = errorSpy.mock.calls.flat().map((value) => String(value)); return { destroyed: socket.destroyed, logs, received: Buffer.concat(receivedChunks).toString("utf8"), }; } finally { errorSpy.mockRestore(); await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } } /** Every status line in a response stream — more than one would mean the platform corrupted the * connection by writing a second response after Node (or a handler) already wrote one. */ function statusLines(received: string): readonly string[] { return received.match(/^HTTP\/1\.1 \d{3}[^\r\n]*/gmu) ?? []; } /** The exact operational line `apps/api/src/server.ts` promises for a transport-level client * failure — no request path, query, body, cookie, authorization value, client-address header, * forwarding chain, raw peer address, stack, or parser payload may appear anywhere in the logs. */ function expectSanitizedTransportLog(logs: readonly string[]): void { expect(logs).toHaveLength(1); const emitted = logs.join("\n"); for (const forbidden of [ "/api/v1/principal", "/api/auth", "127.0.0.1", "Cookie", "cookie", "Authorization", "authorization", "x-forwarded-for", "x-ekklesia-client-address", "partial", "Parse Error", "HPE_", "ERR_HTTP_REQUEST_TIMEOUT", "at ", ]) { expect(emitted).not.toContain(forbidden); } } it("answers a never-completed header sequence with exactly one 408, closes the socket, invokes no handler, and emits exactly one sanitized operational log", async () => { const { destroyed, logs, received } = await driveRawConnection( "node_timeouts", // An incomplete request: a request line and one header, deliberately never terminated by the // blank line that would mark the end of the headers, and no further data ever sent. (socket) => socket.write("GET /api/v1/principal HTTP/1.1\r\nHost: 127.0.0.1\r\n"), ); expect(received).toBe("HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n"); expect(statusLines(received)).toHaveLength(1); // A handler would have produced a JSON principal body; Node answered before routing. expect(received).not.toContain("anonymous"); expect(destroyed).toBe(true); expectSanitizedTransportLog(logs); }); it("answers a never-completed body with exactly one 408, closes the socket, invokes no auth logic, and emits exactly one sanitized operational log", async () => { const { destroyed, logs, received } = await driveRawConnection( "node_timeouts_body", // Complete, well-formed headers declaring a body that never actually arrives. (socket) => socket.write( "POST /api/auth/sign-in/email HTTP/1.1\r\n" + "Host: 127.0.0.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 1000\r\n" + "\r\n" + '{"partial":', ), { connectionsCheckingIntervalMs: 100, headersTimeoutMs: 300, keepAliveTimeoutMs: 200, requestTimeoutMs: 400, }, ); expect(received).toBe("HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n"); expect(statusLines(received)).toHaveLength(1); expect(received).not.toContain("Set-Cookie"); expect(destroyed).toBe(true); expectSanitizedTransportLog(logs); }); it("preserves Node's own 400 for a malformed request line, without logging the malformed bytes", async () => { const { destroyed, logs, received } = await driveRawConnection("node_bad_request", (socket) => socket.write("!!!!NOT-HTTP-AT-ALL!!!! /secret-path\r\n\r\n"), ); expect(received).toBe("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"); expect(statusLines(received)).toHaveLength(1); expect(destroyed).toBe(true); expectSanitizedTransportLog(logs); // The attacker-controlled bytes must never reach the log. expect(logs.join("\n")).not.toContain("NOT-HTTP-AT-ALL"); expect(logs.join("\n")).not.toContain("secret-path"); }); it("preserves Node's own 431 for an oversized header block, without logging the header contents", async () => { const { destroyed, logs, received } = await driveRawConnection( "node_header_overflow", (socket) => socket.write( `GET /api/v1/principal HTTP/1.1\r\nHost: 127.0.0.1\r\nX-Overflow: ${"z".repeat(20_000)}\r\n\r\n`, ), ); expect(received).toBe( "HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n", ); expect(statusLines(received)).toHaveLength(1); expect(destroyed).toBe(true); expectSanitizedTransportLog(logs); expect(logs.join("\n")).not.toContain("zzz"); }); });