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]>
This commit is contained in:
ekklesia
2026-07-26 15:58:08 -05:00
co-authored by Claude Opus 5
parent d3c6bd73d3
commit 1161f5a076
10 changed files with 293 additions and 53 deletions
+36 -14
View File
@@ -6,6 +6,7 @@ export type IdentityConfigErrorCode =
| "BASE_URL_LOOPBACK_IN_PRODUCTION"
| "BASE_URL_MISSING"
| "PUBLIC_SIGNUP_ENABLED_INVALID"
| "PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION"
| "SECRET_LOW_ENTROPY"
| "SECRET_MISSING"
| "SECRET_TOO_SHORT"
@@ -32,6 +33,8 @@ const errorMessages: Readonly<Record<IdentityConfigErrorCode, string>> = Object.
BASE_URL_MISSING: "The identity base URL is required.",
PUBLIC_SIGNUP_ENABLED_INVALID:
'Public signup enablement must be an explicit boolean or the exact string "true"/"false".',
PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION:
"Public signup cannot be enabled in production until durable signup-abuse controls exist.",
SECRET_LOW_ENTROPY: "The identity secret does not have sufficient entropy.",
SECRET_MISSING: "The identity secret is required.",
SECRET_TOO_SHORT: `The identity secret must be at least ${MIN_SECRET_LENGTH} characters.`,
@@ -66,8 +69,10 @@ export interface IdentityConfig {
readonly baseUrl: string;
/** Whether `POST /api/auth/sign-up/email` accepts new accounts. Defaults to `false` (disabled) —
* public signup never becomes reachable merely because this input was omitted, misspelled, or
* loosely parsed. Development and test composition may set this explicitly; production
* composition must set it explicitly too, since there is no environment-sensitive default. */
* loosely parsed. Development and test composition may enable it explicitly; production
* currently cannot enable it at all (`PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION`), because the
* durable signup-abuse prerequisites do not exist yet — see `validatePublicSignupEnabled`. In
* production this is therefore always `false`. */
readonly publicSignupEnabled: boolean;
readonly secret: string;
readonly trustedOrigins: readonly string[];
@@ -168,21 +173,38 @@ function validateTrustedOrigin(origin: unknown, environment: IdentityEnvironment
* `"false"` are accepted so an environment variable (always a string) can opt in — anything else
* (a misspelling, `"1"`, `"yes"`, mixed case, an empty string) is rejected outright rather than
* silently coerced to `false` or `true`. There is deliberately no "loosely truthy" branch.
*
* Parsing is deliberately separated from the production policy below: a malformed value is
* reported as malformed in *every* environment (`PUBLIC_SIGNUP_ENABLED_INVALID`), so an operator
* who typo'd the flag is never told the misleading "unsupported in production" story instead.
*
* Enabling public signup is then refused outright in production
* (`PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION`). This is not a preference — the durable
* signup-abuse prerequisites a public registration endpoint needs (verified email or an
* invitation requirement, plus durable shared/edge throttling rather than this release's
* per-process in-memory limiter) do not exist in this repository yet, so there is no safe
* production configuration in which the flag could be honored. Development and test composition
* may still enable it explicitly through this same typed path. When those prerequisites are
* actually implemented and proven, this check is the single place that changes.
*/
function validatePublicSignupEnabled(value: unknown): boolean {
function validatePublicSignupEnabled(value: unknown, environment: IdentityEnvironment): boolean {
let enabled: boolean;
if (value === undefined) {
return false;
enabled = false;
} else if (typeof value === "boolean") {
enabled = value;
} else if (value === "true") {
enabled = true;
} else if (value === "false") {
enabled = false;
} else {
throw new IdentityConfigError("PUBLIC_SIGNUP_ENABLED_INVALID");
}
if (typeof value === "boolean") {
return value;
if (enabled && environment === "production") {
throw new IdentityConfigError("PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION");
}
if (value === "true") {
return true;
}
if (value === "false") {
return false;
}
throw new IdentityConfigError("PUBLIC_SIGNUP_ENABLED_INVALID");
return enabled;
}
function validateTrustedOrigins(
@@ -211,7 +233,7 @@ export function createIdentityConfig(
const secret = validateSecret(input.secret);
const baseUrl = validateBaseUrl(input.baseUrl, environment);
const trustedOrigins = validateTrustedOrigins(input.trustedOrigins, environment);
const publicSignupEnabled = validatePublicSignupEnabled(input.publicSignupEnabled);
const publicSignupEnabled = validatePublicSignupEnabled(input.publicSignupEnabled, environment);
return Object.freeze({ baseUrl, publicSignupEnabled, secret, trustedOrigins });
}
+27 -3
View File
@@ -635,7 +635,27 @@ describe("Ekklesia Doctor diagnostics", () => {
);
});
it("reports public signup as explicitly enabled when configured", async () => {
it("reports public signup as explicitly enabled when configured in development", async () => {
const report = await diagnoseEkklesia(
{
...options(),
identityConfiguration: {
baseUrl: "http://localhost:3000",
environment: "development",
publicSignupEnabled: "true",
secret: "correct-horse-battery-staple-0123456789",
trustedOrigins: ["http://localhost:3000"],
},
},
dependencies(),
);
const identity = check(report, "identity.configuration");
expect(identity.status).toBe("pass");
expect(identity.details.some((detail) => detail.includes("explicitly ENABLED"))).toBe(true);
});
it("blocks readiness — not merely warns — when production configuration enables public signup", async () => {
const report = await diagnoseEkklesia(
{
...options(),
@@ -651,8 +671,12 @@ describe("Ekklesia Doctor diagnostics", () => {
);
const identity = check(report, "identity.configuration");
expect(identity.status).toBe("pass");
expect(identity.details.some((detail) => detail.includes("explicitly ENABLED"))).toBe(true);
expect(identity.status).toBe("failure");
expect(
identity.details.some((detail) => detail.includes("PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION")),
).toBe(true);
expect(report.result).toBe("not-ready");
expect(doctorExitCode(report)).toBe(1);
});
it("fails identity configuration for a misspelled public-signup value, never silently disabling or enabling it", async () => {
+82 -6
View File
@@ -9,6 +9,17 @@ 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" });
@@ -173,24 +184,89 @@ describe("Core identity configuration", () => {
expect(config.publicSignupEnabled).toBe(false);
});
it("accepts an explicit boolean publicSignupEnabled", () => {
it("accepts an explicit boolean publicSignupEnabled in development", () => {
expect(
createIdentityConfig({ ...validInput(), publicSignupEnabled: true }).publicSignupEnabled,
createIdentityConfig(
{ ...developmentInput(), publicSignupEnabled: true },
{ environment: "development" },
).publicSignupEnabled,
).toBe(true);
expect(
createIdentityConfig({ ...validInput(), publicSignupEnabled: false }).publicSignupEnabled,
createIdentityConfig(
{ ...developmentInput(), publicSignupEnabled: false },
{ environment: "development" },
).publicSignupEnabled,
).toBe(false);
});
it('accepts the exact strings "true"/"false" (environment variables are always strings)', () => {
it('accepts the exact strings "true"/"false" in development (environment variables are always strings)', () => {
expect(
createIdentityConfig({ ...validInput(), publicSignupEnabled: "true" }).publicSignupEnabled,
createIdentityConfig(
{ ...developmentInput(), publicSignupEnabled: "true" },
{ environment: "development" },
).publicSignupEnabled,
).toBe(true);
expect(
createIdentityConfig({ ...validInput(), publicSignupEnabled: "false" }).publicSignupEnabled,
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;