import { randomBytes } from "node:crypto"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { request as httpRequest } from "node:http"; import net from "node:net"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; 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 { bootstrapHost, type EkklesiaHostConfig, signInWithEmail, signOutHost, } from "@ekklesia/shell"; 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 { createApiDatabase } from "../src/database.js"; import { HEALTH_LIVE_PATH, HEALTH_READY_PATH, READINESS_DATABASE_TIMEOUT_MS, } from "../src/health-routes.js"; import { MODULE_UI_CACHE_CONTROL, MODULE_UI_CONTENT_TYPE, moduleUiArtifactUrl, } from "../src/module-ui-routes.js"; import { type NodeServerTimeouts, type RunningApi, startApi } 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); } async function withTransportDeadline( operation: Promise, onTimeout: () => void, ): Promise { let timer: ReturnType | undefined; try { return await Promise.race([ operation, new Promise((_, reject) => { timer = setTimeout(() => { onTimeout(); reject(harnessError("An integration transport operation timed out.")); }, 5_000); }), ]); } finally { clearTimeout(timer); } } 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", "core:0002_authorization_integrity", "core:0003_dashboard_navigation_tables", "core:0004_module_lifecycle_state", "core:0005_module_configuration", "core:0006_device_identity", "core:0007_verified_bootstrap_owners", ]); expect(aggregate.migrations[0]?.checksum).toBe( "f8f2086ef06f7a6943e8ffc4350bb010b37aed04c0a72030d2b153c986b1325f", ); expect(aggregate.migrations[1]?.checksum).toBe( "830e1fbc26d05af08df8d8710c3d10e1f3016d32c41b8363cb661c49afcbe81e", ); // Pass 3's new Core migration, pinned the same way: 0000/0001 above must stay byte-identical. expect(aggregate.migrations[2]?.checksum).toBe( "63429473118065dfe3877a1baec61b8f61663b88127d9cb17707c859f27f6036", ); 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. */ /** A handler gate shared with a dynamically imported fixture module through `globalThis`, the same * technique `createEventRecorder` uses. The handler resolves `entered` when it is genuinely inside * user code and then awaits `released`, so a test can position shutdown precisely instead of * inferring handler entry from a connection count. `release` is idempotent and safe in `finally`. */ function createHandlerGate(): { readonly entered: Promise; readonly expression: string; release: () => void; } { const key = `ekklesia.api.integration.gate.${eventSymbols.length}.${Date.now()}.${Math.random()}`; const symbol = Symbol.for(key); let signalEntered: () => void = () => undefined; let signalReleased: () => void = () => undefined; const entered = new Promise((resolve) => { signalEntered = resolve; }); const released = new Promise((resolve) => { signalReleased = resolve; }); (globalThis as unknown as Record)[symbol] = { enter: () => signalEntered(), released, }; eventSymbols.push(symbol); return { entered, expression: `globalThis[Symbol.for(${JSON.stringify(key)})]`, release: () => signalReleased(), }; } /** A fixture module with one public route whose handler blocks on the supplied gate, plus a stop * hook that records when it ran — everything the graceful-drain ordering assertion needs. */ async function createGatedRouteModule( modulesDirectory: string, id: string, recorder: { expression: string }, gate: { expression: string }, ): Promise { const permissionId = `${id}.gated.read`; const moduleDirectory = path.join(modulesDirectory, id); 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({ compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" }, entrypoints: { server: "./dist/server/index.js" }, id, manifestVersion: 7, name: `${id} module`, permissions: [{ baseline: "public", description: "Gated drain probe.", id: permissionId }], version: "0.1.0", }), ), writeFile( path.join(moduleDirectory, "dist/server/index.js"), `export default { id: ${JSON.stringify(id)}, version: "0.1.0", register({ http, realtime }) { realtime.channel({ channel: "state", permission: ${JSON.stringify(permissionId)} }); http.route({ method: "GET", path: "/gated", permission: ${JSON.stringify(permissionId)}, maxBodyBytes: 0, async handler(_request, context) { ${recorder.expression}.push(${JSON.stringify(`${id}:handler-entered`)}); ${gate.expression}.enter(); await ${gate.expression}.released; context.realtime.publish("state", { status: "finished" }); ${recorder.expression}.push(${JSON.stringify(`${id}:handler-published`)}); ${recorder.expression}.push(${JSON.stringify(`${id}:handler-finished`)}); return Response.json({ gated: true }); }, }); }, start({ realtime }) { realtime.publish("state", { status: "waiting" }); }, stop() { ${recorder.expression}.push(${JSON.stringify(`${id}:stop`)}); }, };`, ), ]); } async function createFixtureModule( modulesDirectory: string, id: string, recorder: { expression: string }, manifestOverrides: Record = {}, ): Promise { const moduleDirectory = path.join(modulesDirectory, id); const manifest = { manifestVersion: 7, 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", ); const realtimePermission = (manifest.permissions as ReadonlyArray<{ id: string }>).find( (declaration) => declaration.id === `${id}.state.view`, )?.id; 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, realtime }) { ${recorder.expression}.push(${JSON.stringify(`${id}:register`)}); ${ realtimePermission ? `realtime.channel({ channel: "state", permission: ${JSON.stringify(realtimePermission)} });` : "" } ${ 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 }); }, }); // An image-sized streamed upload (ADR 0042). The handler counts bytes as they // arrive and never materializes the body, which is exactly what a real module // storing an upload would do. http.route({ method: "POST", path: "/uploads", permission: "calendar.events.read", maxBodyBytes: 33554432, bodyMode: "streamed", async handler(request) { let bytes = 0; if (request.body) { const reader = request.body.getReader(); for (;;) { const chunk = await reader.read(); if (chunk.done) { break; } bytes += chunk.value.byteLength; } } return Response.json({ bytes }, { status: 201 }); }, }); http.route({ method: "POST", path: "/small-uploads", permission: "calendar.events.read", maxBodyBytes: 64, bodyMode: "streamed", async handler(request) { await request.arrayBuffer().catch(() => undefined); return Response.json({ swallowed: true }, { status: 201 }); }, });` : "" } }, start({ realtime }) { ${ realtimePermission ? `realtime.publish("state", { module: ${JSON.stringify(id)}, status: "running" });` : "" } ${recorder.expression}.push(${JSON.stringify(`${id}:start`)}); }, stop() { ${recorder.expression}.push(${JSON.stringify(`${id}:stop`)}); }, };`, ), ]); } /** A module that ships a real, loadable UI artifact plus the neighbouring files a naive static * mount would have exposed: a source map, a sibling bundle chunk, and a package-root secret. */ async function createUiModule( modulesDirectory: string, id: string, version: string, artifact: string, ): Promise { const moduleDirectory = path.join(modulesDirectory, id); await mkdir(path.join(moduleDirectory, "dist/ui"), { recursive: true }); 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, }), ), writeFile( path.join(moduleDirectory, "ekklesia.module.json"), JSON.stringify({ compatibility: { moduleSdk: "^0.1.0", platform: "^0.1.0" }, entrypoints: { server: "./dist/server/index.js", ui: "./dist/ui/index.js" }, id, manifestVersion: 7, name: `${id} module`, version, }), ), writeFile( path.join(moduleDirectory, "dist/server/index.js"), `export default { id: ${JSON.stringify(id)}, version: ${JSON.stringify(version)} };`, ), writeFile(path.join(moduleDirectory, "dist/ui/index.js"), artifact), writeFile( path.join(moduleDirectory, "dist/ui/index.js.map"), '{"version":3,"sourcesContent":["map-secret"]}', ), writeFile(path.join(moduleDirectory, "dist/ui/chunk.js"), "export const chunkSecret = 1;\n"), writeFile(path.join(moduleDirectory, ".env"), "EKKLESIA_IDENTITY_SECRET=env-secret\n"), ]); } 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}`; } /** * Marks an address verified out of band, the same trusted step the initial-owner bootstrap performs * (ADR 0038). * * Deliberately not a weakening of the policy: it is the test-harness equivalent of an operator with * database credentials, used only so scenarios that are about something else are not obliged to run * an SMTP server. That the policy itself is enforced — that an unverified account genuinely cannot * sign in — is asserted separately, both in this file and in `email-verification.integration.ts`. */ async function markEmailVerified(origin: string, email: string): Promise { const url = databaseUrlByOrigin.get(origin); if (!url) { throw harnessError("No database is registered for that API origin."); } const client = new Client({ connectionString: url }); await client.connect(); try { await client.query( 'update "ekklesia_core_user" set "email_verified" = true where lower("email") = $1', [email.trim().toLowerCase()], ); } finally { await client.end().catch(() => undefined); } } interface StartConfiguredApiOptions { readonly clientAddress?: ClientAddressConfigInput; /** Test-only narrowing of the platform's 30-second graceful HTTP drain deadline. */ readonly drainTimeoutMsOverride?: number; 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; } /** * The database each running API in this suite is attached to, keyed by its bound origin. * * Needed because Phase 7C Pass 2 made a verified address a precondition for holding a session * (ADR 0038), so `signUp` below has to mark the account verified before these scenarios — which are * about cookies, CSRF, principals, sessions, and authorization, not about verification — can sign * in. Verification itself has its own dedicated end-to-end suite. */ const databaseUrlByOrigin = new Map(); 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 }, ); const running = await 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 }), ...(options.drainTimeoutMsOverride === undefined ? {} : { drainTimeoutMsOverride: options.drainTimeoutMsOverride }), }); databaseUrlByOrigin.set(apiOrigin(running), url); return running; } 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 SafeCookieObservation { readonly domain: string | undefined; readonly httpOnly: boolean; readonly path: string | undefined; readonly sameSite: string | undefined; readonly secure: boolean; } interface BrowserAuthResponseObservation { readonly accessControlAllowCredentials: string | null; readonly accessControlAllowOrigin: string | null; readonly body: string; readonly contentLength: string | null; readonly cookie: SafeCookieObservation; readonly issuedSetCookie: boolean; readonly status: number; } interface OriginLockedBrowserCookieJar { readonly fetch: typeof fetch; hasSessionCookie(): boolean; signInResponse(): BrowserAuthResponseObservation; signOutResponse(): BrowserAuthResponseObservation; } function observeCookie(setCookie: string | undefined): SafeCookieObservation { const attributes = (setCookie ?? "") .split(";") .slice(1) .map((part) => part.trim()) .filter(Boolean); const attributeValue = (name: string): string | undefined => { const prefix = `${name.toLowerCase()}=`; const attribute = attributes.find((part) => part.toLowerCase().startsWith(prefix)); return attribute?.slice(prefix.length); }; const hasAttribute = (name: string): boolean => attributes.some((part) => part.toLowerCase() === name.toLowerCase()); return Object.freeze({ domain: attributeValue("domain"), httpOnly: hasAttribute("httponly"), path: attributeValue("path"), sameSite: attributeValue("samesite"), secure: hasAttribute("secure"), }); } /** * Node deliberately has no ambient browser cookie store. This origin-locked test transport models * the one host-only session cookie explicitly: it accepts Set-Cookie only from the configured API, * sends only the cookie pair back to that same origin, clears it on an expiry response, and never * exposes the stored value to assertions or error output. The observable cookie data contains only * flags needed to prove the browser contract. */ function createOriginLockedBrowserCookieJar(apiBaseUrl: string): OriginLockedBrowserCookieJar { const apiOrigin = new URL(apiBaseUrl).origin; let cookiePair: string | undefined; let signInObservation: BrowserAuthResponseObservation | undefined; let signOutObservation: BrowserAuthResponseObservation | undefined; const browserFetch: typeof fetch = async (input, init) => { const request = new Request(input, init); const requestUrl = new URL(request.url); if (requestUrl.origin !== apiOrigin) { throw harnessError("The browser integration transport refused a cross-origin request."); } // A real cross-origin browser supplies Origin itself and its cookie jar only when // `credentials: include` is selected. Node's fetch has neither ambient behavior, so this // tightly scoped harness supplies exactly those two transport facts. request.headers.set("origin", dashboardOrigin); request.headers.delete("cookie"); if (cookiePair !== undefined && request.credentials === "include") { request.headers.set("cookie", cookiePair); } const response = await fetch(request); const setCookie = setCookieValue(response); if (setCookie !== undefined && request.credentials === "include") { const nextCookiePair = firstCookiePair(setCookie); const separator = nextCookiePair.indexOf("="); const nextName = separator < 0 ? "" : nextCookiePair.slice(0, separator); const nextValue = separator < 0 ? "" : nextCookiePair.slice(separator + 1); const expiresCookie = /(?:^|;)\s*max-age=0(?:;|$)/iu.test(setCookie); if (nextName !== "" && nextValue !== "" && !expiresCookie) { cookiePair = nextCookiePair; } else if (cookiePair?.startsWith(`${nextName}=`)) { cookiePair = undefined; } } const pathname = requestUrl.pathname; if ( request.method === "POST" && (pathname === "/api/auth/sign-in/email" || pathname === "/api/auth/sign-out") ) { const observation = Object.freeze({ accessControlAllowCredentials: response.headers.get("access-control-allow-credentials"), accessControlAllowOrigin: response.headers.get("access-control-allow-origin"), body: await response.clone().text(), contentLength: response.headers.get("content-length"), cookie: observeCookie(setCookie), issuedSetCookie: setCookie !== undefined, status: response.status, }); if (pathname === "/api/auth/sign-in/email") { signInObservation = observation; } else { signOutObservation = observation; } } return response; }; return Object.freeze({ fetch: browserFetch, hasSessionCookie: () => cookiePair !== undefined, signInResponse: () => { if (signInObservation === undefined) { throw harnessError("The browser integration did not observe a sign-in response."); } return signInObservation; }, signOutResponse: () => { if (signOutObservation === undefined) { throw harnessError("The browser integration did not observe a sign-out response."); } return signOutObservation; }, }); } 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", }); if (response.ok) { // A verified address is a precondition for holding a session (ADR 0038). Sign-up itself always // writes `emailVerified: false`, so these scenarios — none of which are about verification — // complete that step out of band rather than standing up an SMTP server apiece. await markEmailVerified(origin, email); } 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 }; } /** * Signs up an account over real HTTP, grants it platform-owner authority through Core's own real * bootstrap operation (exactly what `pnpm authorization:bootstrap-owner` performs), and signs it * in — so a scenario needing an administrator exercises the genuine evaluator path rather than a * stubbed decision. */ async function signUpPlatformOwner( origin: string, url: string, ): Promise<{ readonly cookie: string; readonly userId: string }> { const email = `owner-${randomBytes(6).toString("hex")}@example.test`; const password = "correct horse battery staple"; const created = await signUp(origin, email, password); const { close, db } = createAuthorizationDrizzleDatabase(url); try { await bootstrapPlatformOwner(db, { userId: parseEkklesiaUserId(created.body.user.id) }); } finally { await close(); } const session = await signIn(origin, email, password); if (!session.cookiePair) { throw harnessError("The platform-owner session cookie was not issued."); } return { cookie: session.cookiePair, userId: created.body.user.id }; } 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); const browserVisibleBody = await response.clone().text(); expect(response.status).toBe(200); expect(JSON.parse(browserVisibleBody)).toEqual({ ok: true }); expect(browserVisibleBody).not.toContain("token"); expect(browserVisibleBody).not.toContain("user"); expect(browserVisibleBody).not.toContain("session"); expect(browserVisibleBody).not.toContain(emailOne); expect(setCookie).toBeDefined(); expect(cookiePair).toBeDefined(); sessionCookie = cookiePair as string; const cookieText = setCookie as string; expect(cookieText).toContain("Path=/"); 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 — exported browser auth shell against PostgreSQL", () => { it("signs a real platform owner in, bootstraps protected Dashboard navigation, and signs out", async () => { const modulesDirectory = await createFixtureModulesDirectory(); await createUiModule( modulesDirectory, "browser-shell", "1.0.0", 'export default { id: "browser-shell", version: "1.0.0", mount() {} };\n', ); const database = await createDisposableDatabase("browser_auth"); await applyCoreMigrations(database.url); const running = await startMigratedApi(database.url, modulesDirectory); try { const origin = apiOrigin(running); const email = `browser-owner-${randomBytes(6).toString("hex")}@example.test`; const password = "correct horse battery staple"; const created = await signUp(origin, email, password); expect(created.response.status).toBe(200); const authorizationDatabase = createAuthorizationDrizzleDatabase(database.url); try { await bootstrapPlatformOwner(authorizationDatabase.db, { userId: parseEkklesiaUserId(created.body.user.id), }); } finally { await authorizationDatabase.close(); } const cookieJar = createOriginLockedBrowserCookieJar(origin); const hostConfig: EkklesiaHostConfig = Object.freeze({ apiBaseUrl: origin, fetch: cookieJar.fetch, }); // This is the public framework-free helper the browser shell itself calls — no test-side // reimplementation of its URL, payload, credentials mode, or response handling. await signInWithEmail(hostConfig, { email, password }); const signIn = cookieJar.signInResponse(); expect(signIn.status).toBe(200); expect(JSON.parse(signIn.body)).toEqual({ ok: true }); expect( signIn.contentLength === null || signIn.contentLength === String(Buffer.byteLength(signIn.body)), ).toBe(true); for (const forbidden of ["token", "user", "session", email]) { expect(signIn.body).not.toContain(forbidden); } expect(signIn.issuedSetCookie).toBe(true); expect(cookieJar.hasSessionCookie()).toBe(true); expect(signIn.cookie).toEqual({ domain: undefined, httpOnly: true, path: "/", sameSite: "Lax", secure: true, }); expect(signIn.accessControlAllowOrigin).toBe(dashboardOrigin); expect(signIn.accessControlAllowCredentials).toBe("true"); // The pinned Better Auth build would return the raw session token from the first three // resources, and from change-password when `revokeOtherSessions` is true. Ekklesia has no // native session/password-management UI, so the outer API boundary must answer before Better // Auth can read these authenticated requests. The valid change-password payload makes this a // meaningful leak regression: if the denylist disappeared it could mint and expose a new // token, not merely fail validation before reaching the vulnerable response. const deniedNativeEndpoints = [ { method: "GET", path: "/api/auth/get-session?source=browser-shell" }, { method: "GET", path: "/api/auth/%67et-session" }, { method: "GET", path: "/api/auth/get-session%2F" }, { body: {}, method: "POST", path: "/api/auth/get-session" }, { method: "GET", path: "/api/auth/list-sessions" }, { body: { arbitrary: "value" }, method: "POST", path: "/api/auth/update-session" }, { body: { currentPassword: password, newPassword: "replacement horse battery staple", revokeOtherSessions: true, }, method: "POST", path: "/api/auth/change-password", }, ] as const; for (const endpoint of deniedNativeEndpoints) { const response = await cookieJar.fetch(`${origin}${endpoint.path}`, { credentials: "include", method: endpoint.method, ...("body" in endpoint ? { body: JSON.stringify(endpoint.body), headers: { "content-type": "application/json" }, } : {}), }); const visibleBody = await response.text(); expect(response.status).toBe(404); expect(JSON.parse(visibleBody)).toEqual({ code: "NOT_FOUND" }); for (const forbidden of ["token", "session", email, password]) { expect(visibleBody).not.toContain(forbidden); } expect(response.headers.get("cache-control")).toBe("no-store"); expect(response.headers.get("set-cookie")).toBeNull(); expect(response.headers.get("access-control-allow-origin")).toBe(dashboardOrigin); expect(response.headers.get("access-control-allow-credentials")).toBe("true"); } // `bootstrapHost` resolves the principal first, then calls the protected Dashboard endpoint. // A real module in the result proves the latter request was authenticated and authorized. const authenticated = await bootstrapHost(hostConfig); expect(authenticated.principal).toEqual({ kind: "authenticated", userId: created.body.user.id, }); expect(authenticated.navigation.groups).toEqual([]); expect(authenticated.navigation.ungrouped.map((entry) => entry.descriptor.id)).toEqual([ "browser-shell", ]); await signOutHost(hostConfig); const signOut = cookieJar.signOutResponse(); expect(signOut.status).toBe(200); expect(JSON.parse(signOut.body)).toEqual({ success: true }); expect(signOut.issuedSetCookie).toBe(true); expect(signOut.accessControlAllowOrigin).toBe(dashboardOrigin); expect(signOut.accessControlAllowCredentials).toBe("true"); expect(cookieJar.hasSessionCookie()).toBe(false); // Reload through the same exported principal-first shell bootstrap. The expired cookie is no // longer sent, protected navigation is not probed, and the host returns its frozen empty view. const reloaded = await bootstrapHost(hostConfig); expect(reloaded.principal).toEqual({ kind: "anonymous" }); expect(reloaded.navigation).toEqual({ groups: [], ungrouped: [] }); } finally { await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); }); describe.sequential("live API — startup and migration guarantees", () => { it("refuses to start against an unmigrated database, and creates no tables or migration history", 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 // uses the disposable plaintext loopback database, which production configuration refuses. { environment: "development" }, ); // Startup reads the stored module enablement configuration and then the stored module // configuration, both before importing any module entrypoint (ADR 0028, ADR 0029). Against an // unmigrated database neither read can succeed, and the platform fails closed rather than // starting every discovered module: it cannot know what an administrator decided, and starting // a module they disabled — or one whose required configuration is absent — is worse than not // starting. The two reads are sequential rather than raced precisely so this error code is // deterministic; the failure is Core's own sanitized store error, with no connection string, // host, SQL, driver message, or stack. let captured: unknown; try { await startApi(config, { rateLimitOverride: { max: 1000, window: 1 } }); } catch (error) { captured = error; } expect(captured).toMatchObject({ code: "MODULE_LIFECYCLE_READ_FAILED", name: "ModuleLifecycleStoreError", }); const rendered = `${String(captured)} ${JSON.stringify(captured)}`; expect(rendered).not.toContain(database.name); expect(rendered).not.toContain("127.0.0.1"); expect(rendered).not.toContain("select"); const admin = await openAdministrativeClient(database.url); try { // The invariant this scenario has always existed to protect, unchanged and now proven on a // path that never even reached Better Auth: a failed start repairs nothing, creates // nothing, and applies no migration. 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: [], nextCursor: null }); }); 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`, { // A subject with no equivalent grant yet — the manager already holds an equivalent // `grants.manage` grant from the delegation scenario above, which the new duplicate // invariant would (correctly) refuse. Unscoped: the generic HTTP surface creates only // unscoped explicit grants, since a scope it accepted could never authorize anything. body: JSON.stringify({ permission: testPermission, subjectUserId: ownerUserId, }), 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(ownerUserId); 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, bounded and with an explicit next-page cursor field", async () => { const response = await fetch(`${origin}/api/v1/grants?userId=${ownerUserId}`, { headers: { cookie: ownerCookie }, }); const body = (await response.json()) as { grants: Array<{ id: string; scope: string | null }>; nextCursor: string | null; }; expect(response.status).toBe(200); expect(response.headers.get("cache-control")).toBe("no-store"); expect(body.grants.some((grant) => grant.id === createdGrantId)).toBe(true); // Created through the generic HTTP surface, so it is unscoped. expect(body.grants.find((grant) => grant.id === createdGrantId)?.scope).toBeNull(); expect(body.nextCursor).toBeNull(); }); 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); }); }); /** * Pass 3's explicit-grant invariants, exercised end-to-end through a real authorized * grants-management session against real PostgreSQL — the layer where a validation decision that * "should" happen before the database either does or does not. */ describe.sequential("live API — explicit-grant invariants over real HTTP", () => { let database: { name: string; url: string }; let running: RunningApi; let origin: string; let ownerUserId: string; let subjectUserId: string; let ownerCookie: string; const password = "correct horse battery staple"; const managePermission = "core.authorization.grants.manage"; async function post(path: string, body?: unknown): Promise { return await fetch(`${origin}${path}`, { ...(body === undefined ? {} : { body: JSON.stringify(body) }), headers: { ...(body === undefined ? {} : { "content-type": "application/json" }), cookie: ownerCookie, origin: dashboardOrigin, }, method: "POST", }); } async function list(query: string): Promise<{ body: unknown; response: Response }> { const response = await fetch(`${origin}/api/v1/grants?${query}`, { headers: { cookie: ownerCookie }, }); return { body: await response.json(), response }; } it("starts a freshly migrated API with a bootstrapped owner and a separate grant subject", async () => { database = await createDisposableDatabase("grant_invariants"); await applyCoreMigrations(database.url); running = await startMigratedApi(database.url); origin = apiOrigin(running); ownerUserId = (await signUp(origin, "invariants-owner@example.test", password)).body.user.id; subjectUserId = (await signUp(origin, "invariants-subject@example.test", password)).body.user .id; const { close, db } = createAuthorizationDrizzleDatabase(database.url); try { await bootstrapPlatformOwner(db, { userId: parseEkklesiaUserId(ownerUserId) }); } finally { await close(); } const ownerSignIn = await signIn(origin, "invariants-owner@example.test", password); expect(ownerSignIn.cookiePair).toBeDefined(); ownerCookie = ownerSignIn.cookiePair as string; }); it("rejects a malformed non-UUID grant id with a sanitized 400, never a database failure", async () => { for (const malformed of [ "not-a-uuid", "0B36B1B6-9A29-4C4E-9E2D-4C0E4C5E6F70", "0b36b1b6-9a29-4c4e-9e2d-4c0e4c5e6f7", ]) { const response = await post(`/api/v1/grants/${malformed}/revoke`); const raw = await response.text(); // Before Pass 3 this reached PostgreSQL's own UUID comparison and returned 503. expect(response.status, malformed).toBe(400); expect(JSON.parse(raw)).toEqual({ code: "INVALID_REQUEST" }); expect(response.headers.get("cache-control")).toBe("no-store"); expect(raw).not.toContain(malformed); } }); it("returns the sanitized 404 for a canonical but unknown grant id", async () => { const response = await post("/api/v1/grants/00000000-0000-4000-8000-000000000000/revoke"); expect(response.status).toBe(404); expect(await response.json()).toEqual({ code: "AUTHORIZATION_GRANT_NOT_FOUND" }); }); it("rejects a scoped create body with a sanitized 400 and writes nothing", async () => { const response = await post("/api/v1/grants", { permission: managePermission, scope: "calendar-event:1", subjectUserId, }); expect(response.status).toBe(400); expect(await response.json()).toEqual({ code: "INVALID_REQUEST" }); const admin = await openAdministrativeClient(database.url); try { expect( await safeQuery(admin, `select id from "ekklesia_core_grant" where user_id = $1`, [ subjectUserId, ]), ).toHaveLength(0); } finally { await safeEnd(admin); } }); it("rejects normalized-impossible, non-canonical, and past expirations with a sanitized 400", async () => { for (const expiresAt of [ "2026-02-31T00:00:00.000Z", "March 5, 2027", "2027-01-01", "2027-01-01T00:00:00Z", "2027-01-01T00:00:00.000+02:00", "2020-01-01T00:00:00.000Z", ]) { const response = await post("/api/v1/grants", { expiresAt, permission: managePermission, subjectUserId, }); expect(response.status, expiresAt).toBe(400); expect(await response.json()).toEqual({ code: "INVALID_REQUEST" }); } }); it("creates one grant, then answers an equivalent creation with a sanitized 409 and no second row", async () => { const created = await post("/api/v1/grants", { permission: managePermission, subjectUserId, }); expect(created.status).toBe(201); const duplicate = await post("/api/v1/grants", { permission: managePermission, subjectUserId, }); const raw = await duplicate.text(); expect(duplicate.status).toBe(409); expect(JSON.parse(raw)).toEqual({ code: "AUTHORIZATION_GRANT_ALREADY_EXISTS" }); expect(duplicate.headers.get("cache-control")).toBe("no-store"); // No existing grant id, actor, scope, source reference, SQL, or driver text leaks. expect(raw).not.toContain(ownerUserId); expect(raw).not.toContain(subjectUserId); expect(raw).not.toContain("ekklesia_core_grant"); // A differing expiration is not a different grant either. const withExpiry = await post("/api/v1/grants", { expiresAt: "2099-01-01T00:00:00.000Z", permission: managePermission, subjectUserId, }); expect(withExpiry.status).toBe(409); const admin = await openAdministrativeClient(database.url); try { expect( await safeQuery( admin, `select id from "ekklesia_core_grant" where user_id = $1 and revoked_at is null`, [subjectUserId], ), ).toHaveLength(1); } finally { await safeEnd(admin); } }); it("paginates deterministically across at least three pages and ends with a null cursor", async () => { // Seeded through the test-only administrative client so the page count is deterministic and // independent of how many grants the scenarios above happened to create. const admin = await openAdministrativeClient(database.url); try { await safeQuery( admin, `insert into "ekklesia_core_grant" (user_id, permission_id, scope, source_kind, source_ref, created_by_actor_type, created_by_actor_id, created_at) select $1, 'calendar.events.page' || generation, null, 'explicit', null, 'user', $2, timestamptz '2026-01-01 00:00:00Z' from generate_series(1, 6) as generation`, [subjectUserId, ownerUserId], ); await safeQuery( admin, `insert into "ekklesia_core_grant" (user_id, permission_id, scope, source_kind, source_ref, created_by_actor_type, created_by_actor_id) values ($1, 'roster.volunteer.read', null, 'relationship', 'roster:1', 'user', $2)`, [subjectUserId, ownerUserId], ); } finally { await safeEnd(admin); } const seen: string[] = []; let cursor: string | null = null; let pages = 0; for (;;) { const query = `userId=${subjectUserId}&limit=3${ cursor === null ? "" : `&cursor=${encodeURIComponent(cursor)}` }`; const { body, response } = await list(query); const page = body as { grants: Array<{ id: string; sourceKind: string }>; nextCursor: string | null; }; expect(response.status).toBe(200); expect(response.headers.get("cache-control")).toBe("no-store"); expect(page.grants.length).toBeLessThanOrEqual(3); // Relationship grants never appear on any page. expect(page.grants.every((grant) => grant.sourceKind === "explicit")).toBe(true); seen.push(...page.grants.map((grant) => grant.id)); pages += 1; if (page.nextCursor === null) { break; } cursor = page.nextCursor; expect(pages).toBeLessThan(10); } expect(pages).toBeGreaterThanOrEqual(3); expect(seen).toHaveLength(7); expect(new Set(seen).size).toBe(seen.length); }); it("rejects an out-of-range limit and a foreign or malformed cursor with a sanitized 400", async () => { for (const query of [ `userId=${subjectUserId}&limit=0`, `userId=${subjectUserId}&limit=101`, `userId=${subjectUserId}&limit=01`, `userId=${subjectUserId}&limit=1&limit=2`, `userId=${subjectUserId}&unknown=1`, `userId=${subjectUserId}&cursor=!!!`, // A cursor minted for a different subject must not be replayable against this one. `userId=${subjectUserId}&cursor=${encodeURIComponent( Buffer.from( JSON.stringify({ g: "00000000-0000-4000-8000-000000000000", t: "2026-01-01T00:00:00.000Z", u: ownerUserId, v: 1, }), "utf8", ).toString("base64url"), )}`, ]) { const { body, response } = await list(query); expect(response.status, query).toBe(400); expect(body).toEqual({ code: "INVALID_REQUEST" }); } }); it("revokes every unrevoked equivalent through one HTTP revocation, leaving nothing authorizing", async () => { const permission = "calendar.events.legacydupe"; const admin = await openAdministrativeClient(database.url); let ids: string[] = []; try { // Legacy duplicates the pre-Pass-3 store could produce, planted directly because the // production store now refuses to create them. const rows = await safeQuery<{ id: string }>( admin, `insert into "ekklesia_core_grant" (user_id, permission_id, scope, source_kind, source_ref, created_by_actor_type, created_by_actor_id) values ($1, $2, null, 'explicit', null, 'user', $3), ($1, $2, null, 'explicit', null, 'user', $3), ($1, $2, null, 'explicit', null, 'user', $3) returning id`, [subjectUserId, permission, ownerUserId], ); ids = rows.map((row) => row.id); } finally { await safeEnd(admin); } const response = await post(`/api/v1/grants/${ids[0]}/revoke`); expect(response.status).toBe(200); expect(await response.json()).toEqual({ success: true }); const inspector = await openAdministrativeClient(database.url); try { const remaining = await safeQuery( inspector, `select id from "ekklesia_core_grant" where user_id = $1 and permission_id = $2 and revoked_at is null`, [subjectUserId, permission], ); expect(remaining).toHaveLength(0); const audit = await safeQuery<{ grant_id: string }>( inspector, `select grant_id from "ekklesia_core_authorization_audit" where action = 'grant.revoke' and grant_id = any($1::uuid[])`, [ids], ); expect([...new Set(audit.map((row) => row.grant_id))].sort()).toEqual([...ids].sort()); } finally { await safeEnd(inspector); } }); it("shuts down cleanly", async () => { await running.shutdown(); await dropDatabase(database.name); }); }); 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({ // Core's own six (`core.authorization.grants.manage`, `core.modules.ui.read`, // `core.dashboard.navigation.manage`, `core.modules.lifecycle.manage`, // `core.modules.configuration.manage`, `core.devices.manage`) plus the fixture module's one. details: ["7 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("streams one image-sized upload end to end over real HTTP", async () => { // 25 MiB — an ordinary photograph, and the size the streamed ceiling exists to accommodate. It // travels through a real socket, a real Node server, and the platform's byte counter, and the // platform holds none of it (ADR 0042). const image = new Uint8Array(25 * 1024 * 1024).fill(0x41); const response = await fetch(`${origin}/api/v1/modules/calendar/uploads`, { body: image, headers: { "content-type": "application/octet-stream", cookie: ownerCookie, origin: dashboardOrigin, }, method: "POST", }); expect(response.status).toBe(201); expect(await response.json()).toEqual({ bytes: image.byteLength }); }, 60_000); it("refuses a declared oversize on a streamed route with a sanitized 413", async () => { const response = await fetch(`${origin}/api/v1/modules/calendar/small-uploads`, { body: "a".repeat(65), headers: { "content-type": "text/plain", cookie: ownerCookie, origin: dashboardOrigin, }, method: "POST", }); expect(response.status).toBe(413); expect(await response.json()).toEqual({ code: "REQUEST_BODY_TOO_LARGE" }); }); it("terminates a chunked streamed body that exceeds the limit, even when the handler swallowed it", async () => { // No declared `Content-Length`, so only the running byte count can bound this — and the // handler deliberately ignores the stream error and answers `201`. The platform's `413` still // wins: a limit a handler can swallow is not a limit. const oversize = new ReadableStream({ pull(controller) { controller.enqueue(new Uint8Array(512).fill(0x41)); }, }); const response = await fetch(`${origin}/api/v1/modules/calendar/small-uploads`, { body: oversize, duplex: "half", headers: { cookie: ownerCookie, origin: dashboardOrigin }, method: "POST", } as RequestInit); expect(response.status).toBe(413); expect(await response.json()).toEqual({ code: "REQUEST_BODY_TOO_LARGE" }); }, 30_000); it("keeps the server healthy after an oversized upload", async () => { // The socket was released rather than left draining, so the very next request is served // normally. const response = await fetch(`${origin}/api/v1/modules/calendar/events/evt-43`, { headers: { cookie: ownerCookie }, }); expect(response.status).toBe(200); expect(await response.json()).toMatchObject({ eventId: "evt-43" }); }); 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); }); }); /** * The publication route over a real socket, against a real started API: real Node HTTP, real * headers, real bytes. The unit suite (`module-ui-routes.test.ts`) covers the security matrix in * depth; this proves the same route behaves identically once it is behind the actual server rather * than `app.request`. */ describe.sequential("live API — published module UI artifact", () => { const uiArtifact = `export default { id: "signage", version: "2.0.1", mount(container, context) { container.textContent = context.moduleId; return () => { container.textContent = ""; }; }, }; `; it("serves exactly the declared artifact, and nothing else in the module package", async () => { const modulesDirectory = await createFixtureModulesDirectory(); await createUiModule(modulesDirectory, "signage", "2.0.1", uiArtifact); const database = await createDisposableDatabase("module_ui"); await applyCoreMigrations(database.url); const running = await startMigratedApi(database.url, modulesDirectory); try { const origin = apiOrigin(running); const url = `${origin}${moduleUiArtifactUrl({ moduleId: "signage", version: "2.0.1" })}`; const response = await fetch(url); const body = await response.text(); expect(response.status).toBe(200); expect(body).toBe(uiArtifact); expect(response.headers.get("content-type")).toBe(MODULE_UI_CONTENT_TYPE); expect(response.headers.get("cache-control")).toBe(MODULE_UI_CACHE_CONTROL); expect(response.headers.get("x-content-type-options")).toBe("nosniff"); const etag = response.headers.get("etag"); expect(etag).toMatch(/^"[A-Za-z0-9_-]+"$/u); // Revalidation over the wire, with the validator the server itself just issued. const revalidated = await fetch(url, { headers: { "if-none-match": etag as string } }); expect(revalidated.status).toBe(304); expect(await revalidated.text()).toBe(""); // The exact-version rule, and the neighbouring files, over the same real transport. const unreachable = [ moduleUiArtifactUrl({ moduleId: "signage", version: "2.0.0" }), moduleUiArtifactUrl({ moduleId: "announcements", version: "2.0.1" }), `${moduleUiArtifactUrl({ moduleId: "signage", version: "2.0.1" })}/index.js.map`, `${moduleUiArtifactUrl({ moduleId: "signage", version: "2.0.1" })}/chunk.js`, `${moduleUiArtifactUrl({ moduleId: "signage", version: "2.0.1" })}/.env`, `${moduleUiArtifactUrl({ moduleId: "signage", version: "2.0.1" })}.map`, ]; for (const candidate of unreachable) { const refused = await fetch(`${origin}${candidate}`); const refusedBody = await refused.text(); expect(refused.status, candidate).toBe(404); expect(refusedBody, candidate).not.toContain("secret"); expect(refusedBody, candidate).not.toContain("mount"); } } finally { await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); it("answers a trusted browser origin with CORS, and an untrusted one without", async () => { const modulesDirectory = await createFixtureModulesDirectory(); await createUiModule(modulesDirectory, "signage", "2.0.1", uiArtifact); const database = await createDisposableDatabase("module_ui_cors"); await applyCoreMigrations(database.url); const running = await startMigratedApi(database.url, modulesDirectory); try { const origin = apiOrigin(running); const url = `${origin}${moduleUiArtifactUrl({ moduleId: "signage", version: "2.0.1" })}`; // A cross-origin browser `import()` of this URL needs exactly this header, which is why the // route lives inside the application's one CORS-governed `/api/v1/*` space. const trusted = await fetch(url, { headers: { origin: dashboardOrigin } }); expect(trusted.status).toBe(200); expect(trusted.headers.get("access-control-allow-origin")).toBe(dashboardOrigin); expect(trusted.headers.get("vary")?.toLowerCase()).toContain("origin"); const untrusted = await fetch(url, { headers: { origin: untrustedOrigin } }); // The bytes are not confidential (ADR 0025), but the platform still never reflects an origin // it does not trust, so a page on that origin cannot read the response. expect(untrusted.headers.get("access-control-allow-origin")).toBeNull(); } finally { await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); }); describe.sequential("live API — health", () => { it("answers liveness and readiness with 200 against a migrated disposable PostgreSQL", async () => { const database = await createDisposableDatabase("health"); await applyCoreMigrations(database.url); const running = await startMigratedApi(database.url); try { const origin = apiOrigin(running); const live = await fetch(`${origin}${HEALTH_LIVE_PATH}`); expect(live.status).toBe(200); expect(live.headers.get("cache-control")).toBe("no-store"); expect(await live.json()).toEqual({ status: "live" }); const ready = await fetch(`${origin}${HEALTH_READY_PATH}`); expect(ready.status).toBe(200); expect(ready.headers.get("cache-control")).toBe("no-store"); expect(await ready.json()).toEqual({ status: "ready" }); } finally { await running.shutdown(); } }); it("pings through the shared pool, and PostgreSQL itself cancels a statement that outlives the ping's transaction-local timeout", async () => { const database = await createDisposableDatabase("health_ping"); const handle = createApiDatabase(database.url); try { await expect(handle.ping(READINESS_DATABASE_TIMEOUT_MS)).resolves.toBeUndefined(); } finally { await handle.close(); } // The ping's exact statement shape, with a statement that cannot finish inside its timeout: the // server cancels it (query_canceled) long before the sleep would end, on a client that has no // client-side timeout of its own. const client = new Client({ connectionString: database.url }); await client.connect(); try { const startedAt = Date.now(); await expect( client.query( "BEGIN READ ONLY; SET LOCAL statement_timeout = 50; SELECT pg_sleep(5); COMMIT", ), ).rejects.toMatchObject({ code: "57014" }); expect(Date.now() - startedAt).toBeLessThan(2_000); } finally { await client.end(); } }); }); 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"); }); }); /** * Pass 4: shutdown is finite even when HTTP work does not settle. * * These drive a real bound listener and a real socket rather than a mock, because the behavior * under test is what Node's own `server.close()` does and does not do. Every socket, server, and * disposable database is closed in `finally`, and each scenario carries its own outer deadline so * a regression fails rather than hangs. */ describe.sequential("live API — module lifecycle governs what actually runs", () => { /** Writes one explicit enablement decision through the real table, before the API starts — * exactly what `pnpm modules:disable` or a prior `PUT` would have left behind. Nothing in this * suite reaches into the store through a double. */ async function storeEnablement(url: string, moduleId: string, enabled: boolean): Promise { const client = await openAdministrativeClient(url); try { await safeQuery( client, `insert into ekklesia_core_module_state (module_id, enabled) values ($1, $2) on conflict (module_id) do update set enabled = excluded.enabled`, [moduleId, enabled], ); } finally { await safeEnd(client); } } it("never imports, registers, starts, or mounts a disabled module, and still runs its neighbours", async () => { const modulesDirectory = await createFixtureModulesDirectory(); const recorder = createEventRecorder(); await createFixtureModule(modulesDirectory, "calendar", recorder, { permissions: [ { baseline: "public", description: "Read calendar events.", id: "calendar.events.read" }, { baseline: "public", description: "View calendar state.", id: "calendar.state.view" }, ], }); await createFixtureModule(modulesDirectory, "announcements", recorder, { permissions: [ { baseline: "public", description: "Read announcements.", id: "announcements.events.read", }, { baseline: "public", description: "View announcement state.", id: "announcements.state.view", }, ], }); const database = await createDisposableDatabase("lifecycle_off"); await applyCoreMigrations(database.url); await storeEnablement(database.url, "announcements", false); const running = await startConfiguredApi(database.url, { modulesDirectoryOverride: modulesDirectory, }); const realtimeAbort = new AbortController(); let calendarReader: ReadableStreamDefaultReader | undefined; try { const origin = apiOrigin(running); // The enabled module really did run: its markers are present, and its route answers. expect(recorder.events).toContain("calendar:register"); expect(recorder.events).toContain("calendar:start"); const calendarRoute = await fetch(`${origin}/api/v1/modules/calendar/events/abc`); expect(calendarRoute.status).toBe(200); await withTransportDeadline( (async () => { const calendarRealtime = await fetch(`${origin}/api/v1/module-realtime/calendar/state`, { signal: realtimeAbort.signal, }); expect(calendarRealtime.status).toBe(200); calendarReader = calendarRealtime.body?.getReader(); const calendarSnapshot = await calendarReader?.read(); expect(new TextDecoder().decode(calendarSnapshot?.value)).toContain("event: snapshot"); })(), () => realtimeAbort.abort(), ); // The disabled module ran nothing at all. Not "registered but skipped", not "started and // gated per request" — its compiled entrypoint was never imported, so no hook could run. expect(recorder.events).not.toContain("announcements:register"); expect(recorder.events).not.toContain("announcements:start"); expect(recorder.events.filter((event) => event.startsWith("announcements:"))).toEqual([]); // No route was ever mounted, so the platform's ordinary 404 answers — there is no per-request // "is this module enabled?" check anywhere, because there is nothing mounted to check. const disabledRoute = await fetch(`${origin}/api/v1/modules/announcements/events/abc`); expect(disabledRoute.status).toBe(404); expect(await disabledRoute.json()).toEqual({ code: "NOT_FOUND" }); const disabledRealtime = await fetch(`${origin}/api/v1/module-realtime/announcements/state`); expect(disabledRealtime.status).toBe(404); expect(await disabledRealtime.json()).toEqual({ code: "NOT_FOUND" }); // Shutdown never invokes a stop hook for a module that never started. const streamClosed = calendarReader?.read(); const shutdown = running.shutdown(); await expect( withTransportDeadline( streamClosed ?? Promise.reject(harnessError("The realtime stream was unavailable.")), () => realtimeAbort.abort(), ), ).resolves.toEqual({ done: true, value: undefined }); await shutdown; expect(recorder.events).toContain("calendar:stop"); expect(recorder.events).not.toContain("announcements:stop"); } finally { realtimeAbort.abort(); await calendarReader?.cancel().catch(() => undefined); await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); it("blocks a dependent of a disabled required dependency, and explains why over HTTP", async () => { const modulesDirectory = await createFixtureModulesDirectory(); const recorder = createEventRecorder(); await createFixtureModule(modulesDirectory, "storage", recorder); await createFixtureModule(modulesDirectory, "announcements", recorder, { dependencies: { modules: { required: { storage: "^0.1.0" } } }, permissions: [ { baseline: "public", description: "View announcement state.", id: "announcements.state.view", }, ], }); const database = await createDisposableDatabase("lifecycle_block"); await applyCoreMigrations(database.url); await storeEnablement(database.url, "storage", false); const running = await startConfiguredApi(database.url, { modulesDirectoryOverride: modulesDirectory, }); try { const origin = apiOrigin(running); // Neither ran: the disabled dependency by decision, the dependent because starting it would // violate its own required-dependency contract. expect(recorder.events).toEqual([]); const owner = await signUpPlatformOwner(origin, database.url); const lifecycle = await fetch(`${origin}/api/v1/module-lifecycle`, { headers: { cookie: owner.cookie, origin: dashboardOrigin }, }); expect(lifecycle.status).toBe(200); const body = (await lifecycle.json()) as { readonly modules: readonly { readonly id: string; readonly configured: { readonly enabled: boolean; readonly explicit: boolean }; readonly runtime: { readonly state: string; readonly reason?: string; readonly dependency?: { readonly kind: string; readonly moduleId: string }; }; readonly restartRequired: boolean; }[]; }; expect(body.modules).toEqual([ { configured: { enabled: false, explicit: true }, id: "storage", restartRequired: false, runtime: { state: "disabled" }, version: "0.1.0", }, { configured: { enabled: true, explicit: false }, id: "announcements", restartRequired: false, runtime: { dependency: { kind: "module", moduleId: "storage" }, reason: "required_dependency_disabled", state: "blocked", }, version: "0.1.0", }, ]); // The operator's question — "why is this module unavailable?" — answered without a log line, // and without a filesystem path anywhere in the payload. const raw = JSON.stringify(body); expect(raw).not.toContain(modulesDirectory); expect(raw).not.toContain("dist/"); const blockedRealtime = await fetch(`${origin}/api/v1/module-realtime/announcements/state`); expect(blockedRealtime.status).toBe(404); expect(await blockedRealtime.json()).toEqual({ code: "NOT_FOUND" }); } finally { await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); it("applies a change made over HTTP only at the next start, and says so in the meantime", async () => { const modulesDirectory = await createFixtureModulesDirectory(); const recorder = createEventRecorder(); await createFixtureModule(modulesDirectory, "calendar", recorder, { permissions: [ { baseline: "public", description: "Read calendar events.", id: "calendar.events.read" }, ], }); const database = await createDisposableDatabase("lifecycle_restart"); await applyCoreMigrations(database.url); const first = await startConfiguredApi(database.url, { modulesDirectoryOverride: modulesDirectory, }); try { const origin = apiOrigin(first); const owner = await signUpPlatformOwner(origin, database.url); const mutation = await fetch(`${origin}/api/v1/module-lifecycle/calendar/enabled`, { body: JSON.stringify({ enabled: false }), headers: { "content-type": "application/json", cookie: owner.cookie, origin: dashboardOrigin, }, method: "PUT", }); expect(mutation.status).toBe(200); expect(await mutation.json()).toEqual({ configured: { enabled: false, explicit: true }, id: "calendar", restartRequired: true, // Honest: this process really is still serving that module. runtime: { state: "running" }, version: "0.1.0", }); // And it demonstrably still is — nothing was hot-unloaded to make the change feel immediate. expect((await fetch(`${origin}/api/v1/modules/calendar/events/abc`)).status).toBe(200); } finally { await first.shutdown().catch(() => undefined); } recorder.events.length = 0; const second = await startConfiguredApi(database.url, { modulesDirectoryOverride: modulesDirectory, }); try { const origin = apiOrigin(second); // The next start applies the stored decision: nothing imported, nothing mounted. expect(recorder.events).toEqual([]); expect((await fetch(`${origin}/api/v1/modules/calendar/events/abc`)).status).toBe(404); } finally { await second.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); it("keeps a disabled module's UI out of the inventory and out of Dashboard navigation", async () => { const modulesDirectory = await createFixtureModulesDirectory(); await createUiModule( modulesDirectory, "shown", "1.0.0", 'export default { id: "shown", version: "1.0.0", mount() {} };\n', ); await createUiModule( modulesDirectory, "hidden", "1.0.0", 'export default { id: "hidden", version: "1.0.0", mount() {} };\n', ); const database = await createDisposableDatabase("lifecycle_ui"); await applyCoreMigrations(database.url); await storeEnablement(database.url, "hidden", false); const running = await startConfiguredApi(database.url, { modulesDirectoryOverride: modulesDirectory, }); try { const origin = apiOrigin(running); const owner = await signUpPlatformOwner(origin, database.url); const headers = { cookie: owner.cookie, origin: dashboardOrigin }; const inventory = (await (await fetch(`${origin}/api/v1/module-ui`, { headers })).json()) as { readonly modules: readonly { readonly id: string }[]; }; const navigation = (await ( await fetch(`${origin}/api/v1/dashboard/navigation`, { headers }) ).json()) as { readonly ungrouped: readonly { readonly module: { readonly id: string } }[]; }; expect(inventory.modules.map((entry) => entry.id)).toEqual(["shown"]); expect(navigation.ungrouped.map((entry) => entry.module.id)).toEqual(["shown"]); // The artifact itself is unreachable too, even to a caller that knows both halves of its // identity — publication is derived from the eligible set, not from the file's existence. const artifact = await fetch(`${origin}/api/v1/module-ui/hidden/1.0.0`); expect(artifact.status).toBe(404); expect((await fetch(`${origin}/api/v1/module-ui/shown/1.0.0`)).status).toBe(200); } finally { await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); it("still discovers a disabled module's migrations: deployed schema ownership is not runtime state", async () => { const modulesDirectory = await createFixtureModulesDirectory(); const recorder = createEventRecorder(); await createFixtureModule(modulesDirectory, "calendar", recorder); const database = await createDisposableDatabase("lifecycle_migrations"); await applyCoreMigrations(database.url); await storeEnablement(database.url, "calendar", false); // Migration discovery is offline and reads the deployed inventory, never the enablement table. // A disabled module's tables must still be migrated, or enabling it later would need a schema // change in the middle of a restart. const aggregate = await discoverAllMigrations({ coreMigrationsDirectory, moduleDiscovery: { moduleSdkVersion: "0.1.0", modulesDirectory, platformVersion: "0.1.0", }, }); expect(aggregate.discoveredModules.map((module) => module.manifest.id)).toEqual(["calendar"]); }); }); describe.sequential("live API — bounded HTTP drain and shutdown", () => { async function withDeadline(operation: Promise, timeoutMs: number): Promise { let timer: ReturnType | undefined; try { return await Promise.race([ operation, new Promise((_resolve, reject) => { timer = setTimeout( () => reject(harnessError("shutdown did not settle in time")), timeoutMs, ); }), ]); } finally { clearTimeout(timer); } } it("serves a real request and then drains cleanly, well inside the deadline", async () => { const database = await createDisposableDatabase("drain_ok"); await applyCoreMigrations(database.url); const running = await startConfiguredApi(database.url, { drainTimeoutMsOverride: 5_000 }); try { const origin = apiOrigin(running); const response = await fetch(`${origin}/api/v1/principal`); expect(response.status).toBe(200); expect(await response.json()).toEqual({ kind: "anonymous" }); // Nothing is stalled, so the graceful path completes and shutdown reports success — the // drain deadline is never reached and no connection is force-closed. await expect(withDeadline(running.shutdown(), 20_000)).resolves.toBeUndefined(); // The listener really is closed afterwards. await expect(fetch(`${origin}/api/v1/principal`)).rejects.toBeTruthy(); } finally { await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); it("lets a handler that has already entered finish, and closes its transport before module stop", async () => { // A real handler gate, not an inference from the connection count: the fixture module's route // handler announces that it has *entered*, then blocks until this test releases it. Shutdown // therefore begins at a point where the handler is provably inside user code, and the ordering // claim below is an assertion about recorded events rather than about timing. const modulesDirectory = await createFixtureModulesDirectory(); const recorder = createEventRecorder(); const gate = createHandlerGate(); await createGatedRouteModule(modulesDirectory, "drainorder", recorder, gate); const database = await createDisposableDatabase("drain_order"); await applyCoreMigrations(database.url); const running = await startConfiguredApi(database.url, { drainTimeoutMsOverride: 20_000, modulesDirectoryOverride: modulesDirectory, }); 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 }); let shutdown: Promise | undefined; try { const responseBytes: Buffer[] = []; const responseClosed = new Promise((resolve) => { socket.once("close", () => resolve()); }); socket.on("data", (chunk: Buffer) => { responseBytes.push(chunk); }); await new Promise((resolve, reject) => { socket.once("error", reject); socket.write( `GET /api/v1/modules/drainorder/gated HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n`, () => resolve(), ); }); // Bounded wait on the handler's own signal — no sleep, no proxy for "entered". await withDeadline(gate.entered, 20_000); expect(recorder.events).toContain("drainorder:handler-entered"); // Shutdown starts while the handler is still inside user code. shutdown = running.shutdown(); // Release it, then require the graceful path to complete. gate.release(); await expect(withDeadline(shutdown, 30_000)).resolves.toBeUndefined(); // The response really was served, in full, rather than force-closed mid-flight. await withDeadline(responseClosed, 10_000); const raw = Buffer.concat(responseBytes).toString("utf8"); expect(raw).toContain("HTTP/1.1 200"); expect(raw).toContain('"gated":true'); // Explicit ordering: the already-running command can publish its committed state while HTTP // drains, then the handler finishes and only afterward does its module stop. const published = recorder.events.indexOf("drainorder:handler-published"); const finished = recorder.events.indexOf("drainorder:handler-finished"); const stopped = recorder.events.indexOf("drainorder:stop"); expect(published).toBeGreaterThanOrEqual(0); expect(finished).toBeGreaterThanOrEqual(0); expect(stopped).toBeGreaterThanOrEqual(0); expect(published).toBeLessThan(finished); expect(finished).toBeLessThan(stopped); } finally { gate.release(); if (shutdown) { await shutdown.catch(() => undefined); } socket.destroy(); await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); it("force-closes a stalled connection at the drain deadline and still reaches pool closure", async () => { const database = await createDisposableDatabase("drain_forced"); await applyCoreMigrations(database.url); // A short, test-only drain deadline; production keeps HTTP_DRAIN_TIMEOUT_MS unchanged. const running = await startConfiguredApi(database.url, { drainTimeoutMsOverride: 250 }); 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 }); try { // A real, deliberately incomplete request: headers are never finished, so Node keeps the // connection open and `server.close()` alone would wait indefinitely. await new Promise((resolve, reject) => { socket.once("error", reject); socket.on("data", () => undefined); socket.write("GET /api/v1/principal HTTP/1.1\r\nHost: 127.0.0.1\r\n", () => resolve()); }); const closed = new Promise((resolve) => { socket.once("close", () => resolve()); }); let captured: unknown; try { await withDeadline(running.shutdown(), 20_000); } catch (error) { captured = error; } // A forced drain is a genuine shutdown failure, never reported as success. expect(captured).toMatchObject({ drainTimedOut: true, name: "ApiShutdownError" }); // ...and it still carries nothing about the request, socket, or database. const publicText = `${String(captured)}\n${(captured as Error).stack ?? ""}`; expect(publicText).not.toContain("127.0.0.1"); expect(publicText).not.toContain(database.name); // The connection was actually closed by the platform, not left dangling. await withDeadline(closed, 10_000); } finally { socket.destroy(); await running.shutdown().catch(() => undefined); await dropDatabase(database.name); } }); it("shares one memoized shutdown across concurrent callers", async () => { const database = await createDisposableDatabase("drain_once"); await applyCoreMigrations(database.url); const running = await startConfiguredApi(database.url, { drainTimeoutMsOverride: 5_000 }); try { const [first, second, third] = await withDeadline( Promise.all([running.shutdown(), running.shutdown(), running.shutdown()]), 20_000, ); expect([first, second, third]).toEqual([undefined, undefined, undefined]); // A later caller observes the same completed result rather than restarting cleanup. await expect(running.shutdown()).resolves.toBeUndefined(); } finally { await dropDatabase(database.name); } }); });