From 247ef141ac916d1264b2fb4e8c20ae81aa45aa86 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 2 Nov 2026 03:55:07 +0000 Subject: [PATCH] fix: revoke cached health tracker on session loss --- apps/web/public/health/sw.js | 12 +++++++++++- apps/web/src/app.js | 10 +++++++++- test/app.test.js | 8 +++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/apps/web/public/health/sw.js b/apps/web/public/health/sw.js index c411905..95b8cbf 100644 --- a/apps/web/public/health/sw.js +++ b/apps/web/public/health/sw.js @@ -7,7 +7,17 @@ self.addEventListener('install', (event) => { self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all(keys.filter((key) => key.startsWith('tleehealth-health-') && key !== CACHE).map((key) => caches.delete(key)))) - .then(() => self.clients.claim()), + .then(() => self.clients.claim()) + .then(() => { + setTimeout(() => { + self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => { + for (const client of clients) { + const path = new URL(client.url).pathname; + if (path === '/health' || path.startsWith('/health/')) client.navigate('/health/').catch(() => {}); + } + }); + }, 0); + }), ); }); diff --git a/apps/web/src/app.js b/apps/web/src/app.js index 6a36f29..84f4e11 100644 --- a/apps/web/src/app.js +++ b/apps/web/src/app.js @@ -65,7 +65,15 @@ app.get('/health/', async (c) => { app.get('/health/health.css', healthAsset('health.css', 'text/css; charset=utf-8')); app.get('/health/health.js', healthAsset('health.js', 'text/javascript; charset=utf-8')); app.get('/health/manifest.webmanifest', healthAsset('manifest.webmanifest', 'application/manifest+json')); -app.get('/health/sw.js', healthAsset('sw.js', 'text/javascript; charset=utf-8', { 'service-worker-allowed': '/health/' })); +// Keep the update script public so a logged-out installed copy can replace an +// older cache-first worker and clear its cached tracker shell. +app.get('/health/sw.js', (c) => + c.body(pub('health/sw.js'), 200, { + 'content-type': 'text/javascript; charset=utf-8', + 'cache-control': 'no-cache', + 'service-worker-allowed': '/health/', + }), +); // The app: one shell for the team, patients and sign-in. It talks only to /api/v1. // The shared profile fields (@profullstack/profile) ride in the shell as a diff --git a/test/app.test.js b/test/app.test.js index 9d527a4..1e0b22c 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -58,9 +58,15 @@ describe('web', () => { expect(page.status).toBe(302); expect(page.headers.get('location')).toBe('/signin?next=%2Fhealth%2F'); } - for (const path of ['/health/health.css', '/health/health.js', '/health/manifest.webmanifest', '/health/sw.js']) { + for (const path of ['/health/health.css', '/health/health.js', '/health/manifest.webmanifest']) { expect((await get(path)).status).toBe(401); } + const worker = await get('/health/sw.js'); + expect(worker.status).toBe(200); + expect(worker.headers.get('service-worker-allowed')).toBe('/health/'); + const workerSource = await worker.text(); + expect(workerSource).toContain('tleehealth-health-v4'); + expect(workerSource).toContain("client.navigate('/health/')"); }); test('patient portal links to the free local health tracker', async () => { const source = await (await get('/assets/app.js')).text();