Files
codecraft/web/src/push.ts
T
Richard ZinkandClaude Opus 5.5 10d4b64289 Add incoming webhooks, fix stale service worker and push re-registration
- Webhooks for automations: per-job secret, Gitea/GitHub HMAC signatures,
  payload appended to the instructions; settings on the automations page
- Service worker gets a new file name per build (proxies that cache *.js
  served a stale worker, which broke updates and Android installation);
  /sw.js is a kill switch that removes old workers
- Push: devices re-register their subscription with the server on start and
  before a test; the test reports why a push service rejected a message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 00:38:04 +01:00

64 lines
2.7 KiB
TypeScript

import { get, post } from "./api";
function b64ToBytes(b64: string): Uint8Array<ArrayBuffer> {
const s = atob(b64.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((b64.length + 3) % 4));
const out = new Uint8Array(new ArrayBuffer(s.length));
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
return out;
}
export const pushSupported = () => "serviceWorker" in navigator && "PushManager" in window && "Notification" in window;
/** Current push subscription of this device (null if none). */
export async function currentSubscription(): Promise<PushSubscription | null> {
if (!pushSupported()) return null;
const reg = await navigator.serviceWorker.ready;
return reg.pushManager.getSubscription();
}
/** Ask for permission, subscribe this device and register it with the server. */
export async function enablePush(): Promise<void> {
if (!pushSupported()) throw new Error("Push is not supported by this browser (on iOS: install the app to the home screen first).");
const perm = await Notification.requestPermission();
if (perm !== "granted") throw new Error("Notifications were not allowed.");
const reg = await navigator.serviceWorker.ready;
const { key } = await get<{ key: string }>("/push/key");
let sub = await reg.pushManager.getSubscription();
// A subscription for another server key must be replaced.
if (sub && sub.options.applicationServerKey) {
const cur = new Uint8Array(sub.options.applicationServerKey);
const want = b64ToBytes(key);
if (cur.length !== want.length || cur.some((b, i) => b !== want[i])) {
await sub.unsubscribe();
sub = null;
}
}
if (!sub) sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: b64ToBytes(key) });
await post("/push/subscribe", sub.toJSON());
localStorage.setItem("codecraft.push", "1");
}
export async function disablePush(): Promise<void> {
const sub = await currentSubscription();
if (sub) {
await fetch("/api/push/subscribe", { method: "DELETE", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endpoint: sub.endpoint }) });
await sub.unsubscribe();
}
localStorage.removeItem("codecraft.push");
}
/** Re-register this device's existing subscription with the server (idempotent). */
export async function syncPush(): Promise<boolean> {
const sub = await currentSubscription().catch(() => null);
if (!sub) return false;
await post("/push/subscribe", sub.toJSON());
localStorage.setItem("codecraft.push", "1");
return true;
}
export async function testPush(): Promise<{ sent: number; errors: string[] }> {
// Make sure the server knows this device before testing.
await syncPush();
return post<{ sent: number; errors: string[] }>("/push/test", {});
}