import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; /** * Structural guarantees about outbound email that no runtime assertion can make (ADR 0037). * * These read the repository itself, because the properties that matter here are about what code * *exists* and what depends on what — a mail credential that never reaches a browser bundle, and a * platform that offers no route anyone could use as an open relay. */ const repositoryRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); async function readJson(relativePath: string): Promise> { return JSON.parse(await readFile(path.join(repositoryRoot, relativePath), "utf8")) as Record< string, unknown >; } async function readTextFiles(directory: string): Promise> { const { readdir } = await import("node:fs/promises"); const results: Array<{ path: string; text: string }> = []; const walk = async (current: string): Promise => { for (const entry of await readdir(current, { withFileTypes: true })) { const full = path.join(current, entry.name); if (entry.isDirectory()) { if (entry.name === "node_modules" || entry.name === "dist") { continue; } await walk(full); continue; } if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) { results.push({ path: full, text: await readFile(full, "utf8") }); } } }; await walk(path.join(repositoryRoot, directory)); return results; } describe("outbound email stays server-only", () => { it("declares the SMTP dependency in Core alone", async () => { const core = await readJson("core/package.json"); expect(Object.keys((core.dependencies ?? {}) as object)).toContain("nodemailer"); // Every package a browser can end up executing, plus the contracts barrel a surface must not // reach. None may carry a mail transport. for (const manifest of [ "packages/contracts/package.json", "packages/module-sdk/package.json", "packages/module-ui-sdk/package.json", "packages/module-ui-host/package.json", "surfaces/shell/package.json", "apps/api/package.json", ]) { const parsed = await readJson(manifest); const dependencies = { ...((parsed.dependencies ?? {}) as Record), ...((parsed.devDependencies ?? {}) as Record), }; const mailDependencies = Object.keys(dependencies).filter((name) => /nodemailer|smtp|mailgun|sendgrid|postmark|resend/iu.test(name), ); expect(mailDependencies, manifest).toEqual([]); } }); it("never imports the transport outside Core's own mail module", async () => { for (const directory of [ "packages/contracts/src", "packages/module-sdk/src", "packages/module-ui-sdk/src", "packages/module-ui-host/src", "surfaces/shell/src", ]) { for (const file of await readTextFiles(directory)) { expect(file.text, file.path).not.toContain("nodemailer"); expect(file.text, file.path).not.toContain("@ekklesia/core/mail"); } } }); it("gives module code no way to send email at all", async () => { // Deliberate scope: Core sends, modules do not (yet). `ModuleRuntimeServices` is the one place a // module capability would appear, and it is added one service at a time when something real // needs it — nothing does here (ADR 0037). const services = await readFile( path.join(repositoryRoot, "packages/contracts/src/module-database.ts"), "utf8", ); expect(services).not.toMatch(/mail|email/iu); for (const directory of ["packages/module-sdk/src", "packages/module-ui-sdk/src"]) { for (const file of await readTextFiles(directory)) { expect(file.text.toLowerCase(), file.path).not.toContain("sendmail"); } } }); it("exposes no HTTP route that could send mail", async () => { // An authenticated relay endpoint is an open relay with extra steps. Nothing in the transport // layer may reference the mail provider at all. for (const file of await readTextFiles("apps/api/src")) { if (file.path.endsWith("server.ts") || file.path.endsWith("config.ts")) { // The composition root legitimately constructs and closes the provider. continue; } expect(file.text, file.path).not.toContain("@ekklesia/core/mail"); expect(file.text.toLowerCase(), file.path).not.toContain("sendmail"); } for (const file of await readTextFiles("apps/api/src")) { expect(file.text, file.path).not.toMatch(/["'`][^"'`]*send-email[^"'`]*["'`]/u); expect(file.text, file.path).not.toMatch(/["'`][^"'`]*\/api\/v1\/mail[^"'`]*["'`]/u); } }); 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. for (const file of await readTextFiles("core/src/mail")) { expect(file.text, file.path).not.toContain("process.env"); } 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"); }); it("keeps no SMTP password out of the committed environment example", async () => { const example = await readFile(path.join(repositoryRoot, ".env.example"), "utf8"); // Documented, but never with a value: a bootstrap credential in a committed file is a // credential in every clone. expect(example).toContain("EKKLESIA_SMTP_HOST"); expect(example).not.toMatch(/^EKKLESIA_SMTP_PASSWORD=.+$/mu); expect(example).not.toContain("smtp-relay.gmail.com"); }); it("hardcodes no provider hostname in Core", async () => { // Google Workspace is a deployment target, not the architecture. for (const file of await readTextFiles("core/src/mail")) { expect(file.text.toLowerCase(), file.path).not.toContain("gmail"); expect(file.text.toLowerCase(), file.path).not.toContain("smtp-relay"); expect(file.text.toLowerCase(), file.path).not.toContain("resend"); expect(file.text.toLowerCase(), file.path).not.toContain("brevo"); } }); it("never disables TLS certificate verification", async () => { for (const file of await readTextFiles("core/src/mail")) { expect(file.text, file.path).not.toContain("rejectUnauthorized"); expect(file.text, file.path).not.toContain("NODE_TLS_REJECT_UNAUTHORIZED"); } }); });