@markwharton/pwa-push 1.5.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/client.d.ts +181 -0
  2. package/dist/client.js +412 -0
  3. package/dist/index.d.ts +9 -0
  4. package/dist/index.js +9 -0
  5. package/dist/pwa-push-sw.js +61 -39
  6. package/dist/{server/send.d.ts → server.d.ts} +6 -2
  7. package/dist/{server/send.js → server.js} +9 -7
  8. package/dist/shared.d.ts +22 -2
  9. package/dist/shared.js +9 -6
  10. package/dist/sw.d.ts +1 -1
  11. package/dist/sw.js +2 -2
  12. package/package.json +11 -8
  13. package/dist/__tests__/client/deviceId.test.d.ts +0 -1
  14. package/dist/__tests__/client/deviceId.test.js +0 -134
  15. package/dist/__tests__/client/encoding.test.d.ts +0 -1
  16. package/dist/__tests__/client/encoding.test.js +0 -89
  17. package/dist/__tests__/client/indexedDb.test.d.ts +0 -1
  18. package/dist/__tests__/client/indexedDb.test.js +0 -195
  19. package/dist/__tests__/client/renewal.test.d.ts +0 -1
  20. package/dist/__tests__/client/renewal.test.js +0 -170
  21. package/dist/__tests__/client/subscribe.test.d.ts +0 -1
  22. package/dist/__tests__/client/subscribe.test.js +0 -299
  23. package/dist/__tests__/server/send.test.d.ts +0 -1
  24. package/dist/__tests__/server/send.test.js +0 -226
  25. package/dist/client/deviceId.d.ts +0 -23
  26. package/dist/client/deviceId.js +0 -49
  27. package/dist/client/encoding.d.ts +0 -17
  28. package/dist/client/encoding.js +0 -32
  29. package/dist/client/index.d.ts +0 -4
  30. package/dist/client/index.js +0 -20
  31. package/dist/client/indexedDb.d.ts +0 -38
  32. package/dist/client/indexedDb.js +0 -89
  33. package/dist/client/renewal.d.ts +0 -31
  34. package/dist/client/renewal.js +0 -80
  35. package/dist/client/subscribe.d.ts +0 -88
  36. package/dist/client/subscribe.js +0 -176
  37. package/dist/server/index.d.ts +0 -1
  38. package/dist/server/index.js +0 -11
  39. package/dist/types.d.ts +0 -39
  40. package/dist/types.js +0 -11
@@ -1 +0,0 @@
1
- export {};
@@ -1,226 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- const vitest_1 = require("vitest");
40
- // Mock web-push
41
- vitest_1.vi.mock('web-push', () => ({
42
- default: {
43
- setVapidDetails: vitest_1.vi.fn(),
44
- sendNotification: vitest_1.vi.fn()
45
- }
46
- }));
47
- const web_push_1 = __importDefault(require("web-push"));
48
- (0, vitest_1.describe)('Push server', () => {
49
- const validConfig = {
50
- publicKey: 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
51
- privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls',
52
- subject: 'mailto:test@example.com'
53
- };
54
- (0, vitest_1.beforeEach)(() => {
55
- vitest_1.vi.resetModules();
56
- vitest_1.vi.clearAllMocks();
57
- });
58
- (0, vitest_1.describe)('initPush', () => {
59
- (0, vitest_1.it)('initializes with valid config', async () => {
60
- const { initPush, getVapidPublicKey } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
61
- initPush(validConfig);
62
- (0, vitest_1.expect)(getVapidPublicKey()).toBe(validConfig.publicKey);
63
- (0, vitest_1.expect)(web_push_1.default.setVapidDetails).toHaveBeenCalledWith(validConfig.subject, validConfig.publicKey, validConfig.privateKey);
64
- });
65
- (0, vitest_1.it)('throws if publicKey is missing', async () => {
66
- const { initPush } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
67
- (0, vitest_1.expect)(() => initPush({ ...validConfig, publicKey: undefined })).toThrow('VAPID keys required');
68
- });
69
- (0, vitest_1.it)('throws if privateKey is missing', async () => {
70
- const { initPush } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
71
- (0, vitest_1.expect)(() => initPush({ ...validConfig, privateKey: undefined })).toThrow('VAPID keys required');
72
- });
73
- (0, vitest_1.it)('throws if subject is missing', async () => {
74
- const { initPush } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
75
- (0, vitest_1.expect)(() => initPush({ ...validConfig, subject: undefined })).toThrow('VAPID subject required');
76
- });
77
- });
78
- (0, vitest_1.describe)('initPushFromEnv', () => {
79
- (0, vitest_1.it)('reads from environment variables', async () => {
80
- process.env.VAPID_PUBLIC_KEY = validConfig.publicKey;
81
- process.env.VAPID_PRIVATE_KEY = validConfig.privateKey;
82
- process.env.VAPID_SUBJECT = validConfig.subject;
83
- const { initPushFromEnv, getVapidPublicKey } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
84
- initPushFromEnv();
85
- (0, vitest_1.expect)(getVapidPublicKey()).toBe(validConfig.publicKey);
86
- delete process.env.VAPID_PUBLIC_KEY;
87
- delete process.env.VAPID_PRIVATE_KEY;
88
- delete process.env.VAPID_SUBJECT;
89
- });
90
- });
91
- (0, vitest_1.describe)('getVapidPublicKey', () => {
92
- (0, vitest_1.it)('throws if not initialized', async () => {
93
- const { getVapidPublicKey } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
94
- (0, vitest_1.expect)(() => getVapidPublicKey()).toThrow('Push not initialized');
95
- });
96
- (0, vitest_1.it)('returns public key after initialization', async () => {
97
- const { initPush, getVapidPublicKey } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
98
- initPush(validConfig);
99
- (0, vitest_1.expect)(getVapidPublicKey()).toBe(validConfig.publicKey);
100
- });
101
- });
102
- (0, vitest_1.describe)('sendPushNotification', () => {
103
- const subscription = {
104
- endpoint: 'https://fcm.googleapis.com/fcm/send/abc123',
105
- keys: {
106
- p256dh: 'testkey',
107
- auth: 'testauthkey'
108
- }
109
- };
110
- const payload = {
111
- title: 'Test',
112
- body: 'Test message'
113
- };
114
- (0, vitest_1.it)('returns ok on success', async () => {
115
- vitest_1.vi.mocked(web_push_1.default.sendNotification).mockResolvedValue({});
116
- const { initPush, sendPushNotification } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
117
- initPush(validConfig);
118
- const result = await sendPushNotification(subscription, payload);
119
- (0, vitest_1.expect)(result.ok).toBe(true);
120
- });
121
- (0, vitest_1.it)('returns error on failure', async () => {
122
- vitest_1.vi.mocked(web_push_1.default.sendNotification).mockRejectedValue({
123
- statusCode: 410,
124
- message: 'Gone'
125
- });
126
- const { initPush, sendPushNotification } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
127
- initPush(validConfig);
128
- const result = await sendPushNotification(subscription, payload);
129
- (0, vitest_1.expect)(result.ok).toBe(false);
130
- (0, vitest_1.expect)(result.error).toBe('Gone');
131
- (0, vitest_1.expect)(result.statusCode).toBe(410);
132
- });
133
- (0, vitest_1.it)('returns error if not initialized', async () => {
134
- const { sendPushNotification } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
135
- const result = await sendPushNotification(subscription, payload);
136
- (0, vitest_1.expect)(result.ok).toBe(false);
137
- (0, vitest_1.expect)(result.error).toBe('Push not initialized');
138
- });
139
- (0, vitest_1.it)('returns unknown error when error has no message', async () => {
140
- vitest_1.vi.mocked(web_push_1.default.sendNotification).mockRejectedValue({ statusCode: 500 });
141
- const { initPush, sendPushNotification } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
142
- initPush(validConfig);
143
- const result = await sendPushNotification(subscription, payload);
144
- (0, vitest_1.expect)(result.ok).toBe(false);
145
- (0, vitest_1.expect)(result.error).toBe('Unknown error');
146
- (0, vitest_1.expect)(result.statusCode).toBe(500);
147
- });
148
- });
149
- (0, vitest_1.describe)('sendPushToAll', () => {
150
- const subscriptions = [
151
- { endpoint: 'https://example.com/1', keys: { p256dh: 'k1', auth: 'a1' } },
152
- { endpoint: 'https://example.com/2', keys: { p256dh: 'k2', auth: 'a2' } }
153
- ];
154
- const payload = { title: 'Broadcast', body: 'Hello all' };
155
- (0, vitest_1.it)('sends to all subscriptions', async () => {
156
- vitest_1.vi.mocked(web_push_1.default.sendNotification).mockResolvedValue({});
157
- const { initPush, sendPushToAll } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
158
- initPush(validConfig);
159
- const results = await sendPushToAll(subscriptions, payload);
160
- (0, vitest_1.expect)(results).toHaveLength(2);
161
- (0, vitest_1.expect)(results.every(r => r.ok)).toBe(true);
162
- });
163
- (0, vitest_1.it)('handles mixed success/failure', async () => {
164
- vitest_1.vi.mocked(web_push_1.default.sendNotification)
165
- .mockResolvedValueOnce({})
166
- .mockRejectedValueOnce({ statusCode: 410, message: 'Gone' });
167
- const { initPush, sendPushToAll } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
168
- initPush(validConfig);
169
- const results = await sendPushToAll(subscriptions, payload);
170
- (0, vitest_1.expect)(results[0].ok).toBe(true);
171
- (0, vitest_1.expect)(results[1].ok).toBe(false);
172
- });
173
- });
174
- (0, vitest_1.describe)('sendPushWithDetails', () => {
175
- const subscription = {
176
- endpoint: 'https://fcm.googleapis.com/fcm/send/abc123',
177
- keys: { p256dh: 'testkey', auth: 'testauthkey' },
178
- basePath: '/app'
179
- };
180
- (0, vitest_1.it)('injects basePath from subscription', async () => {
181
- vitest_1.vi.mocked(web_push_1.default.sendNotification).mockResolvedValue({});
182
- const { initPush, sendPushWithDetails } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
183
- initPush(validConfig);
184
- await sendPushWithDetails(subscription, { title: 'Test', body: 'Hello' });
185
- (0, vitest_1.expect)(web_push_1.default.sendNotification).toHaveBeenCalledWith(subscription, vitest_1.expect.stringContaining('"basePath":"/app"'));
186
- });
187
- (0, vitest_1.it)('uses payload basePath over subscription basePath', async () => {
188
- vitest_1.vi.mocked(web_push_1.default.sendNotification).mockResolvedValue({});
189
- const { initPush, sendPushWithDetails } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
190
- initPush(validConfig);
191
- await sendPushWithDetails(subscription, {
192
- title: 'Test',
193
- body: 'Hello',
194
- basePath: '/override'
195
- });
196
- (0, vitest_1.expect)(web_push_1.default.sendNotification).toHaveBeenCalledWith(subscription, vitest_1.expect.stringContaining('"basePath":"/override"'));
197
- });
198
- (0, vitest_1.it)('defaults basePath to "/" when not specified', async () => {
199
- vitest_1.vi.mocked(web_push_1.default.sendNotification).mockResolvedValue({});
200
- const { initPush, sendPushWithDetails } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
201
- initPush(validConfig);
202
- const subWithoutBasePath = {
203
- endpoint: 'https://example.com',
204
- keys: { p256dh: 'k', auth: 'a' }
205
- };
206
- await sendPushWithDetails(subWithoutBasePath, { title: 'Test', body: 'Hello' });
207
- (0, vitest_1.expect)(web_push_1.default.sendNotification).toHaveBeenCalledWith(subWithoutBasePath, vitest_1.expect.stringContaining('"basePath":"/"'));
208
- });
209
- });
210
- (0, vitest_1.describe)('isSubscriptionExpired', () => {
211
- (0, vitest_1.it)('returns true for 410 status', async () => {
212
- const { isSubscriptionExpired } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
213
- (0, vitest_1.expect)(isSubscriptionExpired(410)).toBe(true);
214
- });
215
- (0, vitest_1.it)('returns false for other statuses', async () => {
216
- const { isSubscriptionExpired } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
217
- (0, vitest_1.expect)(isSubscriptionExpired(400)).toBe(false);
218
- (0, vitest_1.expect)(isSubscriptionExpired(401)).toBe(false);
219
- (0, vitest_1.expect)(isSubscriptionExpired(500)).toBe(false);
220
- });
221
- (0, vitest_1.it)('returns false for undefined', async () => {
222
- const { isSubscriptionExpired } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
223
- (0, vitest_1.expect)(isSubscriptionExpired(undefined)).toBe(false);
224
- });
225
- });
226
- });
@@ -1,23 +0,0 @@
1
- /**
2
- * Device ID utilities for subscription deduplication
3
- * Prevents duplicate subscriptions when service worker re-registers
4
- */
5
- /**
6
- * Gets or creates a persistent device ID for subscription deduplication.
7
- * The ID is stored in localStorage and persists across sessions.
8
- * Uses localStorage (main thread only - not available in service workers).
9
- * @returns The device ID string (UUID v4 format)
10
- * @example
11
- * const deviceId = getDeviceId();
12
- * // Use deviceId in subscription requests to identify this device
13
- */
14
- export declare function getDeviceId(): string;
15
- /**
16
- * Clears the device ID from localStorage.
17
- * Use for testing or when user logs out and wants a fresh device identity.
18
- * @example
19
- * // On user logout
20
- * await unsubscribeFromPush();
21
- * clearDeviceId();
22
- */
23
- export declare function clearDeviceId(): void;
@@ -1,49 +0,0 @@
1
- "use strict";
2
- /**
3
- * Device ID utilities for subscription deduplication
4
- * Prevents duplicate subscriptions when service worker re-registers
5
- */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.getDeviceId = getDeviceId;
8
- exports.clearDeviceId = clearDeviceId;
9
- const DEVICE_ID_KEY = 'push_device_id';
10
- /**
11
- * Generates a cryptographically secure UUID v4.
12
- * Uses Web Crypto API for secure random number generation.
13
- * @returns A UUID v4 string (e.g., '550e8400-e29b-41d4-a716-446655440000')
14
- */
15
- function generateUUID() {
16
- const bytes = crypto.getRandomValues(new Uint8Array(16));
17
- bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
18
- bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1
19
- const hex = [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');
20
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
21
- }
22
- /**
23
- * Gets or creates a persistent device ID for subscription deduplication.
24
- * The ID is stored in localStorage and persists across sessions.
25
- * Uses localStorage (main thread only - not available in service workers).
26
- * @returns The device ID string (UUID v4 format)
27
- * @example
28
- * const deviceId = getDeviceId();
29
- * // Use deviceId in subscription requests to identify this device
30
- */
31
- function getDeviceId() {
32
- let deviceId = localStorage.getItem(DEVICE_ID_KEY);
33
- if (!deviceId) {
34
- deviceId = generateUUID();
35
- localStorage.setItem(DEVICE_ID_KEY, deviceId);
36
- }
37
- return deviceId;
38
- }
39
- /**
40
- * Clears the device ID from localStorage.
41
- * Use for testing or when user logs out and wants a fresh device identity.
42
- * @example
43
- * // On user logout
44
- * await unsubscribeFromPush();
45
- * clearDeviceId();
46
- */
47
- function clearDeviceId() {
48
- localStorage.removeItem(DEVICE_ID_KEY);
49
- }
@@ -1,17 +0,0 @@
1
- /**
2
- * Encoding utilities for push notification client
3
- */
4
- /**
5
- * Converts a base64 URL-encoded string to a Uint8Array.
6
- * Used to convert VAPID public keys for the PushManager API.
7
- * Works in both main thread and service worker contexts.
8
- * @param base64String - The base64 URL-encoded string to convert
9
- * @returns A Uint8Array containing the decoded bytes
10
- * @example
11
- * const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
12
- * await registration.pushManager.subscribe({
13
- * userVisibleOnly: true,
14
- * applicationServerKey
15
- * });
16
- */
17
- export declare function urlBase64ToUint8Array(base64String: string): Uint8Array<ArrayBuffer>;
@@ -1,32 +0,0 @@
1
- "use strict";
2
- /**
3
- * Encoding utilities for push notification client
4
- */
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.urlBase64ToUint8Array = urlBase64ToUint8Array;
7
- /**
8
- * Converts a base64 URL-encoded string to a Uint8Array.
9
- * Used to convert VAPID public keys for the PushManager API.
10
- * Works in both main thread and service worker contexts.
11
- * @param base64String - The base64 URL-encoded string to convert
12
- * @returns A Uint8Array containing the decoded bytes
13
- * @example
14
- * const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
15
- * await registration.pushManager.subscribe({
16
- * userVisibleOnly: true,
17
- * applicationServerKey
18
- * });
19
- */
20
- function urlBase64ToUint8Array(base64String) {
21
- const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
22
- const base64 = (base64String + padding)
23
- .replace(/-/g, '+')
24
- .replace(/_/g, '/');
25
- const rawData = atob(base64);
26
- const buffer = new ArrayBuffer(rawData.length);
27
- const outputArray = new Uint8Array(buffer);
28
- for (let i = 0; i < rawData.length; ++i) {
29
- outputArray[i] = rawData.charCodeAt(i);
30
- }
31
- return outputArray;
32
- }
@@ -1,4 +0,0 @@
1
- export { getDeviceId, clearDeviceId } from './deviceId';
2
- export { saveSubscriptionData, getSubscriptionData, clearSubscriptionData } from './indexedDb';
3
- export { getPermissionState, requestPermission, isPushSupported, toPushSubscription, subscribeToPush, unsubscribeFromPush, getCurrentSubscription } from './subscribe';
4
- export { handleSubscriptionChange } from './renewal';
@@ -1,20 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.handleSubscriptionChange = exports.getCurrentSubscription = exports.unsubscribeFromPush = exports.subscribeToPush = exports.toPushSubscription = exports.isPushSupported = exports.requestPermission = exports.getPermissionState = exports.clearSubscriptionData = exports.getSubscriptionData = exports.saveSubscriptionData = exports.clearDeviceId = exports.getDeviceId = void 0;
4
- var deviceId_1 = require("./deviceId");
5
- Object.defineProperty(exports, "getDeviceId", { enumerable: true, get: function () { return deviceId_1.getDeviceId; } });
6
- Object.defineProperty(exports, "clearDeviceId", { enumerable: true, get: function () { return deviceId_1.clearDeviceId; } });
7
- var indexedDb_1 = require("./indexedDb");
8
- Object.defineProperty(exports, "saveSubscriptionData", { enumerable: true, get: function () { return indexedDb_1.saveSubscriptionData; } });
9
- Object.defineProperty(exports, "getSubscriptionData", { enumerable: true, get: function () { return indexedDb_1.getSubscriptionData; } });
10
- Object.defineProperty(exports, "clearSubscriptionData", { enumerable: true, get: function () { return indexedDb_1.clearSubscriptionData; } });
11
- var subscribe_1 = require("./subscribe");
12
- Object.defineProperty(exports, "getPermissionState", { enumerable: true, get: function () { return subscribe_1.getPermissionState; } });
13
- Object.defineProperty(exports, "requestPermission", { enumerable: true, get: function () { return subscribe_1.requestPermission; } });
14
- Object.defineProperty(exports, "isPushSupported", { enumerable: true, get: function () { return subscribe_1.isPushSupported; } });
15
- Object.defineProperty(exports, "toPushSubscription", { enumerable: true, get: function () { return subscribe_1.toPushSubscription; } });
16
- Object.defineProperty(exports, "subscribeToPush", { enumerable: true, get: function () { return subscribe_1.subscribeToPush; } });
17
- Object.defineProperty(exports, "unsubscribeFromPush", { enumerable: true, get: function () { return subscribe_1.unsubscribeFromPush; } });
18
- Object.defineProperty(exports, "getCurrentSubscription", { enumerable: true, get: function () { return subscribe_1.getCurrentSubscription; } });
19
- var renewal_1 = require("./renewal");
20
- Object.defineProperty(exports, "handleSubscriptionChange", { enumerable: true, get: function () { return renewal_1.handleSubscriptionChange; } });
@@ -1,38 +0,0 @@
1
- /**
2
- * IndexedDB utilities for service worker data persistence
3
- * Service workers cannot access localStorage, so we use IndexedDB
4
- */
5
- import { SubscriptionData } from '../shared';
6
- /**
7
- * Saves subscription data to IndexedDB for service worker access.
8
- * This data is used for subscription renewal when the browser rotates keys.
9
- * @param data - The subscription data to persist (VAPID key, deviceId, basePath)
10
- * @returns Promise that resolves when data is saved
11
- * @example
12
- * await saveSubscriptionData({
13
- * publicKey: vapidPublicKey,
14
- * deviceId: getDeviceId(),
15
- * basePath: '/app'
16
- * });
17
- */
18
- export declare function saveSubscriptionData(data: SubscriptionData): Promise<void>;
19
- /**
20
- * Retrieves subscription data from IndexedDB.
21
- * Used by service workers during subscription renewal.
22
- * @returns The stored subscription data, or null if not found
23
- * @example
24
- * const data = await getSubscriptionData();
25
- * if (data) {
26
- * console.log('Device ID:', data.deviceId);
27
- * }
28
- */
29
- export declare function getSubscriptionData(): Promise<SubscriptionData | null>;
30
- /**
31
- * Clears subscription data from IndexedDB.
32
- * Call when unsubscribing or during cleanup.
33
- * @returns Promise that resolves when data is cleared
34
- * @example
35
- * await unsubscribeFromPush();
36
- * await clearSubscriptionData();
37
- */
38
- export declare function clearSubscriptionData(): Promise<void>;
@@ -1,89 +0,0 @@
1
- "use strict";
2
- /**
3
- * IndexedDB utilities for service worker data persistence
4
- * Service workers cannot access localStorage, so we use IndexedDB
5
- */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.saveSubscriptionData = saveSubscriptionData;
8
- exports.getSubscriptionData = getSubscriptionData;
9
- exports.clearSubscriptionData = clearSubscriptionData;
10
- const DB_NAME = 'PushSubscriptionDB';
11
- const STORE_NAME = 'subscriptionData';
12
- const DATA_KEY = 'current';
13
- /**
14
- * Opens the IndexedDB database, creating the object store if needed.
15
- * Internal helper for subscription data persistence.
16
- * @returns Promise resolving to the opened database
17
- */
18
- function openDatabase() {
19
- return new Promise((resolve, reject) => {
20
- const request = indexedDB.open(DB_NAME, 1);
21
- request.onerror = () => reject(request.error);
22
- request.onsuccess = () => resolve(request.result);
23
- request.onupgradeneeded = (event) => {
24
- const db = event.target.result;
25
- if (!db.objectStoreNames.contains(STORE_NAME)) {
26
- db.createObjectStore(STORE_NAME);
27
- }
28
- };
29
- });
30
- }
31
- /**
32
- * Executes an IndexedDB operation within a transaction.
33
- * Handles database opening, transaction management, and cleanup.
34
- * @typeParam T - The expected result type from the operation
35
- * @param mode - Transaction mode ('readonly' or 'readwrite')
36
- * @param operation - Function that performs the IndexedDB operation
37
- * @returns Promise resolving to the operation result
38
- */
39
- async function withTransaction(mode, operation) {
40
- const db = await openDatabase();
41
- return new Promise((resolve, reject) => {
42
- const transaction = db.transaction(STORE_NAME, mode);
43
- const store = transaction.objectStore(STORE_NAME);
44
- const request = operation(store);
45
- request.onerror = () => reject(request.error);
46
- request.onsuccess = () => resolve(request.result);
47
- transaction.oncomplete = () => db.close();
48
- });
49
- }
50
- /**
51
- * Saves subscription data to IndexedDB for service worker access.
52
- * This data is used for subscription renewal when the browser rotates keys.
53
- * @param data - The subscription data to persist (VAPID key, deviceId, basePath)
54
- * @returns Promise that resolves when data is saved
55
- * @example
56
- * await saveSubscriptionData({
57
- * publicKey: vapidPublicKey,
58
- * deviceId: getDeviceId(),
59
- * basePath: '/app'
60
- * });
61
- */
62
- function saveSubscriptionData(data) {
63
- return withTransaction('readwrite', (store) => store.put(data, DATA_KEY));
64
- }
65
- /**
66
- * Retrieves subscription data from IndexedDB.
67
- * Used by service workers during subscription renewal.
68
- * @returns The stored subscription data, or null if not found
69
- * @example
70
- * const data = await getSubscriptionData();
71
- * if (data) {
72
- * console.log('Device ID:', data.deviceId);
73
- * }
74
- */
75
- async function getSubscriptionData() {
76
- const result = await withTransaction('readonly', (store) => store.get(DATA_KEY));
77
- return result || null;
78
- }
79
- /**
80
- * Clears subscription data from IndexedDB.
81
- * Call when unsubscribing or during cleanup.
82
- * @returns Promise that resolves when data is cleared
83
- * @example
84
- * await unsubscribeFromPush();
85
- * await clearSubscriptionData();
86
- */
87
- function clearSubscriptionData() {
88
- return withTransaction('readwrite', (store) => store.delete(DATA_KEY));
89
- }
@@ -1,31 +0,0 @@
1
- /**
2
- * Push subscription renewal handler for service worker
3
- * Handles the 'pushsubscriptionchange' event when browser rotates subscriptions
4
- */
5
- import { Result } from '@markwharton/pwa-core/types';
6
- import { PushSubscription } from '../shared';
7
- /**
8
- * Handles the pushsubscriptionchange event in a service worker.
9
- * Call this from your service worker's event listener when the browser
10
- * rotates push subscriptions (typically for security reasons).
11
- * @param event - The pushsubscriptionchange event from the service worker
12
- * @param renewEndpoint - The backend endpoint to send the renewal request to
13
- * @returns Result with ok=true on success, or error message on failure
14
- * @example
15
- * // In your service worker (sw.js):
16
- * self.addEventListener('pushsubscriptionchange', (event) => {
17
- * event.waitUntil(handleSubscriptionChange(event, '/api/push/renew'));
18
- * });
19
- */
20
- export declare function handleSubscriptionChange(event: PushSubscriptionChangeEvent, renewEndpoint: string): Promise<Result<void>>;
21
- /**
22
- * Type definition for the pushsubscriptionchange event.
23
- * Fired when the browser rotates push subscriptions.
24
- */
25
- interface PushSubscriptionChangeEvent extends ExtendableEvent {
26
- /** The old subscription that is being replaced (may be null) */
27
- oldSubscription?: PushSubscription;
28
- /** The new subscription (may be null if unsubscribed) */
29
- newSubscription?: PushSubscription;
30
- }
31
- export {};
@@ -1,80 +0,0 @@
1
- "use strict";
2
- /**
3
- * Push subscription renewal handler for service worker
4
- * Handles the 'pushsubscriptionchange' event when browser rotates subscriptions
5
- */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.handleSubscriptionChange = handleSubscriptionChange;
8
- const types_1 = require("@markwharton/pwa-core/types");
9
- const encoding_1 = require("./encoding");
10
- const indexedDb_1 = require("./indexedDb");
11
- const subscribe_1 = require("./subscribe");
12
- /**
13
- * Handles the pushsubscriptionchange event in a service worker.
14
- * Call this from your service worker's event listener when the browser
15
- * rotates push subscriptions (typically for security reasons).
16
- * @param event - The pushsubscriptionchange event from the service worker
17
- * @param renewEndpoint - The backend endpoint to send the renewal request to
18
- * @returns Result with ok=true on success, or error message on failure
19
- * @example
20
- * // In your service worker (sw.js):
21
- * self.addEventListener('pushsubscriptionchange', (event) => {
22
- * event.waitUntil(handleSubscriptionChange(event, '/api/push/renew'));
23
- * });
24
- */
25
- async function handleSubscriptionChange(event, renewEndpoint) {
26
- try {
27
- // Get stored data from IndexedDB
28
- const data = await (0, indexedDb_1.getSubscriptionData)();
29
- if (!data) {
30
- return (0, types_1.err)('No subscription data found for renewal');
31
- }
32
- // Resubscribe with same VAPID key
33
- const newSubscription = await resubscribe(data);
34
- if (!newSubscription) {
35
- return (0, types_1.err)('Failed to create new subscription');
36
- }
37
- // Build renewal request (uses SubscriptionRequest type)
38
- const renewalRequest = {
39
- subscription: newSubscription,
40
- deviceId: data.deviceId,
41
- basePath: data.basePath
42
- };
43
- // Send to backend
44
- const response = await fetch(renewEndpoint, {
45
- method: 'POST',
46
- headers: { 'Content-Type': 'application/json' },
47
- body: JSON.stringify(renewalRequest)
48
- });
49
- if (!response.ok) {
50
- return (0, types_1.err)(`Renewal request failed: ${response.status}`);
51
- }
52
- return (0, types_1.okVoid)();
53
- }
54
- catch (error) {
55
- const message = error instanceof Error ? error.message : 'Subscription renewal failed';
56
- return (0, types_1.err)(message);
57
- }
58
- }
59
- /**
60
- * Resubscribes to push notifications using stored VAPID key.
61
- * Internal helper for subscription renewal in service worker context.
62
- * @param data - The stored subscription data containing VAPID key
63
- * @returns The new PushSubscription, or null on failure
64
- */
65
- async function resubscribe(data) {
66
- try {
67
- const self = globalThis;
68
- const registration = self.registration;
69
- const applicationServerKey = (0, encoding_1.urlBase64ToUint8Array)(data.publicKey);
70
- const subscription = await registration.pushManager.subscribe({
71
- userVisibleOnly: true,
72
- applicationServerKey
73
- });
74
- return (0, subscribe_1.toPushSubscription)(subscription);
75
- }
76
- catch (error) {
77
- console.error('Resubscribe failed:', error);
78
- return null;
79
- }
80
- }