From 5b3e8aff3b46891ed6f69ca35bbc64936ebd5324 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Sun, 8 Mar 2026 05:06:29 +0100 Subject: [PATCH] Bypass auth/session pages in frontend serviceworker cache --- README.md | 2 +- scripts/patch-frontend-serviceworker-file.mjs | 112 +++++++++++++++--- 2 files changed, 97 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 077f6ec4..93c0e063 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,6 @@ - The frontend sharp runtime patch makes icon generation non-fatal on FreeBSD when `sharp` cannot load, preventing startup crashes in asset controller imports. - The files upload route patch fixes browser multi-upload by posting to `/files/upload` (session-authenticated) instead of direct `/media` calls without bearer token. - The files upload locale patch adds missing `files.upload.dropText`/`files.upload.browse`/`files.upload.submitMultiple` labels in endpoint locale files so UI text does not render raw translation keys. -- The frontend serviceworker patch ensures `@indiekit/frontend/lib/serviceworker.js` exists at runtime to avoid ENOENT in the offline/service worker route. +- The frontend serviceworker patch ensures `@indiekit/frontend/lib/serviceworker.js` exists at runtime, and forces network-only handling for `/auth` and `/session` pages to avoid stale cached login/consent screens. - The conversations guard patch prevents `Cannot read properties of undefined (reading 'find')` when the `conversation_items` collection is temporarily unavailable. - The indieauth dev-mode guard patch prevents accidental production auth bypass by requiring explicit `INDIEKIT_ALLOW_DEV_AUTH=1` to enable dev auto-login. diff --git a/scripts/patch-frontend-serviceworker-file.mjs b/scripts/patch-frontend-serviceworker-file.mjs index f22c28d0..a75d0ee6 100644 --- a/scripts/patch-frontend-serviceworker-file.mjs +++ b/scripts/patch-frontend-serviceworker-file.mjs @@ -23,6 +23,51 @@ self.addEventListener("activate", (event) => { self.addEventListener("fetch", () => {}); `; +const authBypassMarker = "Never cache auth/session pages"; +const oldFetchCacheLine = " const retrieveFromCache = caches.match(request);"; +const newFetchCacheBlock = ` const requestUrl = new URL(request.url); + + // Never cache auth/session pages; always go to network. + if ( + requestUrl.origin === self.location.origin && + /^\\/(auth|session)(?:\\/|$)/.test(requestUrl.pathname) + ) { + event.respondWith(fetch(request)); + return; + } + + const retrieveFromCache = caches.match(request);`; + +const clearAuthSessionEntriesFn = ` +async function clearAuthSessionEntries() { + try { + const pagesCache = await caches.open(pagesCacheName); + const keys = await pagesCache.keys(); + + await Promise.all( + keys + .filter((request) => { + const requestUrl = new URL(request.url); + return ( + requestUrl.origin === self.location.origin && + /^\\/(auth|session)(?:\\/|$)/.test(requestUrl.pathname) + ); + }) + .map((request) => pagesCache.delete(request)), + ); + } catch (error) { + console.error("Error clearing auth/session cache entries", error); + } +} +`; + +const activateOld = ` await clearOldCaches(); + await clients.claim();`; + +const activateNew = ` await clearOldCaches(); + await clearAuthSessionEntries(); + await clients.claim();`; + async function exists(filePath) { try { await access(filePath); @@ -32,26 +77,61 @@ async function exists(filePath) { } } -if (await exists(expected)) { - console.log("[postinstall] frontend serviceworker already present"); - process.exit(0); +function patchServiceworker(content) { + let updated = content; + + if (!updated.includes(authBypassMarker) && updated.includes(oldFetchCacheLine)) { + updated = updated.replace(oldFetchCacheLine, newFetchCacheBlock); + } + + if ( + !updated.includes("async function clearAuthSessionEntries()") && + updated.includes("async function trimCache(cacheName, maxItems)") + ) { + updated = updated.replace( + "async function trimCache(cacheName, maxItems)", + `${clearAuthSessionEntriesFn}\nasync function trimCache(cacheName, maxItems)`, + ); + } + + if (updated.includes(activateOld)) { + updated = updated.replace(activateOld, activateNew); + } + + return updated; } -let sourcePath = null; -for (const candidate of candidates) { - if (await exists(candidate)) { - sourcePath = candidate; - break; +let restored = false; + +if (!(await exists(expected))) { + let sourcePath = null; + for (const candidate of candidates) { + if (await exists(candidate)) { + sourcePath = candidate; + break; + } + } + + await mkdir(path.dirname(expected), { recursive: true }); + + if (sourcePath) { + const content = await readFile(sourcePath, "utf8"); + await writeFile(expected, content, "utf8"); + restored = true; + console.log(`[postinstall] Restored frontend serviceworker from ${sourcePath}`); + } else { + await writeFile(expected, fallback, "utf8"); + restored = true; + console.log("[postinstall] Created fallback frontend serviceworker"); } } -await mkdir(path.dirname(expected), { recursive: true }); +const source = await readFile(expected, "utf8"); +const updated = patchServiceworker(source); -if (sourcePath) { - const content = await readFile(sourcePath, "utf8"); - await writeFile(expected, content, "utf8"); - console.log(`[postinstall] Restored frontend serviceworker from ${sourcePath}`); -} else { - await writeFile(expected, fallback, "utf8"); - console.log("[postinstall] Created fallback frontend serviceworker"); +if (updated !== source) { + await writeFile(expected, updated, "utf8"); + console.log("[postinstall] Patched frontend serviceworker auth/session cache bypass"); +} else if (!restored) { + console.log("[postinstall] frontend serviceworker already present"); }