/// /// // Minimal PWA service worker (Phase 1): precache the built app shell + static // files so the app launches offline. Pages and relay/auth requests are NOT // cached (they're user-specific). Phase 2 adds `push` / `notificationclick` // handlers for web push. import { build, files, version } from '$service-worker'; const sw = self as unknown as ServiceWorkerGlobalScope; const CACHE = `atmo-pub-${version}`; // Built JS/CSS (immutable, hashed) + static assets (icon, manifest, robots). const PRECACHE = [...build, ...files]; sw.addEventListener('install', (event) => { event.waitUntil( caches .open(CACHE) .then((cache) => cache.addAll(PRECACHE)) .then(() => sw.skipWaiting()) ); }); sw.addEventListener('activate', (event) => { event.waitUntil( (async () => { for (const key of await caches.keys()) { if (key !== CACHE) await caches.delete(key); } await sw.clients.claim(); })() ); }); sw.addEventListener('fetch', (event) => { const { request } = event; if (request.method !== 'GET') return; const url = new URL(request.url); if (url.protocol !== 'https:' && url.protocol !== 'http:') return; // Only serve precached, build/static assets from the cache; everything else // (pages, OAuth, relay data) goes straight to the network so we never serve // stale or another user's content. if (PRECACHE.includes(url.pathname)) { event.respondWith( caches.match(request).then((cached) => cached ?? fetch(request)) ); } }); // Web push: show the notification the relay delivered. sw.addEventListener('push', (event) => { let data: { title?: string; body?: string; uri?: string; notificationId?: string } = {}; try { data = event.data?.json() ?? {}; } catch { /* non-JSON payload — fall back to defaults */ } event.waitUntil( sw.registration.showNotification(data.title ?? 'atmo.pub', { body: data.body ?? '', icon: '/icon.svg', badge: '/icon.svg', // `n` lets the click handler mark this inbox notification read on open. data: { uri: data.uri, n: data.notificationId } }) ); }); // A click goes to the PWA (the SW can only open in-scope pages). For an external // link we open the in-scope `/go` handoff page, which then bounces to the user's // default browser (a PWA can't render an out-of-scope URL); links to nowhere just // open the inbox. Either way we carry `n` (the notification id) so the opened page // can mark it read — iOS doesn't do that just because the app came to the front. // Routing through `/go` also keeps `client.navigate` same-origin (it can't navigate // an existing window to a cross-origin URL). sw.addEventListener('notificationclick', (event) => { event.notification.close(); const { uri, n } = (event.notification.data as { uri?: string; n?: string } | null) ?? {}; const origin = sw.location.origin; const nParam = n ? `&n=${encodeURIComponent(n)}` : ''; const target = uri && /^https?:/.test(uri) ? new URL(`/go?to=${encodeURIComponent(uri)}${nParam}`, origin).href : new URL(`/inbox${n ? `?n=${encodeURIComponent(n)}` : ''}`, origin).href; event.waitUntil( (async () => { const clients = await sw.clients.matchAll({ type: 'window', includeUncontrolled: true }); for (const client of clients) { await client.focus(); await client.navigate(target); return; } await sw.clients.openWindow(target); })() ); });