NextMyDay/internal/core/web/static/sw.js
Claus Lohmar dd865d4f27 feat(web): add the web shell and the PWA (SPEC-009)
- One base layout (layout.html) with the header, the nav from Registry.Nav()
  (D-038), a content slot and the CSRF token; every page renders inside it and
  a module supplies content only
- Embedded same-origin assets (app.css · app.js · webauthn.js · sw.js ·
  manifest.webmanifest · icons) served by the reserved `static` workflow
- webauthn.js drives the three ceremonies from data-* attributes — no inline
  script, so the CSP `script-src 'self'` holds (AC-3)
- manifest start_url "/" (D-011); the service worker never caches an
  authenticated response or /{GUID} (A5)
2026-10-14 10:53:57 +00:00

79 lines
2.3 KiB
JavaScript

// MyDay service worker (SPEC-009 §6, register A5).
//
// THE RULE: cache STATIC ASSETS ONLY. An authenticated response and a /{GUID}
// page are NEVER cached — and not merely "not cached": such a request is not
// intercepted by this worker at all, so it cannot reach a cache. Offline is
// explicitly out of scope.
//
// CACHEABLE_PREFIXES is kept in step with web.StaticCachePrefixes (Go) by
// TestAC5_ServiceWorkerPolicy — there is no second, drifting copy of the rule.
"use strict";
var CACHEABLE_PREFIXES = ["/static/"];
var CACHE_NAME = "myday-static-v1";
// isCacheable is the single gate: GET · same origin · a static asset.
function isCacheable(request) {
if (request.method !== "GET") {
return false;
}
var url = new URL(request.url);
if (url.origin !== self.location.origin) {
return false;
}
for (var i = 0; i < CACHEABLE_PREFIXES.length; i++) {
if (url.pathname.indexOf(CACHEABLE_PREFIXES[i]) === 0) {
return true;
}
}
return false;
}
self.addEventListener("install", function (event) {
// Nothing is precached (offline is out of scope) — just take over.
event.waitUntil(self.skipWaiting());
});
self.addEventListener("activate", function (event) {
event.waitUntil(
caches
.keys()
.then(function (keys) {
return Promise.all(
keys
.filter(function (key) {
return key !== CACHE_NAME;
})
.map(function (key) {
return caches.delete(key);
})
);
})
.then(function () {
return self.clients.claim();
})
);
});
self.addEventListener("fetch", function (event) {
// Anything that is not a static asset is NOT intercepted: an authenticated
// response can never be read from — or written to — a cache (A5).
if (!isCacheable(event.request)) {
return;
}
event.respondWith(
caches.open(CACHE_NAME).then(function (cache) {
return cache.match(event.request).then(function (hit) {
if (hit) {
return hit;
}
return fetch(event.request).then(function (response) {
if (response && response.ok && response.type === "basic") {
cache.put(event.request, response.clone());
}
return response;
});
});
})
);
});