diff --git a/src/__tests__/proxy-well-known-public.test.ts b/src/__tests__/proxy-well-known-public.test.ts new file mode 100644 index 000000000..f9ed9f25f --- /dev/null +++ b/src/__tests__/proxy-well-known-public.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it, vi } from "vitest"; +import { NextRequest } from "next/server"; + +/** + * v1.4.33 — `/.well-known/*` is a credential-free IETF discovery + * namespace (RFC 8615). Apple in particular reads + * `/.well-known/apple-app-site-association` without a session to wire + * Web Credentials (passkey sharing) and Universal Links to the iOS + * bundle. This guard locks the proxy allowlist in place so a future + * refactor of `src/proxy.ts` can't silently re-gate the namespace + * behind a session cookie and break the Apple CDN fetch. + */ + +vi.mock("@/lib/process-type", () => ({ + shouldRunWeb: () => true, +})); + +import { proxy } from "../proxy"; + +function makeRequest( + pathname: string, + cookies: Record = {}, +): NextRequest { + const cookieHeader = Object.entries(cookies) + .map(([k, v]) => `${k}=${v}`) + .join("; "); + return new NextRequest(`http://localhost${pathname}`, { + headers: cookieHeader ? { cookie: cookieHeader } : {}, + }); +} + +describe("proxy.ts /.well-known/* public-allowlist (v1.4.33)", () => { + it("does not 307 unauthenticated requests for the AASA file", () => { + const res = proxy(makeRequest("/.well-known/apple-app-site-association")); + expect(res.status).toBe(200); + expect(res.headers.get("location")).toBeNull(); + }); + + it("does not 307 a pending-onboarding session away from the AASA file", () => { + // Apple's CDN fetch carries no cookies, so this branch is mainly + // a belt-and-braces check; the redirect would only fire for a + // human visitor who happened to have a half-finished onboarding + // session and typed the URL by hand. Either way the asset must + // answer 200 with the JSON body unchanged. + const res = proxy( + makeRequest("/.well-known/apple-app-site-association", { + healthlog_session: "sess-1", + hl_onboarding: "pending", + }), + ); + expect(res.status).toBe(200); + expect(res.headers.get("location")).toBeNull(); + }); + + it("admits any other future /.well-known/* discovery file", () => { + // The allowlist entry is a `/.well-known/` prefix so future + // additions (e.g. `/.well-known/security.txt` or + // `/.well-known/openid-configuration`) don't need a second proxy + // edit. This check fails the moment the prefix is tightened to + // a literal `/.well-known/apple-app-site-association` only. + const res = proxy(makeRequest("/.well-known/security.txt")); + expect(res.status).toBe(200); + expect(res.headers.get("location")).toBeNull(); + }); + + it("still redirects unauthenticated requests for protected pages", () => { + // Negative check — confirms the test fixture would catch a real + // regression. `/insights` is not public; an unauthenticated visit + // must still 307 to /auth/login. + const res = proxy(makeRequest("/insights")); + expect([307, 308]).toContain(res.status); + expect(res.headers.get("location")).toMatch(/\/auth\/login/); + }); +}); diff --git a/src/proxy.ts b/src/proxy.ts index b7dd322f4..c1bdd719a 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -33,6 +33,14 @@ const PUBLIC_PATHS = [ // alongside the project credits. The CC licence requires the // attribution to be reachable without a sign-in. "/about", + // v1.4.33 — `/.well-known/*` covers IETF-registered discovery + // endpoints (RFC 8615). Apple reads + // `/.well-known/apple-app-site-association` without credentials to + // wire Web Credentials (passkey sharing) and Universal Links to the + // iOS bundle, so the path must answer 200 with the bare JSON body + // before any auth gate runs. The trailing slash future-proofs the + // namespace for `/security.txt`, `/openid-configuration`, etc. + "/.well-known/", // `/onboarding` itself + its subroutes are matched exactly via // `isPublicPath()` so we don't admit `/onboarding-export` etc. "/robots.txt",