aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-03-28 06:25:30 +0000
committerFuwn <[email protected]>2026-03-28 06:25:30 +0000
commitb956fafb411fdb4f4ab5d6ed0d417bc3a14c656e (patch)
tree2432a44c7ea901e891e36ce4f7fcab5c9f7ec91c
parentfix(notifications): prune dead push endpoints (diff)
downloaddue.moe-b956fafb411fdb4f4ab5d6ed0d417bc3a14c656e.tar.xz
due.moe-b956fafb411fdb4f4ab5d6ed0d417bc3a14c656e.zip
fix(notifications): stabilize browser subscription identity
-rw-r--r--src/lib/Utility/fingerprint.ts25
-rw-r--r--src/trigger/notifications.ts21
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", {