self.addEventListener("install", () => { self.skipWaiting(); }); self.addEventListener("activate", (event) => { // Navigations through this worker wait for activation. Awaiting a client // reload here deadlocks first visits, including the token sign-in screen. event.waitUntil(self.clients.claim()); }); self.addEventListener("fetch", (event) => { if (event.request.mode !== "navigate") return; event.respondWith(fetch(new Request(event.request, { cache: "no-store" }))); }); // This worker never receives credentials and never performs authenticated fetches. // Safari requires a visible notification for each valid message push. Reads are // reconciled only by foreground page messages, never an invisible Web Push. const PUSH_TITLE = "Sybil Agent", PUSH_BODY = "You have a new message.", PUSH_URL = "/?agent=root"; const sourceId = value => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); const cursor = value => typeof value === "string" && /^\d{1,16}$/.test(value) && Number.isSafeInteger(Number(value)); const messageId = value => typeof value === "string" && /^[A-Za-z0-9:_-]{1,200}$/.test(value); const owned = value => value?.version === 1 && value.kind === "message" && sourceId(value.sourceId) && cursor(value.cursor) && messageId(value.messageId); let queue = Promise.resolve(); const serial = operation => { const result = queue.then(operation); queue = result.catch(() => undefined); return result; }; function database() { return new Promise((resolve, reject) => { const request = indexedDB.open("sybil-agent-notifications-v1", 1); request.onupgradeneeded = () => request.result.createObjectStore("sources", { keyPath: "sourceId" }); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(new Error("Notification storage unavailable.")); request.onblocked = () => reject(new Error("Notification storage unavailable.")); }); } async function stored(id) { const db = await database(); try { return await new Promise((resolve, reject) => { const request = db.transaction("sources").objectStore("sources").get(id); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(new Error("Notification storage unavailable.")); }); } finally { db.close(); } } async function save(value) { const db = await database(); try { await new Promise((resolve, reject) => { const tx = db.transaction("sources", "readwrite"), store = tx.objectStore("sources"); store.put({ ...value, touched: Date.now() }); const request = store.getAll(); request.onsuccess = () => { const rows = request.result.sort((a, b) => b.touched - a.touched); for (const stale of rows.slice(16)) store.delete(stale.sourceId); }; tx.oncomplete = () => resolve(); tx.onerror = () => reject(new Error("Notification storage unavailable.")); tx.onabort = () => reject(new Error("Notification storage unavailable.")); }); } finally { db.close(); } } async function announce(type) { for (const client of await self.clients.matchAll({ type: "window", includeUncontrolled: true })) { if (new URL(client.url).origin === self.location.origin) client.postMessage({ type }); } } self.addEventListener("push", event => { event.waitUntil(serial(async () => { let value; try { const raw = event.data?.text(); if (!raw || raw.length > 4096) return; value = JSON.parse(raw); } catch { return; } if (!owned(value) || value.url !== PUSH_URL || value.tag !== `sybil-agent:${value.sourceId}` || value.title !== PUSH_TITLE || value.body !== PUSH_BODY) return; const previous = await stored(value.sourceId).catch(() => null); // Re-delivery replaces the same owned tag without regressing its cursor. // Still show it visibly: local read state is not a reason for an invisible push. const identity = owned(previous?.latest) && BigInt(previous.latest.cursor) > BigInt(value.cursor) ? previous.latest : { version: 1, kind: "message", sourceId: value.sourceId, messageId: value.messageId, cursor: BigInt(value.cursor).toString(), }; await self.registration.showNotification(PUSH_TITLE, { body: PUSH_BODY, tag: `sybil-agent:${identity.sourceId}`, icon: "/icons/icon-192.png", badge: "/icons/icon-192.png", renotify: false, data: identity, }); await save({ ...previous, sourceId: identity.sourceId, latest: identity }).catch(() => undefined); await announce("sybil-push-shown"); })); }); self.addEventListener("notificationclick", event => { event.notification.close(); if (!owned(event.notification.data) || event.notification.tag !== `sybil-agent:${event.notification.data.sourceId}`) return; event.waitUntil((async () => { const destination = new URL(PUSH_URL, self.location.origin).href; const windows = await self.clients.matchAll({ type: "window", includeUncontrolled: true }); for (const client of windows) { const url = new URL(client.url); if (url.origin !== self.location.origin || !["/", "/search"].includes(url.pathname)) continue; try { const routed = await client.navigate(destination); if (routed) { await routed.focus(); return; } } catch { /* Try another client. */ } } await self.clients.openWindow(destination); // A tap routes only. The rendered transcript owns the read acknowledgment. })()); }); self.addEventListener("message", event => { const value = event.data; if (value?.version !== 1 || !["sybil-agent-read", "sybil-agent-logout"].includes(value.type)) return; event.waitUntil(serial(async () => { // A same-origin WindowClient is required; no remote/message-channel authority. const client = event.source?.id ? await self.clients.get(event.source.id) : null; if (!client || client.type !== "window" || new URL(client.url).origin !== self.location.origin) return; if (value.type === "sybil-agent-read") { if (!sourceId(value.sourceId) || !cursor(value.cursor)) return; const previous = await stored(value.sourceId).catch(() => null); const readCursor = cursor(previous?.readCursor) && BigInt(previous.readCursor) > BigInt(value.cursor) ? previous.readCursor : BigInt(value.cursor).toString(); await save({ ...previous, sourceId: value.sourceId, readCursor }).catch(() => undefined); const notifications = await self.registration.getNotifications({ tag: `sybil-agent:${value.sourceId}` }); for (const notification of notifications) { const data = notification.data; if (owned(data) && data.sourceId === value.sourceId && notification.tag === `sybil-agent:${value.sourceId}` && BigInt(data.cursor) <= BigInt(readCursor)) notification.close(); } } else { for (const notification of await self.registration.getNotifications()) { if (owned(notification.data) && notification.tag === `sybil-agent:${notification.data.sourceId}`) notification.close(); } } event.ports?.[0]?.postMessage({ ok: true }); })); }); self.addEventListener("pushsubscriptionchange", event => { // Re-registration needs a currently authenticated page. Never cache its token here. event.waitUntil(announce("sybil-push-subscription-changed")); });