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>
61 lines
1.8 KiB
JavaScript
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);
|
|
}),
|
|
);
|
|
});
|