aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-05 21:47:30 -0700
committerAdam Soltys <[email protected]>2026-09-05 21:47:30 -0700
commit7e96dc4d4292b29156ec6b9b9c7009f8c60106f1 (patch)
tree5705b219a401bd419f523078d1c937ff0134425b
parentec1d6b8c6da0f9e4e169f4e8f95b88133207b741 (diff)
downloadcoinosv3-7e96dc4d4292b29156ec6b9b9c7009f8c60106f1.tar.gz
coinosv3-7e96dc4d4292b29156ec6b9b9c7009f8c60106f1.zip
Heal dead push subscriptions from a VAPID key rotation — the reason notifications went silent. registerPush reused whatever getSubscription() returned and only ever subscribed fresh when there was none; a subscription made against an OLD VAPID key can never receive a push (the notifier's send fails with a permanent 403, which was never pruned, and the client kept re-registering the same dead endpoint). Now the client fetches the current VAPID key first and, when an existing subscription's applicationServerKey doesn't match, unsubscribes and re-subscribes against the current key — so a device self-heals on next open. And the notifier prunes 403 endpoints alongside 404/410. Deployed nwcpush on cs
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
-rw-r--r--nwcpush/server.js7
-rw-r--r--src/features/messages.js17
2 files changed, 20 insertions, 4 deletions
diff --git a/nwcpush/server.js b/nwcpush/server.js
index 61cab60..cd7d28f 100644
--- a/nwcpush/server.js
+++ b/nwcpush/server.js
@@ -169,11 +169,14 @@ async function pushNotify(id, r, reason, extra) {
}
log(`notify(${reason}) -> device ${id}`);
} catch (e) {
- if (e.statusCode === 404 || e.statusCode === 410) {
+ // 404/410 = gone; 403 = the sub was made against a different VAPID key
+ // and can NEVER receive a push (a stale key rotation), so prune it too —
+ // the client re-subscribes fresh against the current key on next open.
+ if (e.statusCode === 404 || e.statusCode === 410 || e.statusCode === 403) {
delete notifyRegs[id];
persist();
resubscribeNotify();
- log(`dropped an expired notify endpoint ${id}`);
+ log(`dropped a dead notify endpoint ${id} (${e.statusCode})`);
} else {
log(`notify(${reason}) push failed for ${id}: ${e.statusCode || e.message}`);
}
diff --git a/src/features/messages.js b/src/features/messages.js
index 582bf3b..66a1270 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -1101,10 +1101,23 @@ export function messagesFeature(ctx) {
if ((await Notification.requestPermission()) !== 'granted') return false;
}
const reg = await navigator.serviceWorker.ready;
+ const { publicKey } = await (await fetch(`${NOTIFIER}/vapid`)).json();
+ const wantKey = b64ToBytes(publicKey);
let sub = await reg.pushManager.getSubscription();
+ // A subscription made against a DIFFERENT VAPID key can never receive a
+ // push — the notifier's sends fail with a permanent 403 and it keeps
+ // reusing the dead sub, so the device goes silent forever (this is what
+ // stranded notifications after the key last rotated). Re-subscribe when
+ // the existing sub's server key doesn't match the current one.
+ const keyMatches = (s) => {
+ try {
+ const cur = new Uint8Array(s.options.applicationServerKey || []);
+ return cur.length === wantKey.length && cur.every((b, i) => b === wantKey[i]);
+ } catch { return false; }
+ };
+ if (sub && !keyMatches(sub)) { try { await sub.unsubscribe(); } catch {} sub = null; }
if (!sub) {
- const { publicKey } = await (await fetch(`${NOTIFIER}/vapid`)).json();
- sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: b64ToBytes(publicKey) });
+ sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: wantKey });
}
const r = await fetch(`${NOTIFIER}/register`, {
method: 'POST', headers: { 'content-type': 'application/json' },