summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator>2026-03-17 08:04:52 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-18 07:54:48 +0000
commit1d550c1196ea836e0a0f798ba0127c1086f5f963 (patch)
tree9bc4a9d8934b0e993e2802528551d2b6b013a799 /web
parentb5df9cadd7b8a275b8688ee8fb957142536fd26a (diff)
fix: unsubscribe stale push subscription before re-subscribing
When the VAPID key changes (e.g. after the key-swap fix), the browser's cached PushSubscription was created with the old key. Calling PushManager.subscribe() with a different applicationServerKey then throws "The provided applicationServerKey is not valid". Fix by calling getSubscription()/unsubscribe() before subscribe() so any stale subscription is cleared. Adds web test covering both the stale and fresh subscription paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web')
-rw-r--r--web/app.js8
-rw-r--r--web/test/enable-notifications.test.mjs64
2 files changed, 72 insertions, 0 deletions
diff --git a/web/app.js b/web/app.js
index 9a5a460..77a5d19 100644
--- a/web/app.js
+++ b/web/app.js
@@ -2646,6 +2646,14 @@ async function enableNotifications(btn) {
await registerServiceWorker();
const registration = await navigator.serviceWorker.ready;
+ // Unsubscribe any stale subscription (e.g. from a VAPID key rotation).
+ // PushManager.subscribe() throws "applicationServerKey is not valid" if the
+ // existing subscription was created with a different key.
+ const existingSub = await registration.pushManager.getSubscription();
+ if (existingSub) {
+ await existingSub.unsubscribe();
+ }
+
// Subscribe via PushManager.
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
diff --git a/web/test/enable-notifications.test.mjs b/web/test/enable-notifications.test.mjs
new file mode 100644
index 0000000..c8afdd3
--- /dev/null
+++ b/web/test/enable-notifications.test.mjs
@@ -0,0 +1,64 @@
+// enable-notifications.test.mjs — Tests for the enableNotifications subscription flow.
+//
+// Run with: node --test web/test/enable-notifications.test.mjs
+
+import { describe, it, beforeEach } from 'node:test';
+import assert from 'node:assert/strict';
+
+// ── Logic under test ──────────────────────────────────────────────────────────
+//
+// When subscribing to push notifications, any existing stale subscription
+// (e.g. from before a VAPID key rotation) must be unsubscribed first.
+// Otherwise the browser rejects subscribe() with "applicationServerKey is not valid".
+
+/**
+ * Extracted subscription logic (mirrors enableNotifications in app.js).
+ * Returns the new subscription endpoint, or throws on error.
+ */
+async function subscribeWithUnsubscribeStale(pushManager, applicationServerKey) {
+ // Clear any stale existing subscription.
+ const existing = await pushManager.getSubscription();
+ if (existing) {
+ await existing.unsubscribe();
+ }
+ const sub = await pushManager.subscribe({ userVisibleOnly: true, applicationServerKey });
+ return sub;
+}
+
+describe('subscribeWithUnsubscribeStale', () => {
+ it('unsubscribes existing subscription before subscribing', async () => {
+ let unsubscribeCalled = false;
+ const existingSub = {
+ unsubscribe: async () => { unsubscribeCalled = true; },
+ };
+ let subscribeCalled = false;
+ const pushManager = {
+ getSubscription: async () => existingSub,
+ subscribe: async (opts) => {
+ subscribeCalled = true;
+ return { endpoint: 'https://push.example.com/sub/new', toJSON: () => ({}) };
+ },
+ };
+
+ await subscribeWithUnsubscribeStale(pushManager, new Uint8Array([4, 1, 2]));
+
+ assert.equal(unsubscribeCalled, true, 'existing subscription should have been unsubscribed');
+ assert.equal(subscribeCalled, true, 'new subscription should have been created');
+ });
+
+ it('subscribes normally when no existing subscription', async () => {
+ let subscribeCalled = false;
+ const pushManager = {
+ getSubscription: async () => null,
+ subscribe: async (opts) => {
+ subscribeCalled = true;
+ return { endpoint: 'https://push.example.com/sub/new', toJSON: () => ({}) };
+ },
+ };
+
+ const sub = await subscribeWithUnsubscribeStale(pushManager, new Uint8Array([4, 1, 2]));
+
+ assert.equal(subscribeCalled, true, 'subscribe should have been called');
+ assert.ok(sub, 'subscription object should be returned');
+ });
+});