fix: give Doctor the host configuration the runtime receives
`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:
co-authored by
Claude Opus 5
parent
a835215eb8
commit
2b4f0afb72
@@ -118,15 +118,25 @@ describe("outbound email stays server-only", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("reads SMTP environment only in the executable that owns configuration", async () => {
|
||||
// Reusable Core code never touches `process.env`; the composition root turns environment into
|
||||
// validated configuration and injects it.
|
||||
it("maps SMTP environment in exactly one place, and never inside the mail provider", async () => {
|
||||
// The mail provider takes validated configuration and nothing else — it never reads the
|
||||
// environment, under any name.
|
||||
for (const file of await readTextFiles("core/src/mail")) {
|
||||
expect(file.text, file.path).not.toContain("process.env");
|
||||
expect(file.text, file.path).not.toContain("EKKLESIA_SMTP_");
|
||||
}
|
||||
const server = await readFile(path.join(repositoryRoot, "apps/api/src/server.ts"), "utf8");
|
||||
expect(server).toContain("EKKLESIA_SMTP_HOST");
|
||||
expect(server).toContain("EKKLESIA_SMTP_PASSWORD");
|
||||
// The variable names live in the shared host-configuration loader, which both `startApi` and
|
||||
// `pnpm doctor` call with their own environment. Two executables mapping these names
|
||||
// independently is what let Doctor and the runtime disagree about object storage.
|
||||
const loader = await readFile(
|
||||
path.join(repositoryRoot, "core/src/host/configuration.ts"),
|
||||
"utf8",
|
||||
);
|
||||
expect(loader).toContain("EKKLESIA_SMTP_HOST");
|
||||
expect(loader).toContain("EKKLESIA_SMTP_PASSWORD");
|
||||
// That the loader itself reads no environment is asserted in
|
||||
// `host-configuration-boundaries.test.ts`, which strips comments first — this file's plain
|
||||
// substring match would trip over the loader's own prose about the rule.
|
||||
});
|
||||
|
||||
it("keeps no SMTP password out of the committed environment example", async () => {
|
||||
|
||||
Reference in New Issue
Block a user