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, // Unset: the Dashboard is served from the identity base URL, which is the ordinary // same-origin production shape (ADR 0038). dashboardOrigin: 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("requires the base URL to be exactly one canonical origin, rejecting every non-origin component", () => { for (const baseUrl of [ "https://id.example.test/", // a bare trailing slash is a path; `URL.origin` is slashless "https://id.example.test/auth", // Better Auth is never mounted beneath a configurable path "https://id.example.test?a=1", "https://id.example.test#fragment", "https://user:pw@id.example.test", "https://:pw@id.example.test", " https://id.example.test ", "https://ID.EXAMPLE.TEST", // non-canonical hostname case "https://id.example.test:443", // redundant default port "http://id.example.test:80", "ftp://id.example.test", "id.example.test", ]) { let captured: unknown; try { createIdentityConfig({ ...validInput(), baseUrl }, { environment: "production" }); } catch (error) { captured = error; } expect(captured, `expected ${JSON.stringify(baseUrl)} to be rejected`).toMatchObject({ code: "BASE_URL_INVALID", }); } }); it("stores the base URL and trusted origins as one identical canonical origin representation", () => { const config = createIdentityConfig( { ...validInput(), baseUrl: "https://id.example.test", trustedOrigins: ["https://id.example.test", "https://dashboard.example.test:8443"], }, { environment: "production" }, ); expect(config.baseUrl).toBe("https://id.example.test"); expect(config.trustedOrigins).toEqual([ "https://id.example.test", "https://dashboard.example.test:8443", ]); }); it("rejects every loopback and unspecified-host spelling in production, for the base URL and trusted origins alike", () => { // Canonical spellings the WHATWG parser preserves — these must be rejected as production // policy failures, by the identical classifier on both fields. const canonicalLoopback = [ "https://127.0.0.1", "https://127.0.0.2", // the full 127.0.0.0/8 block, not just .1 "https://127.255.255.254", "https://[::1]", // IPv6 loopback, compressed "https://[::ffff:7f00:1]", // IPv4-mapped loopback, hexadecimal notation "https://localhost", "https://app.localhost", // RFC 6761 reserved namespace "https://0.0.0.0", "https://[::]", // IPv6 unspecified "https://[::ffff:0:0]", // IPv4-mapped unspecified ]; for (const value of canonicalLoopback) { let baseCaptured: unknown; try { createIdentityConfig({ ...validInput(), baseUrl: value }, { environment: "production" }); } catch (error) { baseCaptured = error; } expect(baseCaptured, `expected base URL ${value} to be rejected`).toMatchObject({ code: "BASE_URL_LOOPBACK_IN_PRODUCTION", }); let originCaptured: unknown; try { createIdentityConfig( { ...validInput(), trustedOrigins: [value] }, { environment: "production" }, ); } catch (error) { originCaptured = error; } expect(originCaptured, `expected trusted origin ${value} to be rejected`).toMatchObject({ code: "TRUSTED_ORIGIN_LOOPBACK_IN_PRODUCTION", }); } }); it("rejects non-canonical loopback spellings structurally, never as a misleading production-policy failure", () => { // WHATWG canonicalization folds some of these into `127.0.0.1`; others simply are not the // canonical origin text. Either way the honest answer is "this is not a canonical origin". for (const baseUrl of [ "https://127.1", "https://2130706433", "https://0x7f000001", "https://[0:0:0:0:0:0:0:1]", // expanded IPv6 loopback "https://[::ffff:127.0.0.1]", // mixed IPv4-mapped notation "https://LOCALHOST", ]) { let captured: unknown; try { createIdentityConfig({ ...validInput(), baseUrl }, { environment: "production" }); } catch (error) { captured = error; } expect(captured, `expected ${baseUrl} to be structurally rejected`).toMatchObject({ code: "BASE_URL_INVALID", }); } }); it("still accepts canonical loopback development origins", () => { const config = createIdentityConfig( { ...validInput(), baseUrl: "http://localhost:3000", trustedOrigins: ["http://localhost:3000", "http://127.0.0.1:5173"], }, { environment: "development" }, ); expect(config.baseUrl).toBe("http://localhost:3000"); expect(config.trustedOrigins).toEqual(["http://localhost:3000", "http://127.0.0.1:5173"]); }); 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); } }); }); describe("Core identity Dashboard origin", () => { /** * Where an emailed verification or reset link sends a browser (ADR 0038). * * The rules exist because a link in an email is the one artifact nobody inspects before clicking: * it must land on the deployment's own Dashboard, over https in production, at an origin Better * Auth will actually accept as a redirect target — and a misconfiguration must fail at startup * rather than produce a link every recipient is refused at. */ it("defaults to the identity base URL, the ordinary same-origin production shape", () => { expect(createIdentityConfig(validInput()).dashboardOrigin).toBe(validBaseUrl); expect(createIdentityConfig({ ...validInput(), dashboardOrigin: "" }).dashboardOrigin).toBe( validBaseUrl, ); }); it("accepts a separate Dashboard origin that is also a trusted origin", () => { const config = createIdentityConfig({ ...validInput(), dashboardOrigin: "https://dashboard.example.test", }); expect(config.dashboardOrigin).toBe("https://dashboard.example.test"); // Separate from the API origin, deliberately: the two are different concepts and a deployment // may split them. expect(config.dashboardOrigin).not.toBe(config.baseUrl); }); it("refuses an origin Better Auth would not accept as a redirect target", () => { // The failure this prevents is silent and total: Better Auth validates the emailed callback // against its trusted origins, so an untrusted Dashboard origin breaks every link it sends. try { createIdentityConfig({ ...validInput(), dashboardOrigin: "https://elsewhere.example.test" }); throw new Error("expected the untrusted Dashboard origin to be refused"); } catch (error) { expect(error).toMatchObject({ code: "DASHBOARD_ORIGIN_UNTRUSTED" }); } }); it("refuses anything that is not a canonical origin", () => { for (const value of [ "dashboard.example.test", "https://dashboard.example.test/", "https://dashboard.example.test/dashboard", "https://user:pass@dashboard.example.test", "https://dashboard.example.test?a=b", "javascript:alert(1)", "//dashboard.example.test", " https://dashboard.example.test", 42, ]) { try { createIdentityConfig({ ...validInput(), dashboardOrigin: value }); throw new Error(`expected ${String(value)} to be refused`); } catch (error) { expect(error).toMatchObject({ code: "DASHBOARD_ORIGIN_INVALID" }); } } }); it("requires https and a non-loopback host in production, and relaxes both in development", () => { try { createIdentityConfig( // Structural and policy validity are checked before membership, so a plaintext origin is // reported as plaintext rather than as "untrusted" — which would send an operator looking // in the wrong place. { ...validInput(), dashboardOrigin: "http://dashboard.example.test" }, { environment: "production" }, ); throw new Error("expected plaintext to be refused in production"); } catch (error) { expect(error).toMatchObject({ code: "DASHBOARD_ORIGIN_INSECURE_IN_PRODUCTION" }); } try { createIdentityConfig( { ...validInput(), dashboardOrigin: "https://localhost" }, { environment: "production" }, ); throw new Error("expected loopback to be refused in production"); } catch (error) { expect(error).toMatchObject({ code: "DASHBOARD_ORIGIN_LOOPBACK_IN_PRODUCTION" }); } const development = createIdentityConfig( { ...developmentInput(), dashboardOrigin: "http://localhost:5173", trustedOrigins: ["http://localhost:3000", "http://localhost:5173"], }, { environment: "development" }, ); expect(development.dashboardOrigin).toBe("http://localhost:5173"); }); it("never carries the offending origin in the thrown error", () => { try { createIdentityConfig({ ...validInput(), dashboardOrigin: "https://evil.example.test" }); throw new Error("expected a refusal"); } catch (error) { expect(JSON.stringify({ message: (error as Error).message })).not.toContain("evil"); } }); });