fix: give Doctor the host configuration the runtime receives
CI / verify (pull_request) Successful in 42s
CI / infrastructure-smoke (pull_request) Successful in 14s
CI / migration-smoke (pull_request) Successful in 1m24s

`pnpm doctor` and `startApi` each read their own subset of the same
environment variables, and the subsets had drifted. Doctor never read
`S3_*` or module secrets at all, so a correctly configured installation
was told its object storage was "not configured" and every module
requiring a bucket or a secret was reported blocked -- while the runtime,
from the identical environment, bound those buckets and ran the modules.

Both executables now map the environment through one
`loadHostConfiguration(env)` in Core, which takes the environment as an
argument and never reads `process.env` itself. Validation stays with the
existing per-domain validators, so each caller keeps its own failure
behaviour: startup refuses a rejected value, Doctor reports it as a
sanitized code.

Doctor's storage option becomes raw input like its mail option, so a
half-configured provider is reported as a blocking configuration failure
instead of throwing before a report exists. The outbound-email check --
computed, counted toward the result, but absent from every rendered
section -- is now printed, and the rendered set is asserted against a
real report so a future check cannot go missing the same way.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-08-24 19:15:49 -05:00
co-authored by Claude Opus 5
parent a835215eb8
commit 2b4f0afb72
23 changed files with 1411 additions and 209 deletions
@@ -0,0 +1,169 @@
import { loadHostConfiguration } from "@ekklesia/core/host";
import { createMailProviderConfig } from "@ekklesia/core/mail";
import { createStorageProviderConfig } from "@ekklesia/core/storage";
import { describe, expect, it } from "vitest";
import { createApiRuntimeConfig } from "../src/config.js";
import { readConfigFromEnvironment } from "../src/server.js";
/**
* `pnpm doctor` and `startApi` must reach the same conclusions from the same environment. They
* previously could not: each executable read its own subset of the variables, and Doctor's subset
* omitted `S3_*` and the module secrets entirely.
*
* These tests assert the property directly — the API's startup inputs and the inputs Doctor
* validates come from one `loadHostConfiguration` call and produce identical validated
* configuration, including every default.
*/
const secretKey = "sk-example-secret-material";
const smtpPassword = "sup3r-secret-relay-password";
function environment(
overrides: Readonly<Record<string, string | undefined>> = {},
): Readonly<Record<string, string | undefined>> {
return {
DATABASE_URL: "postgres://[email protected]:5432/ekklesia",
EKKLESIA_IDENTITY_BASE_URL: "https://ekklesia.example.org",
EKKLESIA_IDENTITY_SECRET: "b7QwZ2rTn4Xs9LmVpKdF6yHc3JgEuNaW",
EKKLESIA_IDENTITY_TRUSTED_ORIGINS: "https://ekklesia.example.org",
NODE_ENV: "production",
...overrides,
};
}
const configuredInfrastructure = Object.freeze({
EKKLESIA_SMTP_FROM_ADDRESS: "[email protected]",
EKKLESIA_SMTP_HOST: "smtp.example.org",
EKKLESIA_SMTP_PASSWORD: smtpPassword,
EKKLESIA_SMTP_USERNAME: "[email protected]",
S3_ACCESS_KEY: "AKIAEXAMPLEACCESSID",
S3_ENDPOINT: "https://storage.example.org",
S3_SECRET_KEY: secretKey,
});
/** Exactly what Doctor validates, from exactly the same load the API performs. */
function doctorView(env: Readonly<Record<string, string | undefined>>) {
const host = loadHostConfiguration(env);
return {
mail: createMailProviderConfig(host.mail, { environment: host.environment }),
storage: createStorageProviderConfig(host.storage, { environment: host.environment }),
};
}
function apiView(env: Readonly<Record<string, string | undefined>>) {
const { environment: resolved, input } = readConfigFromEnvironment(env);
return createApiRuntimeConfig(input, { environment: resolved });
}
describe("host configuration parity between startApi and Doctor", () => {
it("derives identical object-storage configuration, defaults included", () => {
const env = environment(configuredInfrastructure);
expect(apiView(env).storage).toEqual(doctorView(env).storage);
// Named explicitly rather than only compared, so a change to either default fails here loudly.
expect(apiView(env).storage).toEqual({
accessKey: "AKIAEXAMPLEACCESSID",
bucketPrefix: "ekklesia",
deliveryEndpoint: "https://storage.example.org",
endpoint: "https://storage.example.org",
forcePathStyle: true,
region: "us-east-1",
secretKey,
});
});
it("derives identical outbound-email configuration, defaults included", () => {
const env = environment(configuredInfrastructure);
expect(apiView(env).mail).toEqual(doctorView(env).mail);
expect(apiView(env).mail).toMatchObject({
host: "smtp.example.org",
port: 587,
security: "starttls",
});
});
it("agrees that an installation with neither service is complete", () => {
const env = environment();
expect(apiView(env).storage).toBeUndefined();
expect(doctorView(env).storage).toBeUndefined();
expect(apiView(env).mail).toBeUndefined();
expect(doctorView(env).mail).toBeUndefined();
});
it("carries every storage field the direct-delivery pass established", () => {
const env = environment({
...configuredInfrastructure,
S3_BUCKET_PREFIX: "rogc",
S3_DELIVERY_ENDPOINT: "https://files.example.org",
S3_FORCE_PATH_STYLE: "false",
S3_REGION: "eu-west-1",
});
expect(apiView(env).storage).toEqual(doctorView(env).storage);
expect(apiView(env).storage).toMatchObject({
bucketPrefix: "rogc",
deliveryEndpoint: "https://files.example.org",
forcePathStyle: false,
region: "eu-west-1",
});
});
it("rejects the same malformed environment with the same sanitized code", () => {
for (const [overrides, code] of [
[{ S3_ENDPOINT: "https://storage.example.org" }, "STORAGE_ACCESS_KEY_MISSING"],
[{ ...configuredInfrastructure, S3_ENDPOINT: "not-a-url" }, "STORAGE_ENDPOINT_INVALID"],
[
{ ...configuredInfrastructure, S3_ENDPOINT: "http://storage.example.org" },
"STORAGE_ENDPOINT_INSECURE_IN_PRODUCTION",
],
[
{ ...configuredInfrastructure, EKKLESIA_SMTP_PASSWORD: undefined },
"MAIL_USERNAME_WITHOUT_PASSWORD",
],
[{ ...configuredInfrastructure, EKKLESIA_SMTP_PORT: "70000" }, "MAIL_PORT_INVALID"],
] as const) {
const env = environment(overrides);
expect(() => apiView(env)).toThrowError(expect.objectContaining({ code }));
expect(() => doctorView(env)).toThrowError(expect.objectContaining({ code }));
}
});
it("resolves module secrets from the same source the runtime is given", () => {
const env = environment({
EKKLESIA_MODULE_SECRET__ANNOUNCEMENTS__STORAGE_CREDENTIAL: "module-secret-material",
});
expect(
readConfigFromEnvironment(env).moduleSecrets.get("announcements", "storageCredential"),
).toBe("module-secret-material");
expect(loadHostConfiguration(env).moduleSecrets.get("announcements", "storageCredential")).toBe(
"module-secret-material",
);
});
it("reads only this listener's own address and port outside the shared loader", () => {
const config = apiView(
environment({ EKKLESIA_API_HOST: "0.0.0.0", EKKLESIA_API_PORT: "8080" }),
);
expect(config.host).toBe("0.0.0.0");
expect(config.port).toBe(8080);
// Defaults, when unset, stay where they always were.
const defaults = apiView(environment());
expect(defaults.host).toBe("127.0.0.1");
expect(defaults.port).toBe(3000);
});
it("interprets a set-but-empty flag identically instead of one side ignoring it", () => {
// Doctor used to drop any falsy identity value before validating, so `startApi` refused to
// start on `EKKLESIA_PUBLIC_SIGNUP_ENABLED=` while Doctor reported the deployment ready.
const env = environment({ EKKLESIA_PUBLIC_SIGNUP_ENABLED: "" });
expect(loadHostConfiguration(env).identity.publicSignupEnabled).toBe("");
expect(() => apiView(env)).toThrowError(
expect.objectContaining({ code: "PUBLIC_SIGNUP_ENABLED_INVALID" }),
);
});
});