Files
Ekklesia/apps/api/tests/config.test.ts
T
ekklesiaandClaude Opus 5 1161f5a076 fix: fail closed on production signup
Production configuration accepted publicSignupEnabled: true and started a
working Identity/API runtime, even though Ekklesia has no verified email,
invitation requirement, CAPTCHA, or durable shared/edge limiter — Doctor
merely reported "Public signup is explicitly ENABLED" as a pass.

validatePublicSignupEnabled now takes the same IdentityEnvironment the rest
of this config validates against and throws the stable
PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION whenever the parsed value is true in
production. Parsing stays separate from the policy, so a malformed value
still reports PUBLIC_SIGNUP_ENABLED_INVALID everywhere. Because the
executable entry point validates configuration before startApi, such a
deployment fails before any pool, Better Auth instance, module runtime, or
listener exists, and Doctor reports NOT READY via its existing
IdentityConfigError path. Development and test composition still enable it
explicitly through the identical typed path.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-26 15:58:08 -05:00

206 lines
6.4 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" },
databaseUrl: validInput().databaseUrl,
host: "127.0.0.1",
identity: {
baseUrl: "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"],
});
});
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");
});
});