eleven / messenger Public

Self-heal push subscriptions when the VAPID key changes

A redeploy regenerates VAPID (DB wipe), so a browser's existing subscription was
tied to a stale key and the push service rejected sends. subscribePush now
recreates the subscription if its applicationServerKey != the current key, and
registerSW re-validates on load. (Real fix for the churn is the persistent-disk
deploy; this makes re-enabling work correctly meanwhile.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Paul Campbell 773885f8dea17669d5f3e33eab945df9221b4529 parent c55881c
1 file changed, +28 −5
  • web/static/push.js +28 −5
diff --git a/web/static/push.js b/web/static/push.js
index 1bdd698..5b76bba 100644
--- a/web/static/push.js
+++ b/web/static/push.js
@@ -68,10 +68,9 @@ export async function registerSW(session) {
return;
}
if (pushEnabled()) {
- try {
- const sub = await swReg.pushManager.getSubscription();
- if (sub) await sendSubscription(session.token, sub);
- } catch {}
+ // Re-validate the subscription against the current VAPID key (it changes if
+ // the server DB was reset) and recreate it if stale — self-heals on load.
+ await subscribePush(session);
}
}
@@ -91,11 +90,20 @@ export async function subscribePush(session) {
try {
const reg = swReg || (await navigator.serviceWorker.register("/sw.js"));
const { publicKey } = await (await fetch("/api/push/vapid")).json();
+ const appKey = urlB64ToUint8Array(publicKey);
let sub = await reg.pushManager.getSubscription();
+ // If an existing subscription was made with a different (old) VAPID key, the
+ // push service will reject sends — recreate it against the current key.
+ if (sub && !sameAppServerKey(sub, appKey)) {
+ try {
+ await sub.unsubscribe();
+ } catch {}
+ sub = null;
+ }
if (!sub) {
sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
- applicationServerKey: urlB64ToUint8Array(publicKey),
+ applicationServerKey: appKey,
});
}
await sendSubscription(session.token, sub);
@@ -106,6 +114,21 @@ export async function subscribePush(session) {
}
}
+function sameAppServerKey(sub, appKey) {
+ try {
+ const cur = sub.options && sub.options.applicationServerKey;
+ if (!cur) return false;
+ const a = new Uint8Array(cur);
+ if (a.length !== appKey.length) return false;
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== appKey[i]) return false;
+ }
+ return true;
+ } catch {
+ return false;
+ }
+}
+
export async function isSubscribed() {
if (!("serviceWorker" in navigator)) return false;
try {