@tencentcloud/web-push 1.0.2 → 1.0.3
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/README.md +102 -175
- package/dist/index.d.ts +259 -0
- package/{index.esm.js → dist/index.esm.js} +1118 -158
- package/dist/index.umd.js +1 -0
- package/dist/src/components/message-popup.d.ts +63 -0
- package/{src → dist/src}/core/web-push-sdk.d.ts +23 -1
- package/{src → dist/src}/index.d.ts +1 -0
- package/{src → dist/src}/types/inner.d.ts +2 -10
- package/dist/src/types/outer.d.ts +120 -0
- package/{src → dist/src}/utils/logger.d.ts +6 -0
- package/{src → dist/src}/utils/validator.d.ts +0 -13
- package/dist/sw.js +1 -0
- package/package.json +47 -9
- package/src/__tests__/index.test.ts +120 -0
- package/src/__tests__/integration.test.ts +285 -0
- package/src/__tests__/setup.ts +210 -0
- package/src/__tests__/types.test.ts +303 -0
- package/src/__tests__/web-push-sdk.test.ts +257 -0
- package/src/components/message-popup.ts +1007 -0
- package/src/core/event-emitter.ts +61 -0
- package/src/core/service-worker-manager.ts +614 -0
- package/src/core/web-push-sdk.ts +690 -0
- package/src/debug/GenerateTestUserSig.js +37 -0
- package/src/debug/index.d.ts +6 -0
- package/src/debug/index.js +1 -0
- package/src/debug/lib-generate-test-usersig-es.min.js +2 -0
- package/src/index.ts +9 -0
- package/src/service-worker/sw.ts +494 -0
- package/src/types/index.ts +2 -0
- package/src/types/inner.ts +44 -0
- package/src/types/outer.ts +142 -0
- package/src/utils/browser-support.ts +412 -0
- package/src/utils/logger.ts +66 -0
- package/src/utils/storage.ts +51 -0
- package/src/utils/validator.ts +267 -0
- package/CHANGELOG.md +0 -55
- package/index.d.ts +0 -110
- package/index.umd.js +0 -1
- package/src/types/outer.d.ts +0 -66
- package/sw.js +0 -1
- /package/{src → dist/src}/core/event-emitter.d.ts +0 -0
- /package/{src → dist/src}/core/service-worker-manager.d.ts +0 -0
- /package/{src → dist/src}/service-worker/sw.d.ts +0 -0
- /package/{src → dist/src}/types/index.d.ts +0 -0
- /package/{src → dist/src}/utils/browser-support.d.ts +0 -0
- /package/{src → dist/src}/utils/storage.d.ts +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { EVENT } from '../types';
|
|
2
|
+
import { logger } from '../utils/logger';
|
|
3
|
+
|
|
4
|
+
export class EventEmitter {
|
|
5
|
+
private events: Map<EVENT, Map<string, Function>> = new Map();
|
|
6
|
+
|
|
7
|
+
on(eventName: EVENT, listener: Function): string {
|
|
8
|
+
if (!this.events.has(eventName)) {
|
|
9
|
+
this.events.set(eventName, new Map());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const listenerId = Math.random().toString(36).substr(2, 9);
|
|
13
|
+
this.events.get(eventName)!.set(listenerId, listener);
|
|
14
|
+
|
|
15
|
+
return listenerId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
off(eventName: EVENT, listener: Function): boolean {
|
|
19
|
+
const eventMap = this.events.get(eventName);
|
|
20
|
+
if (!eventMap) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const [id, fn] of eventMap.entries()) {
|
|
25
|
+
if (fn === listener) {
|
|
26
|
+
eventMap.delete(id);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
emit(eventName: EVENT, ...args: any[]): void {
|
|
35
|
+
const eventMap = this.events.get(eventName);
|
|
36
|
+
if (!eventMap) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
eventMap.forEach((listener) => {
|
|
41
|
+
try {
|
|
42
|
+
listener(...args);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logger.error(`Event listener error for ${eventName}:`, error);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
removeAllListeners(eventName?: EVENT): void {
|
|
50
|
+
if (eventName) {
|
|
51
|
+
this.events.delete(eventName);
|
|
52
|
+
} else {
|
|
53
|
+
this.events.clear();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
listenerCount(eventName: EVENT): number {
|
|
58
|
+
const eventMap = this.events.get(eventName);
|
|
59
|
+
return eventMap ? eventMap.size : 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { ServiceWorkerMessage } from '../types';
|
|
2
|
+
import { logger } from '../utils/logger';
|
|
3
|
+
import { EventEmitter } from './event-emitter';
|
|
4
|
+
import { EVENT, SubscriptionInfo } from '../types';
|
|
5
|
+
import { Validator, ValidationError } from '../utils/validator';
|
|
6
|
+
import { getBrowserInfo } from '../utils/browser-support';
|
|
7
|
+
import { Storage } from '../utils/storage';
|
|
8
|
+
|
|
9
|
+
export class ServiceWorkerManager {
|
|
10
|
+
private static instance: ServiceWorkerManager;
|
|
11
|
+
private registration: ServiceWorkerRegistration | null = null;
|
|
12
|
+
private eventEmitter: EventEmitter;
|
|
13
|
+
private swUrl: string = '/sw.js';
|
|
14
|
+
|
|
15
|
+
private constructor(eventEmitter: EventEmitter) {
|
|
16
|
+
this.eventEmitter = eventEmitter;
|
|
17
|
+
this.setupMessageListener();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static getInstance(eventEmitter: EventEmitter): ServiceWorkerManager {
|
|
21
|
+
if (!ServiceWorkerManager.instance) {
|
|
22
|
+
ServiceWorkerManager.instance = new ServiceWorkerManager(eventEmitter);
|
|
23
|
+
}
|
|
24
|
+
return ServiceWorkerManager.instance;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async checkServiceWorkerExists(url: string): Promise<boolean> {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(url, { method: 'HEAD' });
|
|
30
|
+
return response.ok;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async autoDetectServiceWorkerUrl(): Promise<void> {
|
|
37
|
+
if (typeof window !== 'undefined') {
|
|
38
|
+
const currentPath = window.location.pathname;
|
|
39
|
+
const basePath = currentPath.endsWith('/')
|
|
40
|
+
? currentPath
|
|
41
|
+
: currentPath + '/';
|
|
42
|
+
|
|
43
|
+
if (basePath !== '/') {
|
|
44
|
+
this.swUrl = './sw.js';
|
|
45
|
+
logger.log(
|
|
46
|
+
'Detected subdirectory deployment, using relative path:',
|
|
47
|
+
this.swUrl
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
this.swUrl = '/sw.js';
|
|
51
|
+
logger.log(
|
|
52
|
+
'Detected root directory deployment, using absolute path:',
|
|
53
|
+
this.swUrl
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isAvailable = await this.checkServiceWorkerExists(this.swUrl);
|
|
58
|
+
if (!isAvailable) {
|
|
59
|
+
logger.warn(
|
|
60
|
+
'Default Service Worker path not available, will try other paths during registration'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setServiceWorkerUrl(url: string): void {
|
|
67
|
+
try {
|
|
68
|
+
Validator.validateServiceWorkerPath(url);
|
|
69
|
+
|
|
70
|
+
this.swUrl = url;
|
|
71
|
+
logger.log('Manually set Service Worker path:', url);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof ValidationError) {
|
|
74
|
+
logger.error(
|
|
75
|
+
'Set Service Worker path parameter validation failed',
|
|
76
|
+
error.message
|
|
77
|
+
);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
throw new Error('Failed to set Service Worker path');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async register(): Promise<ServiceWorkerRegistration> {
|
|
85
|
+
if (!('serviceWorker' in navigator)) {
|
|
86
|
+
throw new Error('Service Worker is not supported');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await this.autoDetectServiceWorkerUrl();
|
|
90
|
+
|
|
91
|
+
const possibleUrls = [
|
|
92
|
+
this.swUrl,
|
|
93
|
+
'./sw.js',
|
|
94
|
+
'/sw.js',
|
|
95
|
+
'/dist/sw.js',
|
|
96
|
+
'/assets/sw.js',
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const uniqueUrls = [...new Set(possibleUrls)];
|
|
100
|
+
let lastError: Error | null = null;
|
|
101
|
+
|
|
102
|
+
for (const url of uniqueUrls) {
|
|
103
|
+
try {
|
|
104
|
+
logger.log('Attempting to register Service Worker:', url);
|
|
105
|
+
this.registration = await navigator.serviceWorker.register(url);
|
|
106
|
+
|
|
107
|
+
this.swUrl = url;
|
|
108
|
+
logger.log('Service Worker registration successful', {
|
|
109
|
+
url,
|
|
110
|
+
registration: this.registration,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await this.waitForServiceWorkerReady();
|
|
114
|
+
|
|
115
|
+
return this.registration;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
lastError = error as Error;
|
|
118
|
+
logger.warn(`Service Worker registration failed (${url}):`, error);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
logger.error('All Service Worker paths failed to register', lastError);
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Service Worker registration failed: ${lastError?.message || 'Unknown error'}. Please ensure sw.js file exists in one of the following paths: ${uniqueUrls.join(', ')}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async waitForServiceWorkerReady(): Promise<void> {
|
|
130
|
+
if (!this.registration) {
|
|
131
|
+
throw new Error('Service Worker not registered');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
if (this.registration!.active) {
|
|
136
|
+
resolve();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const worker =
|
|
141
|
+
this.registration!.installing || this.registration!.waiting;
|
|
142
|
+
if (worker) {
|
|
143
|
+
worker.addEventListener('statechange', () => {
|
|
144
|
+
if (worker.state === 'activated') {
|
|
145
|
+
resolve();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getPushSubscription(vapidPublicKey: string): Promise<SubscriptionInfo> {
|
|
153
|
+
try {
|
|
154
|
+
Validator.validateValue('vapidPublicKey', vapidPublicKey, {
|
|
155
|
+
required: true,
|
|
156
|
+
type: 'string',
|
|
157
|
+
min: 1,
|
|
158
|
+
max: 512,
|
|
159
|
+
message: 'VAPID public key must be a valid string',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!this.registration) {
|
|
163
|
+
throw new Error('Service Worker not registered');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const storedVapidKey = this.getStoredVapidPublicKey();
|
|
167
|
+
|
|
168
|
+
let subscription = await this.registration.pushManager.getSubscription();
|
|
169
|
+
|
|
170
|
+
if (subscription && storedVapidKey !== vapidPublicKey) {
|
|
171
|
+
logger.log(
|
|
172
|
+
'VAPID public key changed, unsubscribing existing subscription'
|
|
173
|
+
);
|
|
174
|
+
await subscription.unsubscribe();
|
|
175
|
+
subscription = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!subscription) {
|
|
179
|
+
subscription = await this.registration.pushManager.subscribe({
|
|
180
|
+
userVisibleOnly: true,
|
|
181
|
+
applicationServerKey: this.urlBase64ToUint8Array(
|
|
182
|
+
vapidPublicKey
|
|
183
|
+
) as BufferSource,
|
|
184
|
+
});
|
|
185
|
+
this.setStoreVapidPublicKey(vapidPublicKey);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
logger.log('Get push subscription successful', subscription);
|
|
189
|
+
|
|
190
|
+
const subscriptionInfo = this.getSubscriptionInfo(subscription);
|
|
191
|
+
logger.log('Extracted subscription information:', {
|
|
192
|
+
token: subscriptionInfo.token,
|
|
193
|
+
auth: subscriptionInfo.auth,
|
|
194
|
+
p256dh: subscriptionInfo.p256dh,
|
|
195
|
+
endpoint: subscriptionInfo.endpoint,
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
subscription,
|
|
199
|
+
token: subscriptionInfo.token || '',
|
|
200
|
+
auth: subscriptionInfo.auth || '',
|
|
201
|
+
p256dh: subscriptionInfo.p256dh || '',
|
|
202
|
+
endpoint: subscriptionInfo.endpoint || '',
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof ValidationError) {
|
|
206
|
+
logger.error(
|
|
207
|
+
'Get push subscription parameter validation failed',
|
|
208
|
+
error.message
|
|
209
|
+
);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
logger.error('Get push subscription failed', error);
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private getSubscriptionInfo(subscription: PushSubscription): {
|
|
218
|
+
token: string | null;
|
|
219
|
+
auth: string | null;
|
|
220
|
+
p256dh: string | null;
|
|
221
|
+
endpoint: string | null;
|
|
222
|
+
} {
|
|
223
|
+
if (!subscription || !subscription.endpoint) {
|
|
224
|
+
return {
|
|
225
|
+
token: null,
|
|
226
|
+
auth: null,
|
|
227
|
+
p256dh: null,
|
|
228
|
+
endpoint: null,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const endpoint = subscription.endpoint;
|
|
233
|
+
const browserInfo = getBrowserInfo();
|
|
234
|
+
|
|
235
|
+
const keys = subscription.getKey
|
|
236
|
+
? {
|
|
237
|
+
auth: subscription.getKey('auth')
|
|
238
|
+
? this.arrayBufferToBase64(subscription.getKey('auth')!)
|
|
239
|
+
: null,
|
|
240
|
+
p256dh: subscription.getKey('p256dh')
|
|
241
|
+
? this.arrayBufferToBase64(subscription.getKey('p256dh')!)
|
|
242
|
+
: null,
|
|
243
|
+
}
|
|
244
|
+
: {
|
|
245
|
+
auth: null,
|
|
246
|
+
p256dh: null,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
logger.log('Extract push subscription information', {
|
|
250
|
+
browser: `${browserInfo.name} ${browserInfo.version}`,
|
|
251
|
+
hasAuth: !!keys.auth,
|
|
252
|
+
hasP256dh: !!keys.p256dh,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
if (browserInfo.name === 'Chrome' || browserInfo.name === 'Edge') {
|
|
257
|
+
const fcmMatch = endpoint.match(
|
|
258
|
+
/https:\/\/fcm\.googleapis\.com\/fcm\/send\/(.+)/
|
|
259
|
+
);
|
|
260
|
+
if (fcmMatch) {
|
|
261
|
+
logger.log('Extract Token using Chrome/Edge FCM format');
|
|
262
|
+
return {
|
|
263
|
+
token: fcmMatch[1],
|
|
264
|
+
auth: keys.auth,
|
|
265
|
+
p256dh: keys.p256dh,
|
|
266
|
+
endpoint: endpoint,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const gcmMatch = endpoint.match(
|
|
271
|
+
/https:\/\/android\.googleapis\.com\/gcm\/send\/(.+)/
|
|
272
|
+
);
|
|
273
|
+
if (gcmMatch) {
|
|
274
|
+
logger.log('Extract Token using Chrome/Edge GCM format');
|
|
275
|
+
return {
|
|
276
|
+
token: gcmMatch[1],
|
|
277
|
+
auth: keys.auth,
|
|
278
|
+
p256dh: keys.p256dh,
|
|
279
|
+
endpoint: endpoint,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
} else if (browserInfo.name === 'Firefox') {
|
|
283
|
+
const mozillaV2Match = endpoint.match(
|
|
284
|
+
/https:\/\/updates\.push\.services\.mozilla\.com\/wpush\/v(\d+)\/(.+)/
|
|
285
|
+
);
|
|
286
|
+
if (mozillaV2Match) {
|
|
287
|
+
logger.log(
|
|
288
|
+
`Extract Token using Firefox WebPush v${mozillaV2Match[1]} format`
|
|
289
|
+
);
|
|
290
|
+
return {
|
|
291
|
+
token: mozillaV2Match[2],
|
|
292
|
+
auth: keys.auth,
|
|
293
|
+
p256dh: keys.p256dh,
|
|
294
|
+
endpoint: endpoint,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const mozillaV1Match = endpoint.match(
|
|
299
|
+
/https:\/\/updates\.push\.services\.mozilla\.com\/push\/(.+)/
|
|
300
|
+
);
|
|
301
|
+
if (mozillaV1Match) {
|
|
302
|
+
logger.log('Extract Token using Firefox legacy format');
|
|
303
|
+
return {
|
|
304
|
+
token: mozillaV1Match[1],
|
|
305
|
+
auth: keys.auth,
|
|
306
|
+
p256dh: keys.p256dh,
|
|
307
|
+
endpoint: endpoint,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
} else if (browserInfo.name === 'Safari') {
|
|
311
|
+
if (browserInfo.majorVersion >= 16) {
|
|
312
|
+
const safariWebPushMatch = endpoint.match(
|
|
313
|
+
/https:\/\/web\.push\.apple\.com\/(.+)/
|
|
314
|
+
);
|
|
315
|
+
if (safariWebPushMatch) {
|
|
316
|
+
logger.log('Extract Token using Safari Web Push format');
|
|
317
|
+
return {
|
|
318
|
+
token: safariWebPushMatch[1],
|
|
319
|
+
auth: keys.auth,
|
|
320
|
+
p256dh: keys.p256dh,
|
|
321
|
+
endpoint: endpoint,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (endpoint.includes('safari') || endpoint.includes('apple')) {
|
|
327
|
+
logger.log(
|
|
328
|
+
'Safari push notification uses complete endpoint as identifier'
|
|
329
|
+
);
|
|
330
|
+
return {
|
|
331
|
+
token: this.generateSafariToken(endpoint),
|
|
332
|
+
auth: keys.auth,
|
|
333
|
+
p256dh: keys.p256dh,
|
|
334
|
+
endpoint: endpoint,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
} else if (browserInfo.name === 'Opera') {
|
|
338
|
+
const operaFcmMatch = endpoint.match(
|
|
339
|
+
/https:\/\/fcm\.googleapis\.com\/fcm\/send\/(.+)/
|
|
340
|
+
);
|
|
341
|
+
if (operaFcmMatch) {
|
|
342
|
+
logger.log('Extract Token using Opera FCM format');
|
|
343
|
+
return {
|
|
344
|
+
token: operaFcmMatch[1],
|
|
345
|
+
auth: keys.auth,
|
|
346
|
+
p256dh: keys.p256dh,
|
|
347
|
+
endpoint: endpoint,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
} else if (browserInfo.name === 'Samsung Internet') {
|
|
351
|
+
const samsungFcmMatch = endpoint.match(
|
|
352
|
+
/https:\/\/fcm\.googleapis\.com\/fcm\/send\/(.+)/
|
|
353
|
+
);
|
|
354
|
+
if (samsungFcmMatch) {
|
|
355
|
+
logger.log('Extract Token using Samsung Internet FCM format');
|
|
356
|
+
return {
|
|
357
|
+
token: samsungFcmMatch[1],
|
|
358
|
+
auth: keys.auth,
|
|
359
|
+
p256dh: keys.p256dh,
|
|
360
|
+
endpoint: endpoint,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const genericPatterns = [
|
|
366
|
+
{
|
|
367
|
+
pattern: /https:\/\/fcm\.googleapis\.com\/fcm\/send\/(.+)/,
|
|
368
|
+
name: 'FCM Standard',
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
pattern: /https:\/\/android\.googleapis\.com\/gcm\/send\/(.+)/,
|
|
372
|
+
name: 'GCM Legacy',
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
pattern: /https:\/\/gcm-http\.googleapis\.com\/gcm\/(.+)/,
|
|
376
|
+
name: 'GCM HTTP',
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
{
|
|
380
|
+
pattern:
|
|
381
|
+
/https:\/\/updates\.push\.services\.mozilla\.com\/wpush\/v\d+\/(.+)/,
|
|
382
|
+
name: 'Mozilla WebPush',
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
pattern:
|
|
386
|
+
/https:\/\/updates\.push\.services\.mozilla\.com\/push\/(.+)/,
|
|
387
|
+
name: 'Mozilla Legacy',
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
{
|
|
391
|
+
pattern: /https:\/\/.*\.notify\.windows\.com\/.*\/(.+)/,
|
|
392
|
+
name: 'Windows Notification',
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
{
|
|
396
|
+
pattern: /https:\/\/web\.push\.apple\.com\/(.+)/,
|
|
397
|
+
name: 'Apple Web Push',
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
{ pattern: /https:\/\/.*\/push\/(.+)/, name: 'Generic Push' },
|
|
401
|
+
{ pattern: /https:\/\/.*\/wpush\/(.+)/, name: 'Generic WebPush' },
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
for (const { pattern, name } of genericPatterns) {
|
|
405
|
+
const match = endpoint.match(pattern);
|
|
406
|
+
if (match && match[1]) {
|
|
407
|
+
logger.log(`Extract Token using generic pattern: ${name}`, {
|
|
408
|
+
browser: browserInfo.name,
|
|
409
|
+
pattern: pattern.source,
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
token: match[1],
|
|
413
|
+
auth: keys.auth,
|
|
414
|
+
p256dh: keys.p256dh,
|
|
415
|
+
endpoint: endpoint,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
logger.warn(
|
|
421
|
+
'Unable to extract standard Token from push endpoint, generating unique identifier',
|
|
422
|
+
{
|
|
423
|
+
browser: `${browserInfo.name} ${browserInfo.version}`,
|
|
424
|
+
endpointLength: endpoint.length,
|
|
425
|
+
endpointStart: endpoint.substring(0, 50) + '...',
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const fallbackToken = this.generateFallbackToken(endpoint, browserInfo);
|
|
430
|
+
return {
|
|
431
|
+
token: fallbackToken,
|
|
432
|
+
auth: keys.auth,
|
|
433
|
+
p256dh: keys.p256dh,
|
|
434
|
+
endpoint: endpoint,
|
|
435
|
+
};
|
|
436
|
+
} catch (error) {
|
|
437
|
+
logger.error(
|
|
438
|
+
'Error occurred during push subscription information extraction',
|
|
439
|
+
{
|
|
440
|
+
error: error instanceof Error ? error.message : error,
|
|
441
|
+
browser: browserInfo.name,
|
|
442
|
+
endpointDomain: endpoint.split('/')[2],
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const fallbackToken = this.generateFallbackToken(endpoint, browserInfo);
|
|
447
|
+
return {
|
|
448
|
+
token: fallbackToken,
|
|
449
|
+
auth: keys.auth,
|
|
450
|
+
p256dh: keys.p256dh,
|
|
451
|
+
endpoint: endpoint,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private generateSafariToken(endpoint: string): string {
|
|
457
|
+
const safariMatch = endpoint.match(/https:\/\/.*safari.*\/(.+)/);
|
|
458
|
+
if (safariMatch) {
|
|
459
|
+
return `safari_${safariMatch[1]}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return `safari_${this.generateEndpointHash(endpoint)}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private generateFallbackToken(endpoint: string, browserInfo: any): string {
|
|
466
|
+
const hash = this.generateEndpointHash(endpoint);
|
|
467
|
+
const browserPrefix = browserInfo.name.toLowerCase().replace(/\s+/g, '');
|
|
468
|
+
const timestamp = Date.now().toString(36);
|
|
469
|
+
|
|
470
|
+
return `${browserPrefix}_${hash}_${timestamp}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private generateEndpointHash(endpoint: string): string {
|
|
474
|
+
let hash = 0;
|
|
475
|
+
for (let i = 0; i < endpoint.length; i++) {
|
|
476
|
+
const char = endpoint.charCodeAt(i);
|
|
477
|
+
hash = (hash << 5) - hash + char;
|
|
478
|
+
hash = hash & hash;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return Math.abs(hash).toString(36);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async unsubscribe(): Promise<boolean> {
|
|
485
|
+
if (!this.registration) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const subscription = await this.registration.pushManager.getSubscription();
|
|
490
|
+
if (subscription) {
|
|
491
|
+
const result = await subscription.unsubscribe();
|
|
492
|
+
logger.log('Unsubscribe from push notifications', result);
|
|
493
|
+
this.clearStoredVapidPublicKey();
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async unregister(): Promise<boolean> {
|
|
501
|
+
if (!this.registration) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const result = await this.registration.unregister();
|
|
506
|
+
this.registration = null;
|
|
507
|
+
this.clearStoredVapidPublicKey();
|
|
508
|
+
logger.log('Unregister Service Worker', result);
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private setupMessageListener(): void {
|
|
513
|
+
if ('serviceWorker' in navigator) {
|
|
514
|
+
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
515
|
+
const message: ServiceWorkerMessage = event.data;
|
|
516
|
+
this.handleServiceWorkerMessage(message);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private handleServiceWorkerMessage(message: ServiceWorkerMessage): void {
|
|
522
|
+
logger.log('Received Service Worker message', message);
|
|
523
|
+
|
|
524
|
+
switch (message.type) {
|
|
525
|
+
case 'MESSAGE_RECEIVED':
|
|
526
|
+
this.eventEmitter.emit(EVENT.MESSAGE_RECEIVED, message.data);
|
|
527
|
+
break;
|
|
528
|
+
case 'NOTIFICATION_CLICKED':
|
|
529
|
+
this.eventEmitter.emit(EVENT.NOTIFICATION_CLICKED, message.data);
|
|
530
|
+
break;
|
|
531
|
+
case 'MESSAGE_REVOKED':
|
|
532
|
+
this.eventEmitter.emit(EVENT.MESSAGE_REVOKED, message.data);
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async postMessage(message: any): Promise<void> {
|
|
538
|
+
try {
|
|
539
|
+
Validator.validateValue('message', message, {
|
|
540
|
+
required: true,
|
|
541
|
+
message: 'Message content cannot be empty',
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
if (!this.registration || !this.registration.active) {
|
|
545
|
+
throw new Error('Service Worker not activated');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
this.registration.active.postMessage(message);
|
|
549
|
+
logger.log('Send message to Service Worker successful', message);
|
|
550
|
+
} catch (error) {
|
|
551
|
+
if (error instanceof ValidationError) {
|
|
552
|
+
logger.error('Send message parameter validation failed', error.message);
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
logger.error('Send message to Service Worker failed', error);
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
561
|
+
const bytes = new Uint8Array(buffer);
|
|
562
|
+
let binary = '';
|
|
563
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
564
|
+
binary += String.fromCharCode(bytes[i]);
|
|
565
|
+
}
|
|
566
|
+
return window.btoa(binary);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
570
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
571
|
+
const base64 = (base64String + padding)
|
|
572
|
+
.replace(/-/g, '+')
|
|
573
|
+
.replace(/_/g, '/');
|
|
574
|
+
|
|
575
|
+
const rawData = window.atob(base64);
|
|
576
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
577
|
+
|
|
578
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
579
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
580
|
+
}
|
|
581
|
+
return outputArray;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
getRegistration(): ServiceWorkerRegistration | null {
|
|
585
|
+
return this.registration;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private getStoredVapidPublicKey(): string | null {
|
|
589
|
+
try {
|
|
590
|
+
return Storage.get<string>('sdk_vapid_key');
|
|
591
|
+
} catch (error) {
|
|
592
|
+
logger.error('Failed to get stored VAPID public key', error);
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private setStoreVapidPublicKey(vapidPublicKey: string): void {
|
|
598
|
+
try {
|
|
599
|
+
Storage.set('sdk_vapid_key', vapidPublicKey);
|
|
600
|
+
logger.log('VAPID public key stored successfully');
|
|
601
|
+
} catch (error) {
|
|
602
|
+
logger.error('Failed to store VAPID public key', error);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private clearStoredVapidPublicKey(): void {
|
|
607
|
+
try {
|
|
608
|
+
Storage.remove('sdk_vapid_key');
|
|
609
|
+
logger.log('VAPID public key cleared successfully');
|
|
610
|
+
} catch (error) {
|
|
611
|
+
logger.error('Failed to clear VAPID public key', error);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|