- 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>
64 lines
2.7 KiB
TypeScript
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", {});
|
|
}
|