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]>
302 lines
10 KiB
TypeScript
302 lines
10 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { createIdentityConfig, IdentityConfigError } from "../src/identity/config.js";
|
|
|
|
const validSecret = "correct-horse-battery-staple-0123456789";
|
|
const validBaseUrl = "https://identity.example.test";
|
|
const validTrustedOrigins = ["https://identity.example.test", "https://dashboard.example.test"];
|
|
|
|
function validInput() {
|
|
return { baseUrl: validBaseUrl, secret: validSecret, trustedOrigins: validTrustedOrigins };
|
|
}
|
|
|
|
/** The loopback/`http` shape only `{ environment: "development" }` accepts — used by the tests that
|
|
* exercise development-only behavior (such as explicitly enabling public signup) so they never
|
|
* accidentally pass through production validation. */
|
|
function developmentInput() {
|
|
return {
|
|
baseUrl: "http://localhost:3000",
|
|
secret: validSecret,
|
|
trustedOrigins: ["http://localhost:3000"],
|
|
};
|
|
}
|
|
|
|
describe("Core identity configuration", () => {
|
|
it("accepts a valid secret, base URL, and trusted origins", () => {
|
|
const config = createIdentityConfig(validInput(), { environment: "production" });
|
|
|
|
expect(config).toEqual({
|
|
baseUrl: validBaseUrl,
|
|
publicSignupEnabled: false,
|
|
secret: validSecret,
|
|
trustedOrigins: validTrustedOrigins,
|
|
});
|
|
expect(Object.isFrozen(config)).toBe(true);
|
|
expect(Object.isFrozen(config.trustedOrigins)).toBe(true);
|
|
});
|
|
|
|
it("rejects a missing secret", () => {
|
|
expect(() =>
|
|
createIdentityConfig({ ...validInput(), secret: undefined }, { environment: "production" }),
|
|
).toThrow(IdentityConfigError);
|
|
try {
|
|
createIdentityConfig({ ...validInput(), secret: undefined });
|
|
} catch (error) {
|
|
expect(error).toMatchObject({ code: "SECRET_MISSING" });
|
|
}
|
|
});
|
|
|
|
it("rejects a secret shorter than 32 characters", () => {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig({ ...validInput(), secret: "x".repeat(31) });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toMatchObject({ code: "SECRET_TOO_SHORT" });
|
|
});
|
|
|
|
it("accepts a secret of exactly 32 characters", () => {
|
|
const secret = "aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT0uV";
|
|
expect(secret).toHaveLength(32);
|
|
const config = createIdentityConfig({ ...validInput(), secret });
|
|
expect(config.secret).toBe(secret);
|
|
});
|
|
|
|
it("rejects a degenerate single-character-repeated secret", () => {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig({ ...validInput(), secret: "a".repeat(40) });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toMatchObject({ code: "SECRET_LOW_ENTROPY" });
|
|
});
|
|
|
|
it("rejects an invalid base URL", () => {
|
|
for (const baseUrl of [undefined, "", "not-a-url", "ftp://example.test", "example.test"]) {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig({ ...validInput(), baseUrl });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toBeInstanceOf(IdentityConfigError);
|
|
}
|
|
});
|
|
|
|
it("rejects missing trusted origins", () => {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig({ ...validInput(), trustedOrigins: undefined });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toMatchObject({ code: "TRUSTED_ORIGINS_MISSING" });
|
|
});
|
|
|
|
it("rejects an empty trusted origins array", () => {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig({ ...validInput(), trustedOrigins: [] });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toMatchObject({ code: "TRUSTED_ORIGINS_EMPTY" });
|
|
});
|
|
|
|
it("rejects a trusted origin containing a path, query, or fragment", () => {
|
|
for (const origin of [
|
|
"https://example.test/",
|
|
"https://example.test/path",
|
|
"https://example.test#fragment",
|
|
]) {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig({ ...validInput(), trustedOrigins: [origin] });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toMatchObject({ code: "TRUSTED_ORIGIN_INVALID" });
|
|
}
|
|
});
|
|
|
|
it("rejects wildcard trusted origins", () => {
|
|
for (const origin of ["https://*.example.test", "https://app?.example.test"]) {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig({ ...validInput(), trustedOrigins: [origin] });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toMatchObject({ code: "TRUSTED_ORIGIN_WILDCARD_UNSUPPORTED" });
|
|
}
|
|
});
|
|
|
|
it("rejects insecure http origins and loopback hosts in production", () => {
|
|
let httpError: unknown;
|
|
try {
|
|
createIdentityConfig(
|
|
{ ...validInput(), trustedOrigins: ["http://identity.example.test"] },
|
|
{ environment: "production" },
|
|
);
|
|
} catch (error) {
|
|
httpError = error;
|
|
}
|
|
expect(httpError).toMatchObject({ code: "TRUSTED_ORIGIN_INSECURE_IN_PRODUCTION" });
|
|
|
|
let loopbackError: unknown;
|
|
try {
|
|
createIdentityConfig(
|
|
{ ...validInput(), baseUrl: "https://localhost", trustedOrigins: ["https://localhost"] },
|
|
{ environment: "production" },
|
|
);
|
|
} catch (error) {
|
|
loopbackError = error;
|
|
}
|
|
expect(loopbackError).toMatchObject({ code: "BASE_URL_LOOPBACK_IN_PRODUCTION" });
|
|
});
|
|
|
|
it("defaults to production validation when environment is not specified", () => {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig({ ...validInput(), baseUrl: "http://identity.example.test" });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toMatchObject({ code: "BASE_URL_INSECURE_IN_PRODUCTION" });
|
|
});
|
|
|
|
it("explicitly allows local development origins only when environment is development", () => {
|
|
const config = createIdentityConfig(
|
|
{
|
|
baseUrl: "http://localhost:3000",
|
|
secret: validSecret,
|
|
trustedOrigins: ["http://localhost:3000"],
|
|
},
|
|
{ environment: "development" },
|
|
);
|
|
|
|
expect(config.baseUrl).toBe("http://localhost:3000");
|
|
});
|
|
|
|
it("defaults publicSignupEnabled to false when entirely omitted", () => {
|
|
const config = createIdentityConfig(validInput(), { environment: "production" });
|
|
expect(config.publicSignupEnabled).toBe(false);
|
|
});
|
|
|
|
it("accepts an explicit boolean publicSignupEnabled in development", () => {
|
|
expect(
|
|
createIdentityConfig(
|
|
{ ...developmentInput(), publicSignupEnabled: true },
|
|
{ environment: "development" },
|
|
).publicSignupEnabled,
|
|
).toBe(true);
|
|
expect(
|
|
createIdentityConfig(
|
|
{ ...developmentInput(), publicSignupEnabled: false },
|
|
{ environment: "development" },
|
|
).publicSignupEnabled,
|
|
).toBe(false);
|
|
});
|
|
|
|
it('accepts the exact strings "true"/"false" in development (environment variables are always strings)', () => {
|
|
expect(
|
|
createIdentityConfig(
|
|
{ ...developmentInput(), publicSignupEnabled: "true" },
|
|
{ environment: "development" },
|
|
).publicSignupEnabled,
|
|
).toBe(true);
|
|
expect(
|
|
createIdentityConfig(
|
|
{ ...developmentInput(), publicSignupEnabled: "false" },
|
|
{ environment: "development" },
|
|
).publicSignupEnabled,
|
|
).toBe(false);
|
|
});
|
|
|
|
it("accepts an explicitly disabled publicSignupEnabled in production", () => {
|
|
for (const publicSignupEnabled of [false, "false"]) {
|
|
expect(
|
|
createIdentityConfig(
|
|
{ ...validInput(), publicSignupEnabled },
|
|
{ environment: "production" },
|
|
).publicSignupEnabled,
|
|
).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("refuses to enable public signup in production until durable abuse controls exist", () => {
|
|
// Ekklesia has no verified email, invitation requirement, CAPTCHA, or durable shared/edge
|
|
// limiter yet, so a production deployment must not be able to open public registration at all
|
|
// — not even deliberately. This is a fail-closed configuration error, never a warning.
|
|
for (const publicSignupEnabled of [true, "true"]) {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig(
|
|
{ ...validInput(), publicSignupEnabled },
|
|
{ environment: "production" },
|
|
);
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toBeInstanceOf(IdentityConfigError);
|
|
expect(captured).toMatchObject({ code: "PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION" });
|
|
}
|
|
});
|
|
|
|
it("defaults to production validation for public signup when environment is not specified", () => {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig({ ...validInput(), publicSignupEnabled: "true" });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toMatchObject({ code: "PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION" });
|
|
});
|
|
|
|
it("reports a malformed public-signup value as malformed even in production, never as unsupported", () => {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig(
|
|
{ ...validInput(), publicSignupEnabled: "TRUE" },
|
|
{ environment: "production" },
|
|
);
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toMatchObject({ code: "PUBLIC_SIGNUP_ENABLED_INVALID" });
|
|
});
|
|
|
|
it("rejects a misspelled, loosely-truthy, or malformed publicSignupEnabled rather than silently defaulting", () => {
|
|
for (const publicSignupEnabled of ["1", "0", "yes", "no", "TRUE", "True", " true", "", 1, 0]) {
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig({ ...validInput(), publicSignupEnabled });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
expect(captured).toMatchObject({ code: "PUBLIC_SIGNUP_ENABLED_INVALID" });
|
|
}
|
|
});
|
|
|
|
it("never leaks the secret, base URL, or trusted origins through a thrown error", () => {
|
|
const hostileSecret = "s3cr3t-do-not-log-0123456789abcdef";
|
|
let captured: unknown;
|
|
try {
|
|
createIdentityConfig({ baseUrl: "not-a-url", secret: hostileSecret, trustedOrigins: [] });
|
|
} catch (error) {
|
|
captured = error;
|
|
}
|
|
|
|
expect(captured).toBeInstanceOf(IdentityConfigError);
|
|
const publicText = `${String(captured)}\n${
|
|
captured instanceof Error ? (captured.stack ?? "") : ""
|
|
}\n${JSON.stringify(captured)}`;
|
|
expect(publicText).not.toContain(hostileSecret);
|
|
expect(publicText).not.toContain(validBaseUrl);
|
|
for (const origin of validTrustedOrigins) {
|
|
expect(publicText).not.toContain(origin);
|
|
}
|
|
});
|
|
});
|