@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.
- package/dist/__tests__/client/deviceId.test.d.ts +1 -0
- package/dist/__tests__/client/deviceId.test.js +134 -0
- package/dist/__tests__/client/encoding.test.d.ts +1 -0
- package/dist/__tests__/client/encoding.test.js +89 -0
- package/dist/__tests__/client/indexedDb.test.d.ts +1 -0
- package/dist/__tests__/client/indexedDb.test.js +195 -0
- package/dist/__tests__/client/renewal.test.d.ts +1 -0
- package/dist/__tests__/client/renewal.test.js +171 -0
- package/dist/__tests__/client/subscribe.test.d.ts +1 -0
- package/dist/__tests__/client/subscribe.test.js +268 -0
- package/dist/__tests__/server/send.test.d.ts +1 -0
- package/dist/__tests__/server/send.test.js +217 -0
- package/dist/client/deviceId.d.ts +13 -3
- package/dist/client/deviceId.js +16 -4
- package/dist/client/encoding.d.ts +11 -3
- package/dist/client/encoding.js +11 -3
- package/dist/client/indexedDb.d.ts +24 -3
- package/dist/client/indexedDb.js +33 -5
- package/dist/client/renewal.d.ts +13 -8
- package/dist/client/renewal.js +13 -8
- package/dist/client/subscribe.d.ts +67 -18
- package/dist/client/subscribe.js +76 -30
- package/dist/pwa-push-sw.js +172 -0
- package/dist/server/send.d.ts +64 -16
- package/dist/server/send.js +63 -19
- package/dist/sw.d.ts +14 -0
- package/dist/sw.js +18 -0
- package/dist/types.d.ts +1 -18
- package/dist/types.js +6 -0
- package/package.json +9 -4
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
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;
|
package/dist/client/deviceId.js
CHANGED
|
@@ -8,7 +8,9 @@ exports.getDeviceId = getDeviceId;
|
|
|
8
8
|
exports.clearDeviceId = clearDeviceId;
|
|
9
9
|
const DEVICE_ID_KEY = 'push_device_id';
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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>;
|
package/dist/client/encoding.js
CHANGED
|
@@ -5,9 +5,17 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.urlBase64ToUint8Array = urlBase64ToUint8Array;
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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>;
|