From 04eb0fe506042a1b89c254cb12aad5cc878a1f05 Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Fri, 24 Jul 2026 20:44:02 +0300 Subject: [PATCH] fix(push): reconnect subscriptions after VAPID rotation --- assets/js/hooks.js | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/assets/js/hooks.js b/assets/js/hooks.js index c1059cf..60dacb1 100644 --- a/assets/js/hooks.js +++ b/assets/js/hooks.js @@ -681,6 +681,34 @@ export const Hooks = { return Uint8Array.from([...raw].map(character => character.charCodeAt(0))) } + this.subscriptionUsesPublicKey = (subscription, publicKey) => { + const configuredKey = this.decodeApplicationServerKey(publicKey) + const subscribedKey = subscription?.options?.applicationServerKey + + if (!subscribedKey) return false + + const subscribedBytes = new Uint8Array(subscribedKey) + return subscribedBytes.length === configuredKey.length && + subscribedBytes.every((byte, index) => byte === configuredKey[index]) + } + + this.subscriptionForPublicKey = async (registration, publicKey) => { + let subscription = await registration.pushManager.getSubscription() + + if (subscription && !this.subscriptionUsesPublicKey(subscription, publicKey)) { + const unsubscribed = await subscription.unsubscribe() + if (!unsubscribed) throw new Error("stale_push_subscription_could_not_be_removed") + + window.localStorage.removeItem("wnh.push.server-device-id") + subscription = null + } + + return subscription || registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: this.decodeApplicationServerKey(publicKey) + }) + } + this.installationId = () => { const key = "wnh.push.installation-id" const existing = window.localStorage.getItem(key) @@ -806,11 +834,7 @@ export const Hooks = { } const registration = await navigator.serviceWorker.ready - const subscription = await registration.pushManager.getSubscription() || - await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: this.decodeApplicationServerKey(publicKey) - }) + const subscription = await this.subscriptionForPublicKey(registration, publicKey) const json = subscription.toJSON() await this.registerDevice({ @@ -850,14 +874,20 @@ export const Hooks = { try { const registration = await navigator.serviceWorker.ready const subscription = await registration.pushManager.getSubscription() + const currentKey = subscription && + this.subscriptionUsesPublicKey(subscription, publicKey) const deviceId = window.localStorage.getItem("wnh.push.server-device-id") const activeServerDevice = deviceId && [...this.el.querySelectorAll("[data-disable-device]")] .some(element => element.dataset.disableDevice === deviceId) - if (subscription && activeServerDevice) { + if (subscription && currentKey && activeServerDevice) { this.setButtonState(true) this.setStatus("Push notifications are enabled on this device.") + } else if (subscription && !currentKey) { + this.setStatus( + "Push keys changed. Enable notifications to reconnect this device securely." + ) } else if (subscription || activeServerDevice) { this.setStatus("This browser has a push subscription. Enable it to reconnect this device.") }