Files
Ekklesia/apps/api/tests/api-http.integration.ts
T
ekklesiaandClaude Sonnet 5 d3c6bd73d3 fix: enforce API request limits
Add a single platform-owned request-body policy (hono/body-limit) with
small, named limits for Better Auth and Core's grant-mutation endpoint, and
a new required maxBodyBytes field on the executable module route contract so
every module route makes a conscious no-body-or-bounded-body choice, capped
at a platform ceiling. Every oversized request gets one sanitized 413
contract and never reaches business logic, authorization, or a handler.
Also configure finite Node header/request/keep-alive timeouts, including the
non-obvious connectionsCheckingInterval needed for them to take effect
promptly, so a slow-header or slow-body connection cannot hold a socket
indefinitely.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-26 12:29:24 -05:00

2064 lines
80 KiB
TypeScript

import { randomBytes } from "node:crypto";
import { request as httpRequest } from "node:http";
import net from "node:net";
import { fileURLToPath } from "node:url";
import path from "node:path";
import { parseEkklesiaUserId, parsePermissionId, SYSTEM_ACTOR } from "@ekklesia/contracts";
import {
bootstrapPlatformOwner,
createAuthorizationDrizzleDatabase,
createAuthorizationStore,
} from "@ekklesia/core/authorization";
import { diagnoseEkklesia } from "@ekklesia/core/diagnostics";
import {
applyModuleMigrations,
createPostgresMigrationDatabase,
discoverAllMigrations,
} from "@ekklesia/core/migrations";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { Client, type QueryResultRow } from "pg";
import { afterAll, describe, expect, it, vi } from "vitest";
import type { ClientAddressConfigInput } from "../src/client-address.js";
import { createApiRuntimeConfig } from "../src/config.js";
import { startApi, type NodeServerTimeouts, type RunningApi } from "../src/server.js";
const integrationDatabaseUrlEnvironment = "EKKLESIA_MIGRATION_TEST_DATABASE_URL";
const coreMigrationsDirectory = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"..",
"core",
"drizzle",
);
const trackedDatabases = new Set<string>();
let databaseCounter = 0;
class IntegrationHarnessError extends Error {
constructor(message: string) {
super(message);
this.name = "IntegrationHarnessError";
this.stack = `${this.name}: ${this.message}`;
}
}
function harnessError(message: string): IntegrationHarnessError {
return new IntegrationHarnessError(message);
}
function baseDatabaseUrl(): string {
const value = process.env[integrationDatabaseUrlEnvironment];
if (typeof value !== "string" || value.trim() === "") {
throw harnessError("The API integration database is not configured.");
}
try {
const parsed = new URL(value);
if (parsed.protocol !== "postgres:" && parsed.protocol !== "postgresql:") {
throw new TypeError("Unsupported protocol.");
}
} catch {
throw harnessError("The API integration database configuration is invalid.");
}
return value;
}
function quoteDatabaseName(databaseName: string): string {
if (!/^[a-z][a-z0-9_]{0,62}$/u.test(databaseName)) {
throw harnessError("The generated integration database name is invalid.");
}
return `"${databaseName}"`;
}
function makeDatabaseName(label: string): string {
const safeLabel = label
.toLowerCase()
.replaceAll(/[^a-z0-9]+/gu, "_")
.replaceAll(/^_+|_+$/gu, "")
.slice(0, 14);
databaseCounter += 1;
return `ekk_api_${process.pid}_${databaseCounter}_${safeLabel || "scenario"}_${randomBytes(4).toString("hex")}`;
}
function databaseUrl(databaseName: string): string {
const parsed = new URL(baseDatabaseUrl());
parsed.pathname = `/${databaseName}`;
parsed.searchParams.delete("application_name");
return parsed.toString();
}
async function openAdministrativeClient(connectionString: string): Promise<Client> {
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<Row extends QueryResultRow>(
client: Client,
text: string,
values: unknown[] = [],
): Promise<readonly Row[]> {
try {
const result = await client.query<Row>(text, values);
return result.rows;
} catch {
throw harnessError("An integration database administration operation failed.");
}
}
async function safeEnd(client: Client): Promise<void> {
try {
await client.end();
} catch {
throw harnessError("An integration database connection could not be closed.");
}
}
async function createDatabase(databaseName: string): Promise<void> {
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<void> {
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<number> {
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<void> {
const emptyModulesDirectory = await mkdtemp(path.join(tmpdir(), "ekklesia-api-no-modules-"));
try {
const aggregate = await discoverAllMigrations({
coreMigrationsDirectory,
moduleDiscovery: {
moduleSdkVersion: "0.1.0",
modulesDirectory: emptyModulesDirectory,
platformVersion: "0.1.0",
},
});
// Phase 4A's and Phase 4C pass 1's committed migrations must not have drifted while building
// this transport slice.
expect(aggregate.migrations.map((entry) => entry.identity)).toEqual([
"core:0000_identity_core_tables",
"core:0001_authorization_core_tables",
]);
expect(aggregate.migrations[0]?.checksum).toBe(
"f8f2086ef06f7a6943e8ffc4350bb010b37aed04c0a72030d2b153c986b1325f",
);
expect(aggregate.migrations[1]?.checksum).toBe(
"830e1fbc26d05af08df8d8710c3d10e1f3016d32c41b8363cb661c49afcbe81e",
);
await applyModuleMigrations({ aggregate, database: createPostgresMigrationDatabase(url) });
} finally {
await rm(emptyModulesDirectory, { recursive: true, force: true });
}
}
const trackedModulesDirectories: string[] = [];
const eventSymbols: symbol[] = [];
async function createFixtureModulesDirectory(): Promise<string> {
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<PropertyKey, unknown>)[symbol] = events;
eventSymbols.push(symbol);
return { events, expression: `globalThis[Symbol.for(${JSON.stringify(key)})]` };
}
/** Writes a minimal, structurally valid module fixture straight to disk — real discovery,
* composition, and (where a scenario actually starts the API) real module runtime execution all
* run against it, nothing hand-built. Register/start/stop push markers onto `recorder.events`
* instead of throwing, so both "Doctor/composition never executed this" (an empty recorder) and
* "startApi actually imported, registered, and started this" (a populated recorder) can be proven
* against the exact same fixture. When declared with the `calendar.events.read` permission, the
* module also registers a restricted `GET /events/:eventId` route and a restricted, state-changing
* `POST /events` route against that same permission, so the full HTTP mounting/enforcement chain
* can be exercised live. */
async function createFixtureModule(
modulesDirectory: string,
id: string,
recorder: { expression: string },
manifestOverrides: Record<string, unknown> = {},
): Promise<void> {
const moduleDirectory = path.join(modulesDirectory, id);
const manifest = {
manifestVersion: 3,
id,
name: `${id} module`,
version: "0.1.0",
compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0" },
permissions: [],
entrypoints: { server: "./dist/server/index.js" },
...manifestOverrides,
};
const declaresCalendarRead = (manifest.permissions as ReadonlyArray<{ id: string }>).some(
(declaration) => declaration.id === "calendar.events.read",
);
await mkdir(path.join(moduleDirectory, "dist/server"), { recursive: true });
await Promise.all([
writeFile(
path.join(moduleDirectory, "package.json"),
JSON.stringify({
ekklesia: { layer: "module" },
name: `@test/module-${id}`,
type: "module",
version: "0.1.0",
}),
),
writeFile(path.join(moduleDirectory, "ekklesia.module.json"), JSON.stringify(manifest)),
writeFile(
path.join(moduleDirectory, "dist/server/index.js"),
`export default {
id: ${JSON.stringify(id)},
version: "0.1.0",
register({ http }) {
${recorder.expression}.push(${JSON.stringify(`${id}:register`)});
${
declaresCalendarRead
? `http.route({
method: "GET",
path: "/events/:eventId",
permission: "calendar.events.read",
maxBodyBytes: 0,
async handler(request, context) {
return Response.json({ eventId: context.params.eventId, principal: context.principal });
},
});
http.route({
method: "POST",
path: "/events",
permission: "calendar.events.read",
maxBodyBytes: 1024,
async handler(request, context) {
return Response.json({ created: true, principal: context.principal }, { status: 201 });
},
});`
: ""
}
},
start() {
${recorder.expression}.push(${JSON.stringify(`${id}:start`)});
},
stop() {
${recorder.expression}.push(${JSON.stringify(`${id}:stop`)});
},
};`,
),
]);
}
afterAll(async () => {
for (const databaseName of [...trackedDatabases]) {
await dropDatabase(databaseName).catch(() => undefined);
}
for (const directory of trackedModulesDirectories.splice(0)) {
await rm(directory, { recursive: true, force: true }).catch(() => undefined);
}
const globalValues = globalThis as unknown as Record<PropertyKey, unknown>;
for (const symbol of eventSymbols.splice(0)) {
delete globalValues[symbol];
}
});
const identityBaseUrl = "https://identity.example.test";
const dashboardOrigin = "https://dashboard.example.test";
const untrustedOrigin = "https://evil.example.test";
function apiOrigin(running: RunningApi): string {
const address = running.server.address();
if (!address || typeof address !== "object") {
throw harnessError("The API integration server has no bound address.");
}
return `http://127.0.0.1:${address.port}`;
}
interface StartConfiguredApiOptions {
readonly clientAddress?: ClientAddressConfigInput;
readonly modulesDirectoryOverride?: string;
readonly nodeServerTimeoutOverride?: Partial<NodeServerTimeouts>;
/** Defaults to `true`: almost every existing scenario in this suite signs accounts up over HTTP
* and needs it enabled. Pass `false` explicitly for the dedicated signup-gating scenarios. */
readonly publicSignupEnabled?: boolean;
/** Defaults to a generous override so this suite's own request volume never trips Better Auth's
* strict built-in sign-in/sign-up rule. The dedicated rate-limit/client-address scenarios pass
* `undefined` explicitly to exercise the real, unmodified production rule instead. */
readonly rateLimitOverride?: { readonly max: number; readonly window: number } | undefined;
}
async function startConfiguredApi(
url: string,
options: StartConfiguredApiOptions = {},
): Promise<RunningApi> {
const config = createApiRuntimeConfig(
{
...(options.clientAddress === undefined ? {} : { clientAddress: options.clientAddress }),
databaseUrl: url,
host: "127.0.0.1",
identity: {
baseUrl: identityBaseUrl,
publicSignupEnabled: options.publicSignupEnabled ?? true,
secret: "integration-test-secret-0123456789abcdefgh",
trustedOrigins: [identityBaseUrl, dashboardOrigin],
},
port: 0,
},
{ environment: "production" },
);
return startApi(config, {
...("rateLimitOverride" in options
? { ...(options.rateLimitOverride ? { rateLimitOverride: options.rateLimitOverride } : {}) }
: { rateLimitOverride: { max: 1000, window: 1 } }),
...(options.modulesDirectoryOverride === undefined
? {}
: { modulesDirectoryOverride: options.modulesDirectoryOverride }),
...(options.nodeServerTimeoutOverride === undefined
? {}
: { nodeServerTimeoutOverride: options.nodeServerTimeoutOverride }),
});
}
async function startMigratedApi(
url: string,
modulesDirectoryOverride?: string,
): Promise<RunningApi> {
// Narrow, explicit test-only overrides: production composition (server.ts's `main`) never sets
// either. Better Auth's built-in sign-in/sign-up rate limit (3 requests / 10s) would otherwise
// reject this suite's many sequential requests; `modulesDirectoryOverride` lets a scenario point
// permission composition at a real, disposable module fixture instead of this repository's own
// (empty) `modules/` directory.
return startConfiguredApi(url, {
...(modulesDirectoryOverride === undefined ? {} : { modulesDirectoryOverride }),
});
}
function setCookieValue(response: Response): string | undefined {
return response.headers.get("set-cookie") ?? undefined;
}
function firstCookiePair(setCookieHeader: string): string {
return setCookieHeader.split(",")[0]?.split(";")[0]?.trim() ?? "";
}
interface SignUpResult {
readonly response: Response;
readonly body: { token: string | null; user: { id: string; email: string } };
}
async function signUp(
origin: string,
email: string,
password: string,
headers: Record<string, string> = {},
): Promise<SignUpResult> {
const response = await fetch(`${origin}/api/auth/sign-up/email`, {
body: JSON.stringify({ email, name: "Integration Test User", password }),
// A trusted Origin by default, matching signIn below: Node's own fetch() (and every browser)
// automatically sends `Sec-Fetch-Mode: cors`, which Better Auth's first-login CSRF protection
// treats as "this is a Fetch-API call" and requires a trusted Origin/Referer for regardless of
// whether a cookie is present — see the dedicated no-Origin scenario below, which uses a raw
// `http.request` instead of `fetch()` to exercise a genuinely Fetch-Metadata-free caller.
headers: { "content-type": "application/json", origin: dashboardOrigin, ...headers },
method: "POST",
});
return { body: (await response.clone().json()) as SignUpResult["body"], response };
}
interface SignInResult {
readonly cookiePair: string | undefined;
readonly response: Response;
}
async function signIn(
origin: string,
email: string,
password: string,
headers: Record<string, string> = {},
): Promise<SignInResult> {
const response = await fetch(`${origin}/api/auth/sign-in/email`, {
body: JSON.stringify({ email, password }),
headers: {
"content-type": "application/json",
origin: dashboardOrigin,
...headers,
},
method: "POST",
});
const setCookie = setCookieValue(response);
return { cookiePair: setCookie ? firstCookiePair(setCookie) : undefined, response };
}
async function fetchPrincipal(
origin: string,
headers: Record<string, string> = {},
): 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<string, string | string[] | undefined>;
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<RawResponse> {
return new Promise((resolve, reject) => {
const url = new URL("/api/auth/sign-in/email", origin);
const payload = JSON.stringify({
email: "[email protected]",
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 = "[email protected]";
const emailTwo = "[email protected]";
const password = "correct horse battery staple";
it("applies migrations and starts the API against a freshly migrated database", async () => {
database = await createDisposableDatabase("signup_flow");
await applyCoreMigrations(database.url);
running = await startMigratedApi(database.url);
origin = apiOrigin(running);
expect(running.server.listening).toBe(true);
});
it("returns the anonymous principal shape before any sign-in", async () => {
const { body, response } = await fetchPrincipal(origin);
expect(response.status).toBe(200);
expect(body).toEqual({ kind: "anonymous" });
});
it("publicly creates the first ordinary identity via sign-up, with no automatic session (autoSignIn: false)", async () => {
const { body, response } = await signUp(origin, emailOne, password);
expect(response.status).toBe(200);
expect(body.token).toBeNull();
expect(body.user.email).toBe(emailOne);
expect(setCookieValue(response)).toBeUndefined();
});
it("does not sign in automatically after sign-up", async () => {
const { body } = await fetchPrincipal(origin);
expect(body).toEqual({ kind: "anonymous" });
});
it("rejects wrong credentials without leaking raw internal information", async () => {
const { response } = await signIn(origin, emailOne, "wrong password entirely");
const text = await response.text();
expect(response.status).toBe(401);
expect(text).not.toContain(database.url);
expect(text).not.toContain("postgres");
expect(text.toLowerCase()).not.toContain("stack");
});
let sessionCookie: string;
it("signs in explicitly and returns a valid, host-only, HttpOnly, SameSite=Lax, secure session cookie", async () => {
const { cookiePair, response } = await signIn(origin, emailOne, password);
const setCookie = setCookieValue(response);
expect(response.status).toBe(200);
expect(setCookie).toBeDefined();
expect(cookiePair).toBeDefined();
sessionCookie = cookiePair as string;
const cookieText = setCookie as string;
expect(cookieText).toContain("HttpOnly");
expect(cookieText).toContain("SameSite=Lax");
expect(cookieText).toContain("Secure");
expect(cookieText).not.toMatch(/domain=/iu);
});
it("resolves the authenticated principal containing only kind and the opaque user id", async () => {
const { body, response } = await fetchPrincipal(origin, { cookie: sessionCookie });
expect(response.status).toBe(200);
expect(body).toMatchObject({ kind: "authenticated" });
const record = body as Record<string, unknown>;
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: "[email protected]",
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, "[email protected]", "1234567");
expect(response.status).toBe(400);
});
it("accepts an 8-character password", async () => {
const { response, body } = await signUp(origin, "[email protected]", "12345678");
expect(response.status).toBe(200);
expect(body.user.email).toBe("[email protected]");
});
it("accepts a 128-character password", async () => {
const { response } = await signUp(origin, "[email protected]", "a".repeat(128));
expect(response.status).toBe(200);
});
it("rejects a 129-character password as too long", async () => {
const { response } = await signUp(origin, "[email protected]", "a".repeat(129));
expect(response.status).toBe(400);
});
it("leaves Better Auth's handler unavailable outside /api/auth/*", async () => {
const response = await fetch(`${origin}/sign-in/email`, { method: "POST" });
expect(response.status).toBe(404);
});
it("shuts down cleanly, closing the HTTP listener and the PostgreSQL pool", async () => {
await running.shutdown();
await expect(fetch(origin, { signal: AbortSignal.timeout(1_000) })).rejects.toBeTruthy();
const remainingConnections = await activeConnectionCount(database.name);
expect(remainingConnections).toBe(0);
await dropDatabase(database.name);
});
});
describe.sequential("live API — startup and migration guarantees", () => {
it("does not create any tables or migration history when started against an unmigrated database", async () => {
const database = await createDisposableDatabase("unmigrated");
try {
const config = createApiRuntimeConfig(
{
databaseUrl: database.url,
host: "127.0.0.1",
identity: {
baseUrl: identityBaseUrl,
publicSignupEnabled: true,
secret: "integration-test-secret-0123456789abcdefgh",
trustedOrigins: [identityBaseUrl],
},
port: 0,
},
{ environment: "production" },
);
const running = await startApi(config, { rateLimitOverride: { max: 1000, window: 1 } });
const origin = apiOrigin(running);
try {
// A request that touches the (nonexistent) identity tables fails, but must not repair
// or create anything. The exact failure body is not asserted here (Better Auth's
// internal-error shape for this case is not a stable public contract); only that it is
// not a successful sign-up.
const response = await fetch(`${origin}/api/auth/sign-up/email`, {
body: JSON.stringify({
email: "[email protected]",
name: "Integration Test User",
password: "correct horse battery staple",
}),
headers: { "content-type": "application/json" },
method: "POST",
});
expect(response.status).not.toBe(200);
} finally {
await running.shutdown();
}
const admin = await openAdministrativeClient(database.url);
try {
const rows = await safeQuery<{ count: string }>(
admin,
"select count(*)::text as count from information_schema.tables where table_schema = 'public'",
);
expect(rows[0]?.count).toBe("0");
} finally {
await safeEnd(admin);
}
} finally {
await dropDatabase(database.name);
}
});
});
describe.sequential("live API — principal infrastructure failure is never downgraded to anonymous", () => {
it("returns a sanitized failure, not a false anonymous 200, when the database becomes unreachable mid-session", async () => {
const database = await createDisposableDatabase("infra_failure");
await applyCoreMigrations(database.url);
const running = await startMigratedApi(database.url);
const origin = apiOrigin(running);
await signUp(origin, "[email protected]", "correct horse battery staple");
const { cookiePair } = await signIn(
origin,
"[email protected]",
"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 — 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, "[email protected]", "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: "[email protected]",
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 = "[email protected]";
const managerEmail = "[email protected]";
const password = "correct horse battery staple";
// This scenario intentionally proves out the explicit-grant lifecycle (create, list, use,
// revoke) against Core's own grant-management permission rather than a module's — a module
// permission would exercise the exact same route and evaluator code, adding coverage breadth
// without coverage depth. The "live API — installed module permission end-to-end" scenario
// below is where a real installed module's declared permission is proven end-to-end instead.
const testPermission = "core.authorization.grants.manage";
// Well-formed (passes `PermissionId` shape validation) but never registered — used to prove the
// live route rejects it even for a caller who otherwise has full grant-management authority.
const unregisteredPermission = "calendar.events.read";
it("starts a freshly migrated API and signs up the eventual owner and eventual delegated manager", async () => {
database = await createDisposableDatabase("authz_http");
await applyCoreMigrations(database.url);
running = await startMigratedApi(database.url);
origin = apiOrigin(running);
const ownerSignUp = await signUp(origin, ownerEmail, password);
ownerUserId = ownerSignUp.body.user.id;
const managerSignUp = await signUp(origin, managerEmail, password);
managerUserId = managerSignUp.body.user.id;
const ownerSignIn = await signIn(origin, ownerEmail, password);
expect(ownerSignIn.cookiePair).toBeDefined();
ownerCookie = ownerSignIn.cookiePair as string;
});
it("denies grants access to an anonymous caller", async () => {
const response = await fetch(`${origin}/api/v1/grants?userId=${ownerUserId}`);
expect(response.status).toBe(403);
expect(await response.json()).toEqual({ code: "AUTHORIZATION_DENIED" });
});
it("denies grants access to an authenticated caller with no owner authority and no grant", async () => {
const managerSignInBefore = await signIn(origin, managerEmail, password);
const response = await fetch(`${origin}/api/v1/grants?userId=${ownerUserId}`, {
headers: { cookie: managerSignInBefore.cookiePair as string },
});
expect(response.status).toBe(403);
});
it("bootstraps the owner via Core's real bootstrap operation, then confirms owner authority over the HTTP grants API", async () => {
// Exactly what `pnpm authorization:bootstrap-owner` would do, driven directly against the
// same disposable database the running API already uses — no second process needed to prove
// the HTTP layer honors real platform-owner state.
const { close, db } = createAuthorizationDrizzleDatabase(database.url);
try {
await bootstrapPlatformOwner(db, { userId: parseEkklesiaUserId(ownerUserId) });
} finally {
await close();
}
const response = await fetch(`${origin}/api/v1/grants?userId=${ownerUserId}`, {
headers: { cookie: ownerCookie },
});
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ grants: [] });
});
it("lets the owner delegate grants.manage to another account through the explicit-grant HTTP API", async () => {
const response = await fetch(`${origin}/api/v1/grants`, {
body: JSON.stringify({
permission: "core.authorization.grants.manage",
subjectUserId: managerUserId,
}),
headers: {
"content-type": "application/json",
cookie: ownerCookie,
origin: dashboardOrigin,
},
method: "POST",
});
const body = (await response.json()) as { grant: { sourceKind: string; userId: string } };
expect(response.status).toBe(201);
expect(body.grant.sourceKind).toBe("explicit");
expect(body.grant.userId).toBe(managerUserId);
});
it("lets the delegated manager — who is not a platform owner — manage another user's grants", async () => {
const managerSignIn = await signIn(origin, managerEmail, password);
expect(managerSignIn.cookiePair).toBeDefined();
managerCookie = managerSignIn.cookiePair as string;
const response = await fetch(`${origin}/api/v1/grants?userId=${ownerUserId}`, {
headers: { cookie: managerCookie },
});
expect(response.status).toBe(200);
});
it("rejects a well-formed but unregistered permission even from a caller with full grant-management authority, and writes no grant or audit row", async () => {
const admin = await openAdministrativeClient(database.url);
try {
const grantCountBefore = await safeQuery<{ count: string }>(
admin,
`select count(*)::text as count from "ekklesia_core_grant" where permission_id = $1`,
[unregisteredPermission],
);
const auditCountBefore = await safeQuery<{ count: string }>(
admin,
`select count(*)::text as count from "ekklesia_core_authorization_audit" where permission_id = $1`,
[unregisteredPermission],
);
expect(grantCountBefore[0]?.count).toBe("0");
expect(auditCountBefore[0]?.count).toBe("0");
const response = await fetch(`${origin}/api/v1/grants`, {
body: JSON.stringify({
permission: unregisteredPermission,
subjectUserId: managerUserId,
}),
headers: {
"content-type": "application/json",
cookie: managerCookie,
origin: dashboardOrigin,
},
method: "POST",
});
expect(response.status).toBe(403);
expect(await response.json()).toEqual({ code: "AUTHORIZATION_DENIED" });
const grantCountAfter = await safeQuery<{ count: string }>(
admin,
`select count(*)::text as count from "ekklesia_core_grant" where permission_id = $1`,
[unregisteredPermission],
);
const auditCountAfter = await safeQuery<{ count: string }>(
admin,
`select count(*)::text as count from "ekklesia_core_authorization_audit" where permission_id = $1`,
[unregisteredPermission],
);
expect(grantCountAfter[0]?.count).toBe("0");
expect(auditCountAfter[0]?.count).toBe("0");
} finally {
await safeEnd(admin);
}
});
it("creates an explicit grant via HTTP, deriving sourceKind and actor server-side, and records an atomic audit entry", async () => {
const response = await fetch(`${origin}/api/v1/grants`, {
body: JSON.stringify({
permission: testPermission,
scope: "calendar-event:1",
subjectUserId: managerUserId,
}),
headers: {
"content-type": "application/json",
cookie: managerCookie,
origin: dashboardOrigin,
},
method: "POST",
});
const body = (await response.json()) as {
grant: { id: string; sourceKind: string; userId: string };
};
expect(response.status).toBe(201);
expect(body.grant.sourceKind).toBe("explicit");
expect(body.grant.userId).toBe(managerUserId);
createdGrantId = body.grant.id;
const admin = await openAdministrativeClient(database.url);
try {
const rows = await safeQuery<{ action: string; actor_id: string | null }>(
admin,
`select action, actor_id from "ekklesia_core_authorization_audit" where grant_id = $1`,
[createdGrantId],
);
expect(rows).toEqual([{ action: "grant.create", actor_id: managerUserId }]);
} finally {
await safeEnd(admin);
}
});
it("lists the explicit grant via HTTP", async () => {
const response = await fetch(`${origin}/api/v1/grants?userId=${managerUserId}`, {
headers: { cookie: ownerCookie },
});
const body = (await response.json()) as { grants: Array<{ id: string }> };
expect(response.status).toBe(200);
expect(body.grants.some((grant) => grant.id === createdGrantId)).toBe(true);
});
it("rejects a create-grant request carrying disallowed fields such as actor or sourceKind, even alongside otherwise-valid fields", async () => {
const response = await fetch(`${origin}/api/v1/grants`, {
body: JSON.stringify({
actor: { type: "system" },
permission: testPermission,
sourceKind: "relationship",
subjectUserId: managerUserId,
}),
headers: {
"content-type": "application/json",
cookie: ownerCookie,
origin: dashboardOrigin,
},
method: "POST",
});
expect(response.status).toBe(400);
});
it("rejects a mutation request with no trusted Origin header, and never returns a permissive CORS header", async () => {
const response = await fetch(`${origin}/api/v1/grants`, {
body: JSON.stringify({ permission: testPermission, subjectUserId: managerUserId }),
headers: { "content-type": "application/json", cookie: ownerCookie },
method: "POST",
});
expect(response.status).toBe(403);
expect(await response.json()).toEqual({ code: "UNTRUSTED_ORIGIN" });
expect(response.headers.get("access-control-allow-origin")).toBeNull();
});
it("rejects a mutation request from an untrusted Origin the same way", async () => {
const response = await fetch(`${origin}/api/v1/grants`, {
body: JSON.stringify({ permission: testPermission, subjectUserId: managerUserId }),
headers: {
"content-type": "application/json",
cookie: ownerCookie,
origin: untrustedOrigin,
},
method: "POST",
});
expect(response.status).toBe(403);
expect(response.headers.get("access-control-allow-origin")).toBeNull();
});
it("revokes the explicit grant via HTTP and records the revocation audit entry", async () => {
const response = await fetch(`${origin}/api/v1/grants/${createdGrantId}/revoke`, {
headers: { cookie: ownerCookie, origin: dashboardOrigin },
method: "POST",
});
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ success: true });
const admin = await openAdministrativeClient(database.url);
try {
const rows = await safeQuery<{ action: string }>(
admin,
`select action from "ekklesia_core_authorization_audit" where grant_id = $1 order by created_at`,
[createdGrantId],
);
expect(rows.map((row) => row.action)).toEqual(["grant.create", "grant.revoke"]);
} finally {
await safeEnd(admin);
}
});
it("never exposes a relationship-derived grant through the generic list, create, or revoke HTTP API", async () => {
// Seeded directly through the store, simulating a module's own business operation — never
// through HTTP, which has no way to express sourceKind at all.
const { close, db } = createAuthorizationDrizzleDatabase(database.url);
let relationshipGrantId: string;
try {
const store = createAuthorizationStore(db);
const relationshipGrant = await store.createGrant({
actor: SYSTEM_ACTOR,
permission: parsePermissionId("roster.volunteer.read"),
sourceKind: "relationship",
sourceRef: "roster:volunteer:99",
userId: parseEkklesiaUserId(managerUserId),
});
relationshipGrantId = relationshipGrant.id;
} finally {
await close();
}
const listResponse = await fetch(`${origin}/api/v1/grants?userId=${managerUserId}`, {
headers: { cookie: ownerCookie },
});
const listBody = (await listResponse.json()) as { grants: Array<{ id: string }> };
expect(listBody.grants.some((grant) => grant.id === relationshipGrantId)).toBe(false);
const revokeResponse = await fetch(`${origin}/api/v1/grants/${relationshipGrantId}/revoke`, {
headers: { cookie: ownerCookie, origin: dashboardOrigin },
method: "POST",
});
expect(revokeResponse.status).toBe(404);
expect(await revokeResponse.json()).toEqual({ code: "AUTHORIZATION_GRANT_NOT_FOUND" });
});
it("shuts down cleanly, closing the one shared pool", async () => {
await running.shutdown();
const remainingConnections = await activeConnectionCount(database.name);
expect(remainingConnections).toBe(0);
await dropDatabase(database.name);
});
});
describe.sequential("live API — authorization infrastructure failure is never downgraded to a false denial", () => {
it("returns a sanitized 503, not a 403, when the database becomes unreachable mid-session", async () => {
const database = await createDisposableDatabase("authz_infra_failure");
await applyCoreMigrations(database.url);
const running = await startMigratedApi(database.url);
const origin = apiOrigin(running);
await signUp(origin, "[email protected]", "correct horse battery staple");
const { cookiePair } = await signIn(
origin,
"[email protected]",
"correct horse battery staple",
);
expect(cookiePair).toBeDefined();
// Force the database itself unreachable out from under the running API pool — the same
// technique the identity infra-failure test above uses, applied to a grants route this time.
await dropDatabase(database.name);
const response = await fetch(`${origin}/api/v1/grants?userId=does-not-matter`, {
headers: { cookie: cookiePair as string },
});
expect(response.status).toBe(503);
const body = (await response.json()) as { code: string };
expect(["PRINCIPAL_RESOLUTION_FAILED", "AUTHORIZATION_EVALUATION_FAILED"]).toContain(body.code);
await running.shutdown().catch(() => undefined);
});
});
describe.sequential("live API — installed module permission end-to-end", () => {
let database: { name: string; url: string };
let modulesDirectory: string;
let running: RunningApi;
let origin: string;
let ownerUserId: string;
let ownerCookie: string;
let createdGrantId: string;
const ownerEmail = "[email protected]";
const password = "correct horse battery staple";
const modulePermission = "calendar.events.read";
const absentPermission = "scheduler.jobs.read";
const recorder = createEventRecorder();
it("recognizes a real installed module and passes permission composition in Doctor, without executing its server entrypoint", async () => {
modulesDirectory = await createFixtureModulesDirectory();
await createFixtureModule(modulesDirectory, "calendar", recorder, {
permissions: [
{
baseline: "restricted",
description: "Read calendar events.",
id: modulePermission,
},
],
});
const report = await diagnoseEkklesia({
moduleDiscovery: {
moduleSdkVersion: "0.1.0",
modulesDirectory,
platformVersion: "0.1.0",
},
runtimeRange: ">=0.0.0",
runtimeVersion: process.versions.node,
});
// Only the module-discovery and permission-composition checks are asserted here — this
// fixture's temporary directory has no `core/drizzle` sibling for migrations discovery to
// find, which is unrelated to what this scenario proves and would otherwise make the check
// fail for a reason that has nothing to do with permission composition.
const moduleCheck = report.checks.find((entry) => entry.id === "modules.discovery");
const permissionsCheck = report.checks.find((entry) => entry.id === "modules.permissions");
expect(moduleCheck).toMatchObject({ status: "pass" });
expect(moduleCheck?.details).toEqual(["calendar 0.1.0"]);
expect(permissionsCheck).toMatchObject({
details: ["2 permissions registered"],
status: "pass",
});
// Doctor and permission composition read only the manifest — the fixture's register/start
// markers must remain entirely unset.
expect(recorder.events).toEqual([]);
});
it("starts a freshly migrated API that discovers the fixture module, imports and starts its runtime exactly once, and mounts its route", async () => {
database = await createDisposableDatabase("authz_module");
await applyCoreMigrations(database.url);
running = await startMigratedApi(database.url, modulesDirectory);
origin = apiOrigin(running);
// Unlike Doctor above, a successful production startup does deliberately import, register,
// and start the module's runtime — exactly once each.
expect(recorder.events).toEqual(["calendar:register", "calendar:start"]);
const ownerSignUp = await signUp(origin, ownerEmail, password);
ownerUserId = ownerSignUp.body.user.id;
const ownerSignIn = await signIn(origin, ownerEmail, password);
expect(ownerSignIn.cookiePair).toBeDefined();
ownerCookie = ownerSignIn.cookiePair as string;
// Existing identity HTTP behavior — sign-up, sign-in, principal resolution — is unaffected by
// module permission composition running before it.
const { body: principal } = await fetchPrincipal(origin, { cookie: ownerCookie });
expect(principal).toEqual({ kind: "authenticated", userId: ownerUserId });
});
it("lets an authorized platform owner create an explicit grant for the installed module's permission, with an atomic audit entry using its declared id", async () => {
const { close, db } = createAuthorizationDrizzleDatabase(database.url);
try {
await bootstrapPlatformOwner(db, { userId: parseEkklesiaUserId(ownerUserId) });
} finally {
await close();
}
const response = await fetch(`${origin}/api/v1/grants`, {
body: JSON.stringify({ permission: modulePermission, subjectUserId: ownerUserId }),
headers: {
"content-type": "application/json",
cookie: ownerCookie,
origin: dashboardOrigin,
},
method: "POST",
});
const body = (await response.json()) as {
grant: { id: string; permission: string; sourceKind: string };
};
expect(response.status).toBe(201);
expect(body.grant.permission).toBe(modulePermission);
expect(body.grant.sourceKind).toBe("explicit");
createdGrantId = body.grant.id;
const admin = await openAdministrativeClient(database.url);
try {
const rows = await safeQuery<{ action: string; permission_id: string }>(
admin,
`select action, permission_id from "ekklesia_core_authorization_audit" where grant_id = $1`,
[createdGrantId],
);
expect(rows).toEqual([{ action: "grant.create", permission_id: modulePermission }]);
} finally {
await safeEnd(admin);
}
});
it("denies the module's own mounted route to an anonymous caller, then serves it to the newly granted owner with the decoded path parameter and authenticated principal", async () => {
const anonymousResponse = await fetch(`${origin}/api/v1/modules/calendar/events/evt-42`);
expect(anonymousResponse.status).toBe(403);
expect(await anonymousResponse.json()).toEqual({ code: "AUTHORIZATION_DENIED" });
const grantedResponse = await fetch(`${origin}/api/v1/modules/calendar/events/evt-42`, {
headers: { cookie: ownerCookie },
});
expect(grantedResponse.status).toBe(200);
const body = (await grantedResponse.json()) as {
eventId: string;
principal: { kind: string; userId: string };
};
expect(body.eventId).toBe("evt-42");
expect(body.principal).toEqual({ kind: "authenticated", userId: ownerUserId });
});
it("requires a trusted Origin for the module's own state-changing route, exactly like the platform's own mutation routes", async () => {
const noOriginResponse = await fetch(`${origin}/api/v1/modules/calendar/events`, {
body: JSON.stringify({}),
headers: { "content-type": "application/json", cookie: ownerCookie },
method: "POST",
});
expect(noOriginResponse.status).toBe(403);
expect(await noOriginResponse.json()).toEqual({ code: "UNTRUSTED_ORIGIN" });
const untrustedResponse = await fetch(`${origin}/api/v1/modules/calendar/events`, {
body: JSON.stringify({}),
headers: {
"content-type": "application/json",
cookie: ownerCookie,
origin: untrustedOrigin,
},
method: "POST",
});
expect(untrustedResponse.status).toBe(403);
const trustedResponse = await fetch(`${origin}/api/v1/modules/calendar/events`, {
body: JSON.stringify({}),
headers: {
"content-type": "application/json",
cookie: ownerCookie,
origin: dashboardOrigin,
},
method: "POST",
});
expect(trustedResponse.status).toBe(201);
expect(await trustedResponse.json()).toMatchObject({ created: true });
});
it("rejects a well-formed permission absent from the composed registry, writing no grant or audit row", async () => {
const admin = await openAdministrativeClient(database.url);
try {
const before = await safeQuery<{ count: string }>(
admin,
`select count(*)::text as count from "ekklesia_core_grant" where permission_id = $1`,
[absentPermission],
);
expect(before[0]?.count).toBe("0");
const response = await fetch(`${origin}/api/v1/grants`, {
body: JSON.stringify({ permission: absentPermission, subjectUserId: ownerUserId }),
headers: {
"content-type": "application/json",
cookie: ownerCookie,
origin: dashboardOrigin,
},
method: "POST",
});
expect(response.status).toBe(403);
expect(await response.json()).toEqual({ code: "AUTHORIZATION_DENIED" });
const after = await safeQuery<{ count: string }>(
admin,
`select count(*)::text as count from "ekklesia_core_grant" where permission_id = $1`,
[absentPermission],
);
const audit = await safeQuery<{ count: string }>(
admin,
`select count(*)::text as count from "ekklesia_core_authorization_audit" where permission_id = $1`,
[absentPermission],
);
expect(after[0]?.count).toBe("0");
expect(audit[0]?.count).toBe("0");
} finally {
await safeEnd(admin);
}
});
it("shuts down cleanly, then restarting with the module absent rejects new grants for its former permission, returns NOT_FOUND for its former route, while the historical grant remains listable", async () => {
await running.shutdown();
const remainingConnections = await activeConnectionCount(database.name);
expect(remainingConnections).toBe(0);
// Exactly one stop, matching the exactly-one register/start already asserted above.
expect(recorder.events.filter((event) => event === "calendar:stop")).toHaveLength(1);
const emptyModulesDirectory = await createFixtureModulesDirectory();
running = await startMigratedApi(database.url, emptyModulesDirectory);
origin = apiOrigin(running);
// The module's own route is unmounted along with the module itself.
const removedRouteResponse = await fetch(`${origin}/api/v1/modules/calendar/events/evt-42`, {
headers: { cookie: ownerCookie },
});
expect(removedRouteResponse.status).toBe(404);
expect(await removedRouteResponse.json()).toEqual({ code: "NOT_FOUND" });
// The owner's existing session survives the restart (sessions live in the database, not the
// process), so the same cookie proves ordinary identity behavior is unaffected here too.
const listResponse = await fetch(`${origin}/api/v1/grants?userId=${ownerUserId}`, {
headers: { cookie: ownerCookie },
});
const listBody = (await listResponse.json()) as {
grants: Array<{ id: string; permission: string }>;
};
expect(listResponse.status).toBe(200);
expect(listBody.grants.some((grant) => grant.id === createdGrantId)).toBe(true);
const response = await fetch(`${origin}/api/v1/grants`, {
body: JSON.stringify({ permission: modulePermission, subjectUserId: ownerUserId }),
headers: {
"content-type": "application/json",
cookie: ownerCookie,
origin: dashboardOrigin,
},
method: "POST",
});
expect(response.status).toBe(403);
expect(await response.json()).toEqual({ code: "AUTHORIZATION_DENIED" });
const admin = await openAdministrativeClient(database.url);
try {
const rows = await safeQuery<{ id: string }>(
admin,
`select id from "ekklesia_core_grant" where id = $1 and revoked_at is null`,
[createdGrantId],
);
expect(rows).toHaveLength(1);
} finally {
await safeEnd(admin);
}
await running.shutdown();
await dropDatabase(database.name);
});
});
describe.sequential("live API — public signup is disabled by default", () => {
it("rejects sign-up with a stable sanitized 400 when publicSignupEnabled is not set, creating no user row", async () => {
const database = await createDisposableDatabase("signup_disabled");
await applyCoreMigrations(database.url);
const running = await startConfiguredApi(database.url, { publicSignupEnabled: false });
try {
const origin = apiOrigin(running);
const response = await fetch(`${origin}/api/auth/sign-up/email`, {
body: JSON.stringify({
email: "[email protected]",
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 = "[email protected]";
// 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("[email protected]");
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 = "[email protected]";
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: "[email protected]", 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", () => {
it("closes a connection whose headers never complete within the configured headersTimeout, never invoking a handler", async () => {
const database = await createDisposableDatabase("node_timeouts");
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],
},
port: 0,
},
{ environment: "production" },
);
// Deliberately tiny, test-only timeouts — production composition (server.ts's `main`) never
// sets this override — so this test observes the real transport-event close deterministically,
// in well under a second, rather than waiting out the production values.
const running = await startApi(config, {
nodeServerTimeoutOverride: {
connectionsCheckingIntervalMs: 100,
headersTimeoutMs: 200,
keepAliveTimeoutMs: 200,
requestTimeoutMs: 300,
},
});
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<void>((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 assertion below.
socket.on("data", (chunk) => receivedChunks.push(chunk as Buffer));
const closed = new Promise<void>((resolve) => socket.once("close", resolve));
// 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.write("GET /api/v1/principal HTTP/1.1\r\nHost: 127.0.0.1\r\n");
await closed;
expect(socket.destroyed).toBe(true);
const received = Buffer.concat(receivedChunks).toString("utf8");
expect(received).toContain("408");
} finally {
await running.shutdown().catch(() => undefined);
await dropDatabase(database.name);
}
});
it("closes a connection whose body never completes within the configured requestTimeout, never invoking a handler or writing a second response", async () => {
const database = await createDisposableDatabase("node_timeouts_body");
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,
},
{ environment: "production" },
);
const running = await startApi(config, {
nodeServerTimeoutOverride: {
connectionsCheckingIntervalMs: 100,
headersTimeoutMs: 300,
keepAliveTimeoutMs: 200,
requestTimeoutMs: 400,
},
});
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<void>((resolve, reject) => {
socket.once("connect", () => resolve());
socket.once("error", reject);
});
const receivedChunks: Buffer[] = [];
socket.on("data", (chunk) => receivedChunks.push(chunk as Buffer));
const closed = new Promise<void>((resolve) => socket.once("close", resolve));
// Complete, well-formed headers declaring a body that never actually arrives.
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":',
);
await closed;
const received = Buffer.concat(receivedChunks).toString("utf8");
// Node's own documented requestTimeout behavior: a 408, never a real sign-in attempt (which
// would need the complete body to ever reach Better Auth), and never a second response.
expect(received).toContain("408");
expect(received).not.toContain("Set-Cookie");
expect(received.match(/^HTTP\/1\.1 \d{3}/gmu)?.length ?? 0).toBeLessThanOrEqual(1);
} finally {
await running.shutdown().catch(() => undefined);
await dropDatabase(database.name);
}
});
});