@markwharton/pwa-push 1.1.0 → 1.2.1

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.
@@ -0,0 +1,268 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ // Mock factory for client dependencies (needed after vi.resetModules)
38
+ function mockClientDependencies() {
39
+ vitest_1.vi.mock('../../client/deviceId', () => ({
40
+ getDeviceId: vitest_1.vi.fn(() => 'mock-device-id')
41
+ }));
42
+ vitest_1.vi.mock('../../client/indexedDb', () => ({
43
+ saveSubscriptionData: vitest_1.vi.fn(() => Promise.resolve())
44
+ }));
45
+ vitest_1.vi.mock('../../client/encoding', () => ({
46
+ urlBase64ToUint8Array: vitest_1.vi.fn(() => new Uint8Array([1, 2, 3]))
47
+ }));
48
+ }
49
+ // Initial mock setup (hoisted by Vitest)
50
+ mockClientDependencies();
51
+ // Setup browser mocks
52
+ const mockSubscription = {
53
+ endpoint: 'https://fcm.googleapis.com/test',
54
+ toJSON: () => ({
55
+ endpoint: 'https://fcm.googleapis.com/test',
56
+ keys: {
57
+ p256dh: 'test-p256dh',
58
+ auth: 'test-auth'
59
+ }
60
+ }),
61
+ unsubscribe: vitest_1.vi.fn(() => Promise.resolve(true))
62
+ };
63
+ const mockPushManager = {
64
+ subscribe: vitest_1.vi.fn(() => Promise.resolve(mockSubscription)),
65
+ getSubscription: vitest_1.vi.fn(() => Promise.resolve(mockSubscription))
66
+ };
67
+ const mockRegistration = {
68
+ pushManager: mockPushManager
69
+ };
70
+ // Helper to setup navigator with serviceWorker
71
+ function setupNavigatorWithServiceWorker() {
72
+ Object.defineProperty(global, 'navigator', {
73
+ value: {
74
+ serviceWorker: {
75
+ ready: Promise.resolve(mockRegistration)
76
+ }
77
+ },
78
+ writable: true,
79
+ configurable: true
80
+ });
81
+ }
82
+ // Helper to setup empty navigator (no serviceWorker)
83
+ function setupEmptyNavigator() {
84
+ Object.defineProperty(global, 'navigator', {
85
+ value: {},
86
+ writable: true,
87
+ configurable: true
88
+ });
89
+ }
90
+ (0, vitest_1.beforeEach)(() => {
91
+ vitest_1.vi.clearAllMocks();
92
+ // Mock Notification
93
+ global.Notification = {
94
+ permission: 'default',
95
+ requestPermission: vitest_1.vi.fn(() => Promise.resolve('granted'))
96
+ };
97
+ setupNavigatorWithServiceWorker();
98
+ // Mock window.PushManager
99
+ global.window = {
100
+ PushManager: {}
101
+ };
102
+ });
103
+ (0, vitest_1.describe)('Push subscription client', () => {
104
+ (0, vitest_1.describe)('getPermissionState', () => {
105
+ (0, vitest_1.it)('returns current notification permission', async () => {
106
+ global.Notification.permission = 'granted';
107
+ const { getPermissionState } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
108
+ (0, vitest_1.expect)(getPermissionState()).toBe('granted');
109
+ });
110
+ (0, vitest_1.it)('returns denied permission', async () => {
111
+ global.Notification.permission = 'denied';
112
+ const { getPermissionState } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
113
+ (0, vitest_1.expect)(getPermissionState()).toBe('denied');
114
+ });
115
+ (0, vitest_1.it)('returns default permission', async () => {
116
+ global.Notification.permission = 'default';
117
+ const { getPermissionState } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
118
+ (0, vitest_1.expect)(getPermissionState()).toBe('default');
119
+ });
120
+ });
121
+ (0, vitest_1.describe)('requestPermission', () => {
122
+ (0, vitest_1.it)('requests permission and returns result', async () => {
123
+ global.Notification.requestPermission.mockResolvedValue('granted');
124
+ const { requestPermission } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
125
+ const result = await requestPermission();
126
+ (0, vitest_1.expect)(result).toBe('granted');
127
+ (0, vitest_1.expect)(global.Notification.requestPermission).toHaveBeenCalled();
128
+ });
129
+ (0, vitest_1.it)('returns denied when user denies', async () => {
130
+ global.Notification.requestPermission.mockResolvedValue('denied');
131
+ const { requestPermission } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
132
+ const result = await requestPermission();
133
+ (0, vitest_1.expect)(result).toBe('denied');
134
+ });
135
+ });
136
+ (0, vitest_1.describe)('isPushSupported', () => {
137
+ (0, vitest_1.it)('returns true when serviceWorker and PushManager available', async () => {
138
+ const { isPushSupported } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
139
+ (0, vitest_1.expect)(isPushSupported()).toBe(true);
140
+ });
141
+ (0, vitest_1.it)('returns false when serviceWorker missing', async () => {
142
+ setupEmptyNavigator();
143
+ vitest_1.vi.resetModules();
144
+ const { isPushSupported } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
145
+ (0, vitest_1.expect)(isPushSupported()).toBe(false);
146
+ });
147
+ (0, vitest_1.it)('returns false when PushManager missing', async () => {
148
+ delete global.window.PushManager;
149
+ vitest_1.vi.resetModules();
150
+ const { isPushSupported } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
151
+ (0, vitest_1.expect)(isPushSupported()).toBe(false);
152
+ });
153
+ });
154
+ (0, vitest_1.describe)('toPushSubscription', () => {
155
+ (0, vitest_1.it)('converts browser subscription to internal format', async () => {
156
+ const { toPushSubscription } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
157
+ const result = toPushSubscription(mockSubscription);
158
+ (0, vitest_1.expect)(result.endpoint).toBe('https://fcm.googleapis.com/test');
159
+ (0, vitest_1.expect)(result.keys.p256dh).toBe('test-p256dh');
160
+ (0, vitest_1.expect)(result.keys.auth).toBe('test-auth');
161
+ });
162
+ (0, vitest_1.it)('handles missing keys', async () => {
163
+ const { toPushSubscription } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
164
+ const subWithoutKeys = {
165
+ endpoint: 'https://example.com',
166
+ toJSON: () => ({ endpoint: 'https://example.com', keys: undefined })
167
+ };
168
+ const result = toPushSubscription(subWithoutKeys);
169
+ (0, vitest_1.expect)(result.keys.p256dh).toBe('');
170
+ (0, vitest_1.expect)(result.keys.auth).toBe('');
171
+ });
172
+ });
173
+ (0, vitest_1.describe)('subscribeToPush', () => {
174
+ (0, vitest_1.it)('returns error when push not supported', async () => {
175
+ setupEmptyNavigator();
176
+ vitest_1.vi.resetModules();
177
+ mockClientDependencies();
178
+ const { subscribeToPush } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
179
+ const result = await subscribeToPush('vapid-key');
180
+ (0, vitest_1.expect)(result.ok).toBe(false);
181
+ (0, vitest_1.expect)(result.error).toBe('Push notifications not supported');
182
+ });
183
+ (0, vitest_1.it)('returns error when permission denied', async () => {
184
+ global.Notification.requestPermission.mockResolvedValue('denied');
185
+ const { subscribeToPush } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
186
+ const result = await subscribeToPush('vapid-key');
187
+ (0, vitest_1.expect)(result.ok).toBe(false);
188
+ (0, vitest_1.expect)(result.error).toBe('Push permission denied');
189
+ });
190
+ (0, vitest_1.it)('returns subscription request on success', async () => {
191
+ global.Notification.requestPermission.mockResolvedValue('granted');
192
+ const { subscribeToPush } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
193
+ const result = await subscribeToPush('vapid-key', '/app');
194
+ (0, vitest_1.expect)(result.ok).toBe(true);
195
+ (0, vitest_1.expect)(result.data?.deviceId).toBe('mock-device-id');
196
+ (0, vitest_1.expect)(result.data?.basePath).toBe('/app');
197
+ (0, vitest_1.expect)(result.data?.subscription.endpoint).toBe('https://fcm.googleapis.com/test');
198
+ });
199
+ (0, vitest_1.it)('saves subscription data to IndexedDB', async () => {
200
+ global.Notification.requestPermission.mockResolvedValue('granted');
201
+ const { saveSubscriptionData } = await Promise.resolve().then(() => __importStar(require('../../client/indexedDb')));
202
+ const { subscribeToPush } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
203
+ await subscribeToPush('vapid-key', '/myapp');
204
+ (0, vitest_1.expect)(saveSubscriptionData).toHaveBeenCalledWith({
205
+ publicKey: 'vapid-key',
206
+ deviceId: 'mock-device-id',
207
+ basePath: '/myapp'
208
+ });
209
+ });
210
+ (0, vitest_1.it)('uses default basePath of "/"', async () => {
211
+ global.Notification.requestPermission.mockResolvedValue('granted');
212
+ const { subscribeToPush } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
213
+ const result = await subscribeToPush('vapid-key');
214
+ (0, vitest_1.expect)(result.data?.basePath).toBe('/');
215
+ });
216
+ (0, vitest_1.it)('handles subscription error', async () => {
217
+ global.Notification.requestPermission.mockResolvedValue('granted');
218
+ mockPushManager.subscribe.mockRejectedValueOnce(new Error('Network error'));
219
+ const { subscribeToPush } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
220
+ const result = await subscribeToPush('vapid-key');
221
+ (0, vitest_1.expect)(result.ok).toBe(false);
222
+ (0, vitest_1.expect)(result.error).toBe('Network error');
223
+ });
224
+ });
225
+ (0, vitest_1.describe)('unsubscribeFromPush', () => {
226
+ (0, vitest_1.it)('unsubscribes successfully', async () => {
227
+ const { unsubscribeFromPush } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
228
+ const result = await unsubscribeFromPush();
229
+ (0, vitest_1.expect)(result.ok).toBe(true);
230
+ (0, vitest_1.expect)(mockSubscription.unsubscribe).toHaveBeenCalled();
231
+ });
232
+ (0, vitest_1.it)('succeeds when no subscription exists', async () => {
233
+ mockPushManager.getSubscription.mockResolvedValueOnce(null);
234
+ const { unsubscribeFromPush } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
235
+ const result = await unsubscribeFromPush();
236
+ (0, vitest_1.expect)(result.ok).toBe(true);
237
+ });
238
+ (0, vitest_1.it)('handles unsubscribe error', async () => {
239
+ mockPushManager.getSubscription.mockRejectedValueOnce(new Error('Failed'));
240
+ const { unsubscribeFromPush } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
241
+ const result = await unsubscribeFromPush();
242
+ (0, vitest_1.expect)(result.ok).toBe(false);
243
+ (0, vitest_1.expect)(result.error).toBe('Failed');
244
+ });
245
+ });
246
+ (0, vitest_1.describe)('getCurrentSubscription', () => {
247
+ (0, vitest_1.it)('returns current subscription', async () => {
248
+ const { getCurrentSubscription } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
249
+ const result = await getCurrentSubscription();
250
+ (0, vitest_1.expect)(result.ok).toBe(true);
251
+ (0, vitest_1.expect)(result.data?.endpoint).toBe('https://fcm.googleapis.com/test');
252
+ });
253
+ (0, vitest_1.it)('returns error when no subscription', async () => {
254
+ mockPushManager.getSubscription.mockResolvedValueOnce(null);
255
+ const { getCurrentSubscription } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
256
+ const result = await getCurrentSubscription();
257
+ (0, vitest_1.expect)(result.ok).toBe(false);
258
+ (0, vitest_1.expect)(result.error).toBe('No active subscription');
259
+ });
260
+ (0, vitest_1.it)('handles error', async () => {
261
+ mockPushManager.getSubscription.mockRejectedValueOnce(new Error('Service worker error'));
262
+ const { getCurrentSubscription } = await Promise.resolve().then(() => __importStar(require('../../client/subscribe')));
263
+ const result = await getCurrentSubscription();
264
+ (0, vitest_1.expect)(result.ok).toBe(false);
265
+ (0, vitest_1.expect)(result.error).toBe('Service worker error');
266
+ });
267
+ });
268
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,217 @@
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
+ });
140
+ (0, vitest_1.describe)('sendPushToAll', () => {
141
+ const subscriptions = [
142
+ { endpoint: 'https://example.com/1', keys: { p256dh: 'k1', auth: 'a1' } },
143
+ { endpoint: 'https://example.com/2', keys: { p256dh: 'k2', auth: 'a2' } }
144
+ ];
145
+ const payload = { title: 'Broadcast', body: 'Hello all' };
146
+ (0, vitest_1.it)('sends to all subscriptions', async () => {
147
+ vitest_1.vi.mocked(web_push_1.default.sendNotification).mockResolvedValue({});
148
+ const { initPush, sendPushToAll } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
149
+ initPush(validConfig);
150
+ const results = await sendPushToAll(subscriptions, payload);
151
+ (0, vitest_1.expect)(results).toHaveLength(2);
152
+ (0, vitest_1.expect)(results.every(r => r.ok)).toBe(true);
153
+ });
154
+ (0, vitest_1.it)('handles mixed success/failure', async () => {
155
+ vitest_1.vi.mocked(web_push_1.default.sendNotification)
156
+ .mockResolvedValueOnce({})
157
+ .mockRejectedValueOnce({ statusCode: 410, message: 'Gone' });
158
+ const { initPush, sendPushToAll } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
159
+ initPush(validConfig);
160
+ const results = await sendPushToAll(subscriptions, payload);
161
+ (0, vitest_1.expect)(results[0].ok).toBe(true);
162
+ (0, vitest_1.expect)(results[1].ok).toBe(false);
163
+ });
164
+ });
165
+ (0, vitest_1.describe)('sendPushWithDetails', () => {
166
+ const subscription = {
167
+ endpoint: 'https://fcm.googleapis.com/fcm/send/abc123',
168
+ keys: { p256dh: 'testkey', auth: 'testauthkey' },
169
+ basePath: '/app'
170
+ };
171
+ (0, vitest_1.it)('injects basePath from subscription', async () => {
172
+ vitest_1.vi.mocked(web_push_1.default.sendNotification).mockResolvedValue({});
173
+ const { initPush, sendPushWithDetails } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
174
+ initPush(validConfig);
175
+ await sendPushWithDetails(subscription, { title: 'Test', body: 'Hello' });
176
+ (0, vitest_1.expect)(web_push_1.default.sendNotification).toHaveBeenCalledWith(subscription, vitest_1.expect.stringContaining('"basePath":"/app"'));
177
+ });
178
+ (0, vitest_1.it)('uses payload basePath over subscription basePath', async () => {
179
+ vitest_1.vi.mocked(web_push_1.default.sendNotification).mockResolvedValue({});
180
+ const { initPush, sendPushWithDetails } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
181
+ initPush(validConfig);
182
+ await sendPushWithDetails(subscription, {
183
+ title: 'Test',
184
+ body: 'Hello',
185
+ basePath: '/override'
186
+ });
187
+ (0, vitest_1.expect)(web_push_1.default.sendNotification).toHaveBeenCalledWith(subscription, vitest_1.expect.stringContaining('"basePath":"/override"'));
188
+ });
189
+ (0, vitest_1.it)('defaults basePath to "/" when not specified', async () => {
190
+ vitest_1.vi.mocked(web_push_1.default.sendNotification).mockResolvedValue({});
191
+ const { initPush, sendPushWithDetails } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
192
+ initPush(validConfig);
193
+ const subWithoutBasePath = {
194
+ endpoint: 'https://example.com',
195
+ keys: { p256dh: 'k', auth: 'a' }
196
+ };
197
+ await sendPushWithDetails(subWithoutBasePath, { title: 'Test', body: 'Hello' });
198
+ (0, vitest_1.expect)(web_push_1.default.sendNotification).toHaveBeenCalledWith(subWithoutBasePath, vitest_1.expect.stringContaining('"basePath":"/"'));
199
+ });
200
+ });
201
+ (0, vitest_1.describe)('isSubscriptionExpired', () => {
202
+ (0, vitest_1.it)('returns true for 410 status', async () => {
203
+ const { isSubscriptionExpired } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
204
+ (0, vitest_1.expect)(isSubscriptionExpired(410)).toBe(true);
205
+ });
206
+ (0, vitest_1.it)('returns false for other statuses', async () => {
207
+ const { isSubscriptionExpired } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
208
+ (0, vitest_1.expect)(isSubscriptionExpired(400)).toBe(false);
209
+ (0, vitest_1.expect)(isSubscriptionExpired(401)).toBe(false);
210
+ (0, vitest_1.expect)(isSubscriptionExpired(500)).toBe(false);
211
+ });
212
+ (0, vitest_1.it)('returns false for undefined', async () => {
213
+ const { isSubscriptionExpired } = await Promise.resolve().then(() => __importStar(require('../../server/send')));
214
+ (0, vitest_1.expect)(isSubscriptionExpired(undefined)).toBe(false);
215
+ });
216
+ });
217
+ });
@@ -3,11 +3,21 @@
3
3
  * Prevents duplicate subscriptions when service worker re-registers
4
4
  */
5
5
  /**
6
- * Get or create a persistent device ID
7
- * Uses localStorage (main thread only)
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
8
13
  */
9
14
  export declare function getDeviceId(): string;
10
15
  /**
11
- * Clear the device ID (for testing or logout)
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();
12
22
  */
13
23
  export declare function clearDeviceId(): void;
@@ -8,7 +8,9 @@ exports.getDeviceId = getDeviceId;
8
8
  exports.clearDeviceId = clearDeviceId;
9
9
  const DEVICE_ID_KEY = 'push_device_id';
10
10
  /**
11
- * Generate a cryptographically secure UUID v4
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')
12
14
  */
13
15
  function generateUUID() {
14
16
  const bytes = crypto.getRandomValues(new Uint8Array(16));
@@ -18,8 +20,13 @@ function generateUUID() {
18
20
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
19
21
  }
20
22
  /**
21
- * Get or create a persistent device ID
22
- * Uses localStorage (main thread only)
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
23
30
  */
24
31
  function getDeviceId() {
25
32
  let deviceId = localStorage.getItem(DEVICE_ID_KEY);
@@ -30,7 +37,12 @@ function getDeviceId() {
30
37
  return deviceId;
31
38
  }
32
39
  /**
33
- * Clear the device ID (for testing or logout)
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();
34
46
  */
35
47
  function clearDeviceId() {
36
48
  localStorage.removeItem(DEVICE_ID_KEY);
@@ -2,8 +2,16 @@
2
2
  * Encoding utilities for push notification client
3
3
  */
4
4
  /**
5
- * Convert base64 URL-encoded string to Uint8Array
6
- * Used to convert VAPID public keys for the PushManager API
7
- * Works in both main thread and service worker contexts
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
+ * });
8
16
  */
9
17
  export declare function urlBase64ToUint8Array(base64String: string): Uint8Array<ArrayBuffer>;
@@ -5,9 +5,17 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.urlBase64ToUint8Array = urlBase64ToUint8Array;
7
7
  /**
8
- * Convert base64 URL-encoded string to Uint8Array
9
- * Used to convert VAPID public keys for the PushManager API
10
- * Works in both main thread and service worker contexts
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
+ * });
11
19
  */
12
20
  function urlBase64ToUint8Array(base64String) {
13
21
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
@@ -4,14 +4,35 @@
4
4
  */
5
5
  import { SubscriptionData } from '../types';
6
6
  /**
7
- * Save subscription data to IndexedDB (for service worker access)
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
+ * });
8
17
  */
9
18
  export declare function saveSubscriptionData(data: SubscriptionData): Promise<void>;
10
19
  /**
11
- * Get subscription data from IndexedDB
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
+ * }
12
28
  */
13
29
  export declare function getSubscriptionData(): Promise<SubscriptionData | null>;
14
30
  /**
15
- * Clear subscription data from IndexedDB
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();
16
37
  */
17
38
  export declare function clearSubscriptionData(): Promise<void>;