From e2ea58a4c985b7d9fed12c2aade2b540b9c87094 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Mon, 3 Aug 2026 14:53:16 -0300 Subject: [PATCH] #33: route the DNS verification endpoint to the service that serves it The endpoint added in #32 lives on the protocols service, but the proxy only forwarded admin/dkim and admin/tls there - so admin/dns went to the auth service, which has no such route and answers 401 from its admin guard. The console would have reported "not authorised" for a request that was simply sent to the wrong process. It survived my own check because an unauthenticated probe of a nonexistent admin path returns 401 too: the guard runs before routing, so 401 proves nothing about whether a route exists. Comparing against a path I knew was fake is what exposed it. The list is now named and commented, and a test asserts every protocols-owned area appears in it, because the failure mode reads as a session problem rather than a routing one. Co-Authored-By: Claude Opus 5 --- .../webmail/src/app/api/v1/[...path]/route.ts | 19 +++++++++++---- .../webmail/src/interface-integrity.test.ts | 23 +++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/services/webmail/src/app/api/v1/[...path]/route.ts b/services/webmail/src/app/api/v1/[...path]/route.ts index 92280a4..f56a176 100644 --- a/services/webmail/src/app/api/v1/[...path]/route.ts +++ b/services/webmail/src/app/api/v1/[...path]/route.ts @@ -16,12 +16,23 @@ const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL ?? "http://auth:3001"; // sees one origin and one cookie either way. const PROTOCOLS_SERVICE_URL = process.env.PROTOCOLS_SERVICE_URL ?? "http://protocols:8080"; +/** + * Admin areas the protocols service owns rather than the auth service. + * + * Each is here because that process holds something the auth service does not: + * the DKIM vault, the certificate watcher, and the DNS resolver plus the + * expected-record spec. + * + * Adding a route to protocols without adding it here sends the request to the + * auth service, which answers 401 from its admin guard - indistinguishable from + * a session problem, and the reason to keep this list beside the services it + * names. + */ +const PROTOCOLS_ADMIN_AREAS = new Set(["dkim", "tls", "dns"]); + function upstreamFor(path: string[]): string { - // Mail reads and sends go to protocols, and so does DKIM: the private key - // has to be sealed with that service's vault, which derives its key - // differently from the auth service's. if (path[0] === "mail") return PROTOCOLS_SERVICE_URL; - if (path[0] === "admin" && (path[1] === "dkim" || path[1] === "tls")) return PROTOCOLS_SERVICE_URL; + if (path[0] === "admin" && PROTOCOLS_ADMIN_AREAS.has(path[1])) return PROTOCOLS_SERVICE_URL; return AUTH_SERVICE_URL; } diff --git a/services/webmail/src/interface-integrity.test.ts b/services/webmail/src/interface-integrity.test.ts index 3c3d452..090c9aa 100644 --- a/services/webmail/src/interface-integrity.test.ts +++ b/services/webmail/src/interface-integrity.test.ts @@ -172,3 +172,26 @@ describe("two-factor enrolment", () => { expect(source).toContain("onContinue"); }); }); + +describe("API proxy routing", () => { + // A route added to the protocols service but not to the proxy's list is sent + // to the auth service instead, which answers 401 from its admin guard. That + // is indistinguishable from a session problem, so the failure looks like + // "you are not logged in" rather than "this was routed to the wrong + // service" - which is exactly how it went unnoticed once already. + it("sends every protocols-owned admin area to the protocols service", () => { + const proxy = readFileSync(resolve(process.cwd(), "src/app/api/v1/[...path]/route.ts"), "utf8"); + + // Areas the protocols service actually serves, taken from its router. + for (const area of ["dkim", "tls", "dns"]) { + expect(proxy).toContain(`"${area}"`); + } + expect(proxy).toMatch(/PROTOCOLS_ADMIN_AREAS/); + }); + + it("still sends mail to protocols and everything else to auth", () => { + const proxy = readFileSync(resolve(process.cwd(), "src/app/api/v1/[...path]/route.ts"), "utf8"); + expect(proxy).toMatch(/path\[0\] === "mail".*PROTOCOLS_SERVICE_URL/s); + expect(proxy).toContain("return AUTH_SERVICE_URL"); + }); +});