fix(webui): keep credentials out of service worker caches

This commit is contained in:
Xubin Ren
2026-08-12 21:09:29 +09:00
parent e455a2b7fa
commit 5fc8303f9e
6 changed files with 83 additions and 34 deletions
+31 -23
View File
@@ -1,6 +1,23 @@
const CACHE_NAME = "nanobot-static-v1";
const CACHE_PREFIX = "nanobot-static-";
const CACHE_NAME = `${CACHE_PREFIX}v2`;
const ASSET_MANIFEST_PATH = "/asset-manifest.json";
const PRECACHE = ["/", "/manifest.json", ASSET_MANIFEST_PATH];
const NETWORK_FIRST_STATIC_PATHS = new Set([
"/",
"/manifest.json",
ASSET_MANIFEST_PATH,
"/brand/nanobot_apple_touch.png",
"/brand/nanobot_favicon_32.png",
"/brand/nanobot_icon_192.png",
"/brand/nanobot_icon_512.png",
"/brand/nanobot_icon_maskable.png",
"/brand/nanobot_mark.svg",
]);
function responseMayBeCached(response) {
const cacheControl = response.headers?.get("Cache-Control") ?? "";
return response.ok && !/(?:^|,)\s*(?:private|no-cache|no-store)\b/i.test(cacheControl);
}
self.addEventListener("install", (event) => {
event.waitUntil(
@@ -91,7 +108,7 @@ self.addEventListener("activate", (event) => {
.then((keys) =>
Promise.all(
keys
.filter((k) => k !== CACHE_NAME)
.filter((k) => k.startsWith(CACHE_PREFIX) && k !== CACHE_NAME)
.map((k) => caches.delete(k))
)
)
@@ -107,28 +124,13 @@ self.addEventListener("fetch", (event) => {
// (never reconstructed), so their credentials mode is preserved and gateway
// auth cookies flow through on every path we touch. WebSocket upgrades are
// never dispatched to a service worker's fetch handler, so the WS endpoint
// cannot be cached; the /__nanobot exclusion below still protects its HTTP
// polling/socket bootstrap endpoints.
// cannot be cached. Unknown HTTP endpoints are passed through below.
if (request.method !== "GET") return;
if (new URL(request.url).origin !== self.location.origin) return;
const url = new URL(request.url);
const path = url.pathname;
// Never cache API, auth, WebSocket, HMR, or WebUI endpoint paths. In
// particular /webui/bootstrap issues fresh gateway credentials on every page
// load and must never be cached or replayed offline. The /auth prefix covers
// the default token endpoint; custom token_issue_path values should be kept
// under one of these prefixes.
if (
path.startsWith("/api") ||
path.startsWith("/auth") ||
path.startsWith("/__nanobot") ||
path.startsWith("/webui")
) {
return;
}
// Static assets: cache-first. Only files under /assets/ carry content hashes
// (the gateway serves them immutable); brand icons, the favicon and other
// un-hashed files can change between releases and stay on the network-first
@@ -138,7 +140,7 @@ self.addEventListener("fetch", (event) => {
caches.match(request).then((cached) => {
if (cached) return cached;
return fetch(request).then((response) => {
if (response.ok) {
if (responseMayBeCached(response)) {
const clone = response.clone();
caches.open(CACHE_NAME).then((c) => c.put(request, clone));
}
@@ -149,22 +151,28 @@ self.addEventListener("fetch", (event) => {
return;
}
// Everything else: network-first (index.html, manifest, brand assets, etc.)
// Cache only explicit public files and browser navigations. Dynamic routes
// are deliberately passed through because token_issue_path and extension
// endpoints are configurable and cannot be safely identified by prefixes.
const isNavigation = request.mode === "navigate";
if (!isNavigation && !NETWORK_FIRST_STATIC_PATHS.has(path)) return;
// App shell and public files: network-first with an offline fallback.
const networkResponse = fetch(request);
event.waitUntil(
networkResponse
.then(async (response) => {
if (!response.ok) return;
if (!responseMayBeCached(response)) return;
// Clone before the first await. The original response is also handed
// to respondWith(), which may lock its body as soon as this callback
// yields to the event loop.
const cachedResponse = response.clone();
const cache = await caches.open(CACHE_NAME);
await cache.put(request, cachedResponse);
await cache.put(isNavigation ? "/" : request, cachedResponse);
// Refresh the complete build graph before pruning. A deployment can
// change index.html without changing sw.js, so this cannot rely only
// on the manifest cached when the worker was installed.
if (path === "/") {
if (isNavigation || path === "/") {
if (await refreshAssetManifest(cache)) await pruneStaleEntries();
}
})