Files
Ekklesia/apps/api/tests/config.test.ts
T
ekklesiaandClaude Opus 5 ea28f8aa2e
CI / verify (pull_request) Successful in 42s
CI / infrastructure-smoke (pull_request) Successful in 20s
CI / migration-smoke (pull_request) Successful in 1m42s
feat: verify email addresses and recover forgotten passwords
Ekklesia could authenticate a person and, since Pass 1, send a
transactional email — but it had no way to establish that an address
belonged to whoever claimed it, and no way back in for someone who
forgot their password.

A verified address is now a precondition for holding a session. Better
Auth enforces that after the password check and before any session
exists, so an unverified sign-in returns 403 with no cookie. That is why
no "unverified principal" was needed: a correct password on an
unverified address carries exactly the authority of no password, and
/api/v1/principal is unchanged.

Better Auth keeps every token. Ekklesia adds no token table, no hashing,
and no expiry clock. It supplies a recipient, a subject, and two bodies,
and the mail provider supplies everything about SMTP.

Verification and reset links are rebuilt from a validated Dashboard
origin, taking only the token — never the URL Better Auth prebuilds from
a caller-supplied callbackURL — so no request can steer where a
recipient lands. Following a verification link mints no session, because
mail infrastructure dereferences links. Reset revokes that person's
other sessions and leaves managed devices alone.

Recovery answers identically for an address with an account, without
one, already verified, or undeliverable — including when SMTP is broken,
which is why the delivery adapter never throws.

The initial platform owner is the one exception: created already
verified by an operator command with no mail prerequisite, so a fresh
server can be signed in to before a relay exists. Migration 0007 extends
that to owners of installations that already exist. Public signup stays
disabled; verification is not a licence to self-register.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 07:21:17 -05:00

340 lines
11 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { ApiConfigError, createApiRuntimeConfig } from "../src/config.js";
const validSecret = "correct-horse-battery-staple-0123456789";
function validInput() {
return {
databaseUrl: "postgresql://ekklesia:[email protected]:5432/ekklesia",
host: "127.0.0.1",
identity: {
baseUrl: "https://identity.example.test",
secret: validSecret,
trustedOrigins: ["https://identity.example.test"],
},
port: 3000,
};
}
describe("API runtime configuration", () => {
it("accepts valid API and identity configuration", () => {
const config = createApiRuntimeConfig(validInput(), { environment: "production" });
expect(config).toEqual({
clientAddress: { mode: "direct" },
// Production defaults to certificate-verified TLS with no configuration at all.
databaseTransport: { mode: "verify-full" },
databaseUrl: validInput().databaseUrl,
host: "127.0.0.1",
identity: {
baseUrl: "https://identity.example.test",
// Defaults to the identity base URL: the Dashboard and the API share one origin unless a
// deployment separates them (ADR 0038).
dashboardOrigin: "https://identity.example.test",
publicSignupEnabled: false,
secret: validSecret,
trustedOrigins: ["https://identity.example.test"],
},
port: 3000,
});
expect(Object.isFrozen(config)).toBe(true);
});
it("defaults host to loopback and port to 3000 when omitted", () => {
const { host: _host, port: _port, ...rest } = validInput();
const config = createApiRuntimeConfig(rest, { environment: "production" });
expect(config.host).toBe("127.0.0.1");
expect(config.port).toBe(3000);
});
it("permits an explicit non-loopback bind address", () => {
const config = createApiRuntimeConfig(
{ ...validInput(), host: "0.0.0.0" },
{ environment: "production" },
);
expect(config.host).toBe("0.0.0.0");
});
it("rejects a missing database URL", () => {
let captured: unknown;
try {
createApiRuntimeConfig({ ...validInput(), databaseUrl: undefined });
} catch (error) {
captured = error;
}
expect(captured).toBeInstanceOf(ApiConfigError);
expect(captured).toMatchObject({ code: "DATABASE_URL_MISSING" });
});
it("rejects an invalid database URL", () => {
for (const databaseUrl of ["", "not-a-url", "mysql://example.test/db", "http://example.test"]) {
let captured: unknown;
try {
createApiRuntimeConfig({ ...validInput(), databaseUrl });
} catch (error) {
captured = error;
}
expect(captured).toBeInstanceOf(ApiConfigError);
}
});
it("rejects an empty listen host", () => {
let captured: unknown;
try {
createApiRuntimeConfig({ ...validInput(), host: "" });
} catch (error) {
captured = error;
}
expect(captured).toMatchObject({ code: "HOST_INVALID" });
});
it("rejects an invalid listen port", () => {
for (const port of [-1, 1.5, 70_000, Number.NaN, "not-a-port"]) {
let captured: unknown;
try {
createApiRuntimeConfig({ ...validInput(), port });
} catch (error) {
captured = error;
}
expect(captured).toMatchObject({ code: "PORT_INVALID" });
}
});
it("accepts port 0 as an explicit request for an OS-assigned ephemeral port", () => {
const config = createApiRuntimeConfig({ ...validInput(), port: 0 });
expect(config.port).toBe(0);
});
it("delegates identity validation to Core's createIdentityConfig, rejecting a short secret", () => {
let captured: unknown;
try {
createApiRuntimeConfig({
...validInput(),
identity: { ...validInput().identity, secret: "too-short" },
});
} catch (error) {
captured = error;
}
expect(captured).toMatchObject({ code: "SECRET_TOO_SHORT" });
});
it("explicitly allows local development origins only when environment is development", () => {
const config = createApiRuntimeConfig(
{
databaseUrl: validInput().databaseUrl,
host: "127.0.0.1",
identity: {
baseUrl: "http://localhost:3000",
secret: validSecret,
trustedOrigins: ["http://localhost:3000"],
},
port: 3000,
},
{ environment: "development" },
);
expect(config.identity.baseUrl).toBe("http://localhost:3000");
});
it("defaults public signup to disabled and threads an explicit development enablement through to identity config", () => {
expect(createApiRuntimeConfig(validInput()).identity.publicSignupEnabled).toBe(false);
expect(
createApiRuntimeConfig(
{
...validInput(),
identity: {
baseUrl: "http://localhost:3000",
publicSignupEnabled: true,
secret: validSecret,
trustedOrigins: ["http://localhost:3000"],
},
},
{ environment: "development" },
).identity.publicSignupEnabled,
).toBe(true);
});
it("refuses a production runtime configuration that enables public signup, before any resource is constructed", () => {
let captured: unknown;
try {
createApiRuntimeConfig(
{
...validInput(),
identity: { ...validInput().identity, publicSignupEnabled: true },
},
{ environment: "production" },
);
} catch (error) {
captured = error;
}
expect(captured).toMatchObject({ code: "PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION" });
});
it("defaults client-address configuration to direct mode and threads explicit trusted-proxy configuration through", () => {
expect(createApiRuntimeConfig(validInput()).clientAddress).toEqual({ mode: "direct" });
const config = createApiRuntimeConfig({
...validInput(),
clientAddress: {
header: "x-forwarded-for",
mode: "trusted-proxy",
trustedProxies: ["10.0.0.1"],
},
});
expect(config.clientAddress).toEqual({
header: "x-forwarded-for",
mode: "trusted-proxy",
trustedProxies: ["10.0.0.1"],
});
});
describe("PostgreSQL transport policy", () => {
it("defaults to certificate-verified TLS in production and plaintext in development", () => {
expect(createApiRuntimeConfig(validInput()).databaseTransport).toEqual({
mode: "verify-full",
});
expect(
createApiRuntimeConfig(
{
...validInput(),
identity: {
baseUrl: "http://localhost:3000",
secret: validSecret,
trustedOrigins: ["http://localhost:3000"],
},
},
{ environment: "development" },
).databaseTransport,
).toEqual({ mode: "disable" });
});
it("refuses a production configuration that disables TLS", () => {
let captured: unknown;
try {
createApiRuntimeConfig(
{ ...validInput(), databaseTransport: { mode: "disable" } },
{ environment: "production" },
);
} catch (error) {
captured = error;
}
expect(captured).toMatchObject({ code: "DATABASE_TLS_REQUIRED_IN_PRODUCTION" });
});
it("rejects a malformed or loosely spelled TLS mode in every environment, without coercion", () => {
for (const mode of [
"VERIFY-FULL",
" verify-full ",
"require",
"verify-ca",
"prefer",
"no-verify",
"true",
true,
1,
"",
]) {
for (const environment of ["production", "development"] as const) {
let captured: unknown;
try {
createApiRuntimeConfig(
{
...validInput(),
databaseTransport: { mode },
...(environment === "development"
? {
identity: {
baseUrl: "http://localhost:3000",
secret: validSecret,
trustedOrigins: ["http://localhost:3000"],
},
}
: {}),
},
{ environment },
);
} catch (error) {
captured = error;
}
expect(captured, `expected mode ${JSON.stringify(mode)} to be rejected`).toMatchObject({
code: "DATABASE_TLS_MODE_INVALID",
});
}
}
});
it("rejects a database URL carrying its own TLS parameters, which the pinned driver would let override the policy", () => {
for (const parameter of [
"sslmode=disable",
"sslmode=require",
"sslmode=verify-full",
"ssl=true",
"sslrootcert=/tmp/ca.crt",
"sslcert=/tmp/c.crt",
"sslkey=/tmp/c.key",
"uselibpqcompat=true",
]) {
let captured: unknown;
try {
createApiRuntimeConfig(
{
...validInput(),
databaseUrl: `postgresql://ekklesia:[email protected]:5432/ekklesia?${parameter}`,
},
{ environment: "production" },
);
} catch (error) {
captured = error;
}
expect(captured, `expected ${parameter} to be rejected`).toMatchObject({
code: "DATABASE_TLS_CONNECTION_STRING_CONFLICT",
});
}
});
it("never leaks the database URL, credentials, host, or CA material through a transport error", () => {
let captured: unknown;
try {
createApiRuntimeConfig(
{
...validInput(),
databaseTransport: { certificateAuthority: "-----BEGIN CERTIFICATE-----SECRET" },
databaseUrl:
"postgresql://admin:[email protected]:5432/ekklesia?sslmode=disable",
},
{ environment: "production" },
);
} catch (error) {
captured = error;
}
const publicText = `${String(captured)}\n${
captured instanceof Error ? (captured.stack ?? "") : ""
}\n${JSON.stringify(captured)}`;
expect(publicText).not.toContain("hunter2");
expect(publicText).not.toContain("db.internal.example");
expect(publicText).not.toContain("SECRET");
expect(publicText).not.toContain("sslmode");
});
});
it("never leaks the database URL, secret, or trusted origins through a thrown error", () => {
const hostileUrl = "postgresql://admin:[email protected]:5432/ekklesia";
let captured: unknown;
try {
createApiRuntimeConfig({ ...validInput(), databaseUrl: hostileUrl, port: -1 });
} catch (error) {
captured = error;
}
expect(captured).toBeInstanceOf(ApiConfigError);
const publicText = `${String(captured)}\n${
captured instanceof Error ? (captured.stack ?? "") : ""
}\n${JSON.stringify(captured)}`;
expect(publicText).not.toContain(hostileUrl);
expect(publicText).not.toContain("s3cr3t");
expect(publicText).not.toContain("db.internal.example");
});
});