diff options
| author | Fuwn <[email protected]> | 2026-03-28 06:25:30 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-03-28 06:25:30 +0000 |
| commit | b956fafb411fdb4f4ab5d6ed0d417bc3a14c656e (patch) | |
| tree | 2432a44c7ea901e891e36ce4f7fcab5c9f7ec91c | |
| parent | fix(notifications): prune dead push endpoints (diff) | |
| download | due.moe-b956fafb411fdb4f4ab5d6ed0d417bc3a14c656e.tar.xz due.moe-b956fafb411fdb4f4ab5d6ed0d417bc3a14c656e.zip | |
fix(notifications): stabilize browser subscription identity
| -rw-r--r-- | src/lib/Utility/fingerprint.ts | 25 | ||||
| -rw-r--r-- | src/trigger/notifications.ts | 21 |
2 files changed, 30 insertions, 16 deletions
diff --git a/src/lib/Utility/fingerprint.ts b/src/lib/Utility/fingerprint.ts index 5af258d0..e38ff61f 100644 --- a/src/lib/Utility/fingerprint.ts +++ b/src/lib/Utility/fingerprint.ts @@ -1,18 +1,13 @@ -export const getFingerprint = () => - btoa( - `${(() => { - const gl = new OffscreenCanvas(0, 0).getContext("webgl"); +const STORAGE_KEY = "notificationDeviceId"; - if (!gl) return "none"; +export const getFingerprint = () => { + const existingFingerprint = window.localStorage.getItem(STORAGE_KEY); - const debugInfo = gl.getExtension("WEBGL_debug_renderer_info"); + if (existingFingerprint) return existingFingerprint; - return debugInfo - ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) - : "unknown"; - })()}-${ - navigator === null || navigator === void 0 - ? void 0 - : navigator.hardwareConcurrency - }`, - ); + const generatedFingerprint = window.crypto.randomUUID(); + + window.localStorage.setItem(STORAGE_KEY, generatedFingerprint); + + return generatedFingerprint; +}; diff --git a/src/trigger/notifications.ts b/src/trigger/notifications.ts index 5b2453c2..30a50a12 100644 --- a/src/trigger/notifications.ts +++ b/src/trigger/notifications.ts @@ -14,6 +14,15 @@ const isExpiredSubscriptionError = (error: unknown) => { return statusCode === 404 || statusCode === 410; }; +const subscriptionEndpoint = (subscription: unknown) => + typeof subscription === "object" && + subscription !== null && + "endpoint" in subscription && + typeof subscription.endpoint === "string" && + subscription.endpoint.length + ? subscription.endpoint + : null; + export const notificationsTask = schedules.task({ id: "notifications", run: async (_payload, { ctx }) => { @@ -56,6 +65,7 @@ export const notificationsTask = schedules.task({ }; const transientErrors: unknown[] = []; + const seenEndpoints = new Set<string>(); webpush.setVapidDetails( ( @@ -81,7 +91,15 @@ export const notificationsTask = schedules.task({ ).value, ); - for (const subscription of await getUserSubscriptions()) + for (const subscription of await getUserSubscriptions()) { + const endpoint = subscriptionEndpoint(subscription.subscription); + + if (endpoint) { + if (seenEndpoints.has(endpoint)) continue; + + seenEndpoints.add(endpoint); + } + try { await webpush.sendNotification(subscription.subscription, "."); } catch (error) { @@ -94,6 +112,7 @@ export const notificationsTask = schedules.task({ console.error(error); } + } if (transientErrors.length > 0) throw new Error("Transient push delivery failures occurred", { |