Files
silo-server/web/public/sw.js
QuickandClaude Fable 5 e5b210589d fix(notifications): address PR #136 review findings
Codex + CodeRabbit review fixes, all verified against current behavior:

- Web Push: single-writer VAPID provisioning via a new conditional
  SetIfAbsent settings write (no split-brain identity across nodes), and
  read/decode failures now surface instead of silently rotating the
  keypair; the eager-provisioning goroutine joins the shutdown WaitGroup
- Web Push: endpoint reassignment purges the previous owner's pending
  attempts inside the upsert transaction, with an ownership re-check at
  send time
- Webhooks: per-profile cap enforced atomically (advisory-locked
  count+insert), typed pgconn unique-violation mapping, create-time
  type/URL mismatch rejection, send-time HTTPS re-check, and Retry-After
  HTTP-date support (shared, clamped parser also used by web push)
- Delivery workers: transient delivery-row lookup errors leave the claim
  to lease expiry instead of permanently failing the attempt
- Interest: history-only imports now feed the index (userstore history
  hooks + completed-history folding in recompute/rebuild), rebuild also
  recomputes existing interest rows so removed sources get cleaned up,
  and failed flush mutations requeue (bounded) instead of dropping
- Retention: read notifications age from read_at, not created_at
- Startup: scan queue workers start only after the availability detector
  is wired, so resumed scans cannot skip availability recording
- mail: settings-store read failures propagate instead of reading as
  "not configured"
- DB: new migration adds episode ordinal/key CHECK constraints
- Web: service worker restricts notification clicks to same-origin URLs,
  preferences popover gets an error+retry state, and the realtime
  profile-rebind backoff grows to 5 minutes to keep shared channels
  stable through notifications-only outages

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 17:16:27 -04:00

61 lines
1.8 KiB
JavaScript

/**
* Silo service worker: displays Web Push notifications and routes clicks.
* Payloads arrive end-to-end encrypted (RFC 8291); by the time the push
* event fires the browser has decrypted them for us.
*/
self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("push", (event) => {
let data = {};
try {
data = event.data ? event.data.json() : {};
} catch {
data = {};
}
const title = data.title || "Silo";
const options = {
body: data.body || "",
icon: data.icon || "/web-app-icon-192.png",
badge: "/web-app-icon-192.png",
tag: data.tag || undefined,
data: { url: data.url || "/notifications" },
};
event.waitUntil(self.registration.showNotification(title, options));
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
// Notifications navigate same-origin only: payload data is server-built,
// but a notification surface must never become an open redirect.
const rawUrl = (event.notification.data && event.notification.data.url) || "/notifications";
let url = "/notifications";
try {
const parsed = new URL(rawUrl, self.location.origin);
if (parsed.origin === self.location.origin) {
url = `${parsed.pathname}${parsed.search}${parsed.hash}`;
}
} catch {
// keep the safe default
}
event.waitUntil(
self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientList) => {
for (const client of clientList) {
if ("focus" in client) {
if ("navigate" in client) {
client.navigate(url);
}
return client.focus();
}
}
return self.clients.openWindow(url);
}),
);
});