fix(pwa): keep signed-in onboarding pages out of the page cache

The proxy treats /onboarding/* as public, so those pages are not sent
no-store, and the worker stored the documents a signed-in account saw
during setup, against its own rule that only public pages land in the
page cache. The worker now skips them by path, like the share view.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 20:40:32 +01:00
1 parent 61f9447887
commit 09f3280c1c
2 files changed
+28 -19

No files matched your search

+12 -19
View File
@@ -195,25 +195,13 @@ async function cacheFirst(request, cacheName) {
}
/**
* Privacy gate for the navigation page cache. Most app routes are
* client-fetch-only shells with no server-rendered PII (health JSON loads
* over `/api/*`, which is network-only here), so caching their HTML is
* safe. Two carve-outs:
*
* 1. A `Cache-Control: no-store` response opts itself out — the
* principled, future-proof rule for any current/future server RSC
* that renders user data and sets the header.
* 2. `/c/*` (the clinician-share view) renders health values + wellness
* scores straight into the HTML and emits `no-store`; the explicit
* path skip is belt-and-braces so a revoked share can never linger in
* CacheStorage and render back offline.
*
* In practice rule 1 covers every signed-in page: `src/proxy.ts` pins
* `private, no-store` on all of them, because several server-prefetch one
* account's record into the HTML. Only the public pages (sign-in, legal)
* land in this cache, so a navigation to an app page without a connection
* gets the offline page below, never somebody's cached record. A page that
* is already open keeps what it has in memory.
* Which navigation responses may land in the page cache. Signed-in pages
* never do: the proxy sends them `private, no-store` because some carry one
* account's record in the HTML. Two paths are skipped by name as well: the
* clinician share view `/c/*` (no-store too; a revoked share must never
* render back offline) and `/onboarding/*`. What is left are the public
* pages (sign-in, legal), so a navigation to an app page without a
* connection gets the offline page, never somebody's cached record.
*/
function isCacheableNavigation(request, response) {
const cacheControl = response.headers.get("Cache-Control") || "";
@@ -221,6 +209,11 @@ function isCacheableNavigation(request, response) {
try {
const { pathname } = new URL(request.url);
if (pathname === "/c" || pathname.startsWith("/c/")) return false;
// Public to the proxy, so not no-store, but rendered for a signed-in
// account during its setup.
if (pathname === "/onboarding" || pathname.startsWith("/onboarding/")) {
return false;
}
} catch {
// Unparseable URL — fall through; the no-store check already ran.
}
+16
View File
@@ -622,6 +622,22 @@ describe("sw.js — networkFirst privacy gate", () => {
expect(await pages.match(`${ORIGIN}/c/hls_abc123`)).toBeUndefined();
});
it.each(["/onboarding", "/onboarding/who"])(
"does not cache the signed-in onboarding page %s",
async (path) => {
const harness = bootServiceWorker();
harness.context.fetch = async () => healthlogPage("onboarding step");
const res = await dispatchNavigationFetch(harness, path);
expect(await res.clone().text()).toBe("onboarding step");
await Promise.all(harness.lifetimePromises);
expect(
await harness.cacheStorage.match(`${ORIGIN}${path}`),
).toBeUndefined();
},
);
it("still caches an ordinary navigation response", async () => {
const harness = bootServiceWorker();
harness.context.fetch = async () => healthlogPage("app shell");