@markwharton/pwa-push 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/deviceId.d.ts +13 -0
- package/dist/client/deviceId.js +37 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +19 -0
- package/dist/client/indexedDb.d.ts +17 -0
- package/dist/client/indexedDb.js +70 -0
- package/dist/client/renewal.d.ts +24 -0
- package/dist/client/renewal.js +99 -0
- package/dist/client/subscribe.d.ts +29 -0
- package/dist/client/subscribe.js +128 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +11 -0
- package/dist/server/send.d.ts +35 -0
- package/dist/server/send.js +92 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +5 -0
- package/package.json +35 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device ID utilities for subscription deduplication
|
|
3
|
+
* Prevents duplicate subscriptions when service worker re-registers
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Get or create a persistent device ID
|
|
7
|
+
* Uses localStorage (main thread only)
|
|
8
|
+
*/
|
|
9
|
+
export declare function getDeviceId(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Clear the device ID (for testing or logout)
|
|
12
|
+
*/
|
|
13
|
+
export declare function clearDeviceId(): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Device ID utilities for subscription deduplication
|
|
4
|
+
* Prevents duplicate subscriptions when service worker re-registers
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.getDeviceId = getDeviceId;
|
|
8
|
+
exports.clearDeviceId = clearDeviceId;
|
|
9
|
+
const DEVICE_ID_KEY = 'push_device_id';
|
|
10
|
+
/**
|
|
11
|
+
* Generate a UUID v4
|
|
12
|
+
*/
|
|
13
|
+
function generateUUID() {
|
|
14
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
15
|
+
const r = (Math.random() * 16) | 0;
|
|
16
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
17
|
+
return v.toString(16);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get or create a persistent device ID
|
|
22
|
+
* Uses localStorage (main thread only)
|
|
23
|
+
*/
|
|
24
|
+
function getDeviceId() {
|
|
25
|
+
let deviceId = localStorage.getItem(DEVICE_ID_KEY);
|
|
26
|
+
if (!deviceId) {
|
|
27
|
+
deviceId = generateUUID();
|
|
28
|
+
localStorage.setItem(DEVICE_ID_KEY, deviceId);
|
|
29
|
+
}
|
|
30
|
+
return deviceId;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Clear the device ID (for testing or logout)
|
|
34
|
+
*/
|
|
35
|
+
function clearDeviceId() {
|
|
36
|
+
localStorage.removeItem(DEVICE_ID_KEY);
|
|
37
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { getDeviceId, clearDeviceId } from './deviceId';
|
|
2
|
+
export { saveSubscriptionData, getSubscriptionData, clearSubscriptionData } from './indexedDb';
|
|
3
|
+
export { getPermissionState, requestPermission, isPushSupported, subscribeToPush, unsubscribeFromPush, getCurrentSubscription } from './subscribe';
|
|
4
|
+
export { handleSubscriptionChange } from './renewal';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleSubscriptionChange = exports.getCurrentSubscription = exports.unsubscribeFromPush = exports.subscribeToPush = exports.isPushSupported = exports.requestPermission = exports.getPermissionState = exports.clearSubscriptionData = exports.getSubscriptionData = exports.saveSubscriptionData = exports.clearDeviceId = exports.getDeviceId = void 0;
|
|
4
|
+
var deviceId_1 = require("./deviceId");
|
|
5
|
+
Object.defineProperty(exports, "getDeviceId", { enumerable: true, get: function () { return deviceId_1.getDeviceId; } });
|
|
6
|
+
Object.defineProperty(exports, "clearDeviceId", { enumerable: true, get: function () { return deviceId_1.clearDeviceId; } });
|
|
7
|
+
var indexedDb_1 = require("./indexedDb");
|
|
8
|
+
Object.defineProperty(exports, "saveSubscriptionData", { enumerable: true, get: function () { return indexedDb_1.saveSubscriptionData; } });
|
|
9
|
+
Object.defineProperty(exports, "getSubscriptionData", { enumerable: true, get: function () { return indexedDb_1.getSubscriptionData; } });
|
|
10
|
+
Object.defineProperty(exports, "clearSubscriptionData", { enumerable: true, get: function () { return indexedDb_1.clearSubscriptionData; } });
|
|
11
|
+
var subscribe_1 = require("./subscribe");
|
|
12
|
+
Object.defineProperty(exports, "getPermissionState", { enumerable: true, get: function () { return subscribe_1.getPermissionState; } });
|
|
13
|
+
Object.defineProperty(exports, "requestPermission", { enumerable: true, get: function () { return subscribe_1.requestPermission; } });
|
|
14
|
+
Object.defineProperty(exports, "isPushSupported", { enumerable: true, get: function () { return subscribe_1.isPushSupported; } });
|
|
15
|
+
Object.defineProperty(exports, "subscribeToPush", { enumerable: true, get: function () { return subscribe_1.subscribeToPush; } });
|
|
16
|
+
Object.defineProperty(exports, "unsubscribeFromPush", { enumerable: true, get: function () { return subscribe_1.unsubscribeFromPush; } });
|
|
17
|
+
Object.defineProperty(exports, "getCurrentSubscription", { enumerable: true, get: function () { return subscribe_1.getCurrentSubscription; } });
|
|
18
|
+
var renewal_1 = require("./renewal");
|
|
19
|
+
Object.defineProperty(exports, "handleSubscriptionChange", { enumerable: true, get: function () { return renewal_1.handleSubscriptionChange; } });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndexedDB utilities for service worker data persistence
|
|
3
|
+
* Service workers cannot access localStorage, so we use IndexedDB
|
|
4
|
+
*/
|
|
5
|
+
import { SubscriptionData } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* Save subscription data to IndexedDB (for service worker access)
|
|
8
|
+
*/
|
|
9
|
+
export declare function saveSubscriptionData(data: SubscriptionData): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Get subscription data from IndexedDB
|
|
12
|
+
*/
|
|
13
|
+
export declare function getSubscriptionData(): Promise<SubscriptionData | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Clear subscription data from IndexedDB
|
|
16
|
+
*/
|
|
17
|
+
export declare function clearSubscriptionData(): Promise<void>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* IndexedDB utilities for service worker data persistence
|
|
4
|
+
* Service workers cannot access localStorage, so we use IndexedDB
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.saveSubscriptionData = saveSubscriptionData;
|
|
8
|
+
exports.getSubscriptionData = getSubscriptionData;
|
|
9
|
+
exports.clearSubscriptionData = clearSubscriptionData;
|
|
10
|
+
const DB_NAME = 'PushSubscriptionDB';
|
|
11
|
+
const STORE_NAME = 'subscriptionData';
|
|
12
|
+
const DATA_KEY = 'current';
|
|
13
|
+
/**
|
|
14
|
+
* Open the IndexedDB database
|
|
15
|
+
*/
|
|
16
|
+
function openDatabase() {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const request = indexedDB.open(DB_NAME, 1);
|
|
19
|
+
request.onerror = () => reject(request.error);
|
|
20
|
+
request.onsuccess = () => resolve(request.result);
|
|
21
|
+
request.onupgradeneeded = (event) => {
|
|
22
|
+
const db = event.target.result;
|
|
23
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
24
|
+
db.createObjectStore(STORE_NAME);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Save subscription data to IndexedDB (for service worker access)
|
|
31
|
+
*/
|
|
32
|
+
async function saveSubscriptionData(data) {
|
|
33
|
+
const db = await openDatabase();
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const transaction = db.transaction(STORE_NAME, 'readwrite');
|
|
36
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
37
|
+
const request = store.put(data, DATA_KEY);
|
|
38
|
+
request.onerror = () => reject(request.error);
|
|
39
|
+
request.onsuccess = () => resolve();
|
|
40
|
+
transaction.oncomplete = () => db.close();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get subscription data from IndexedDB
|
|
45
|
+
*/
|
|
46
|
+
async function getSubscriptionData() {
|
|
47
|
+
const db = await openDatabase();
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const transaction = db.transaction(STORE_NAME, 'readonly');
|
|
50
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
51
|
+
const request = store.get(DATA_KEY);
|
|
52
|
+
request.onerror = () => reject(request.error);
|
|
53
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
54
|
+
transaction.oncomplete = () => db.close();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Clear subscription data from IndexedDB
|
|
59
|
+
*/
|
|
60
|
+
async function clearSubscriptionData() {
|
|
61
|
+
const db = await openDatabase();
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const transaction = db.transaction(STORE_NAME, 'readwrite');
|
|
64
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
65
|
+
const request = store.delete(DATA_KEY);
|
|
66
|
+
request.onerror = () => reject(request.error);
|
|
67
|
+
request.onsuccess = () => resolve();
|
|
68
|
+
transaction.oncomplete = () => db.close();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push subscription renewal handler for service worker
|
|
3
|
+
* Handles the 'pushsubscriptionchange' event when browser rotates subscriptions
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Handle pushsubscriptionchange event in service worker
|
|
7
|
+
* Call this from your service worker's event listener
|
|
8
|
+
*
|
|
9
|
+
* Usage in sw.js:
|
|
10
|
+
* ```
|
|
11
|
+
* self.addEventListener('pushsubscriptionchange', (event) => {
|
|
12
|
+
* event.waitUntil(handleSubscriptionChange(event, '/api/push/renew'));
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function handleSubscriptionChange(event: PushSubscriptionChangeEvent, renewEndpoint: string): Promise<boolean>;
|
|
17
|
+
/**
|
|
18
|
+
* Type for pushsubscriptionchange event
|
|
19
|
+
*/
|
|
20
|
+
interface PushSubscriptionChangeEvent extends ExtendableEvent {
|
|
21
|
+
oldSubscription?: PushSubscription;
|
|
22
|
+
newSubscription?: PushSubscription;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Push subscription renewal handler for service worker
|
|
4
|
+
* Handles the 'pushsubscriptionchange' event when browser rotates subscriptions
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.handleSubscriptionChange = handleSubscriptionChange;
|
|
8
|
+
const indexedDb_1 = require("./indexedDb");
|
|
9
|
+
/**
|
|
10
|
+
* Handle pushsubscriptionchange event in service worker
|
|
11
|
+
* Call this from your service worker's event listener
|
|
12
|
+
*
|
|
13
|
+
* Usage in sw.js:
|
|
14
|
+
* ```
|
|
15
|
+
* self.addEventListener('pushsubscriptionchange', (event) => {
|
|
16
|
+
* event.waitUntil(handleSubscriptionChange(event, '/api/push/renew'));
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
async function handleSubscriptionChange(event, renewEndpoint) {
|
|
21
|
+
try {
|
|
22
|
+
// Get stored data from IndexedDB
|
|
23
|
+
const data = await (0, indexedDb_1.getSubscriptionData)();
|
|
24
|
+
if (!data) {
|
|
25
|
+
console.error('No subscription data found for renewal');
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
// Resubscribe with same VAPID key
|
|
29
|
+
const newSubscription = await resubscribe(data);
|
|
30
|
+
if (!newSubscription) {
|
|
31
|
+
console.error('Failed to create new subscription');
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// Build renewal request
|
|
35
|
+
const renewalRequest = {
|
|
36
|
+
subscription: newSubscription,
|
|
37
|
+
deviceId: data.deviceId,
|
|
38
|
+
basePath: data.basePath
|
|
39
|
+
};
|
|
40
|
+
// Send to backend
|
|
41
|
+
const response = await fetch(renewEndpoint, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify(renewalRequest)
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
console.error('Renewal request failed:', response.status);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
console.log('Push subscription renewed successfully');
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error('Subscription renewal failed:', error);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resubscribe to push with stored VAPID key
|
|
60
|
+
*/
|
|
61
|
+
async function resubscribe(data) {
|
|
62
|
+
try {
|
|
63
|
+
const self = globalThis;
|
|
64
|
+
const registration = self.registration;
|
|
65
|
+
const applicationServerKey = urlBase64ToUint8Array(data.publicKey);
|
|
66
|
+
const subscription = await registration.pushManager.subscribe({
|
|
67
|
+
userVisibleOnly: true,
|
|
68
|
+
applicationServerKey
|
|
69
|
+
});
|
|
70
|
+
const json = subscription.toJSON();
|
|
71
|
+
return {
|
|
72
|
+
endpoint: subscription.endpoint,
|
|
73
|
+
keys: {
|
|
74
|
+
p256dh: json.keys?.p256dh || '',
|
|
75
|
+
auth: json.keys?.auth || ''
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error('Resubscribe failed:', error);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Convert base64 URL-encoded string to Uint8Array (service worker version)
|
|
86
|
+
*/
|
|
87
|
+
function urlBase64ToUint8Array(base64String) {
|
|
88
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
89
|
+
const base64 = (base64String + padding)
|
|
90
|
+
.replace(/-/g, '+')
|
|
91
|
+
.replace(/_/g, '/');
|
|
92
|
+
const rawData = atob(base64);
|
|
93
|
+
const buffer = new ArrayBuffer(rawData.length);
|
|
94
|
+
const outputArray = new Uint8Array(buffer);
|
|
95
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
96
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
97
|
+
}
|
|
98
|
+
return outputArray;
|
|
99
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push subscription helpers for main thread
|
|
3
|
+
*/
|
|
4
|
+
import { PushSubscription, SubscriptionRequest, PushPermissionState } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Check current notification permission state
|
|
7
|
+
*/
|
|
8
|
+
export declare function getPermissionState(): PushPermissionState;
|
|
9
|
+
/**
|
|
10
|
+
* Request notification permission from user
|
|
11
|
+
*/
|
|
12
|
+
export declare function requestPermission(): Promise<PushPermissionState>;
|
|
13
|
+
/**
|
|
14
|
+
* Check if push notifications are supported
|
|
15
|
+
*/
|
|
16
|
+
export declare function isPushSupported(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Subscribe to push notifications
|
|
19
|
+
* Returns subscription request ready to send to backend
|
|
20
|
+
*/
|
|
21
|
+
export declare function subscribeToPush(vapidPublicKey: string, basePath?: string): Promise<SubscriptionRequest | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Unsubscribe from push notifications
|
|
24
|
+
*/
|
|
25
|
+
export declare function unsubscribeFromPush(): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Get current push subscription if exists
|
|
28
|
+
*/
|
|
29
|
+
export declare function getCurrentSubscription(): Promise<PushSubscription | null>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Push subscription helpers for main thread
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getPermissionState = getPermissionState;
|
|
7
|
+
exports.requestPermission = requestPermission;
|
|
8
|
+
exports.isPushSupported = isPushSupported;
|
|
9
|
+
exports.subscribeToPush = subscribeToPush;
|
|
10
|
+
exports.unsubscribeFromPush = unsubscribeFromPush;
|
|
11
|
+
exports.getCurrentSubscription = getCurrentSubscription;
|
|
12
|
+
const deviceId_1 = require("./deviceId");
|
|
13
|
+
const indexedDb_1 = require("./indexedDb");
|
|
14
|
+
/**
|
|
15
|
+
* Check current notification permission state
|
|
16
|
+
*/
|
|
17
|
+
function getPermissionState() {
|
|
18
|
+
return Notification.permission;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Request notification permission from user
|
|
22
|
+
*/
|
|
23
|
+
async function requestPermission() {
|
|
24
|
+
const result = await Notification.requestPermission();
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if push notifications are supported
|
|
29
|
+
*/
|
|
30
|
+
function isPushSupported() {
|
|
31
|
+
return 'serviceWorker' in navigator && 'PushManager' in window;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Convert browser PushSubscription to our format
|
|
35
|
+
*/
|
|
36
|
+
function toPushSubscription(browserSub) {
|
|
37
|
+
const json = browserSub.toJSON();
|
|
38
|
+
return {
|
|
39
|
+
endpoint: browserSub.endpoint,
|
|
40
|
+
keys: {
|
|
41
|
+
p256dh: json.keys?.p256dh || '',
|
|
42
|
+
auth: json.keys?.auth || ''
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Subscribe to push notifications
|
|
48
|
+
* Returns subscription request ready to send to backend
|
|
49
|
+
*/
|
|
50
|
+
async function subscribeToPush(vapidPublicKey, basePath = '/') {
|
|
51
|
+
if (!isPushSupported()) {
|
|
52
|
+
console.error('Push notifications not supported');
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const permission = await requestPermission();
|
|
56
|
+
if (permission !== 'granted') {
|
|
57
|
+
console.warn('Push permission denied');
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const registration = await navigator.serviceWorker.ready;
|
|
61
|
+
const deviceId = (0, deviceId_1.getDeviceId)();
|
|
62
|
+
// Convert VAPID key to Uint8Array
|
|
63
|
+
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
|
|
64
|
+
const browserSub = await registration.pushManager.subscribe({
|
|
65
|
+
userVisibleOnly: true,
|
|
66
|
+
applicationServerKey
|
|
67
|
+
});
|
|
68
|
+
// Save data for service worker renewal
|
|
69
|
+
await (0, indexedDb_1.saveSubscriptionData)({
|
|
70
|
+
publicKey: vapidPublicKey,
|
|
71
|
+
deviceId,
|
|
72
|
+
basePath
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
subscription: toPushSubscription(browserSub),
|
|
76
|
+
deviceId,
|
|
77
|
+
basePath
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Unsubscribe from push notifications
|
|
82
|
+
*/
|
|
83
|
+
async function unsubscribeFromPush() {
|
|
84
|
+
try {
|
|
85
|
+
const registration = await navigator.serviceWorker.ready;
|
|
86
|
+
const subscription = await registration.pushManager.getSubscription();
|
|
87
|
+
if (subscription) {
|
|
88
|
+
await subscription.unsubscribe();
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error('Unsubscribe failed:', error);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get current push subscription if exists
|
|
99
|
+
*/
|
|
100
|
+
async function getCurrentSubscription() {
|
|
101
|
+
try {
|
|
102
|
+
const registration = await navigator.serviceWorker.ready;
|
|
103
|
+
const subscription = await registration.pushManager.getSubscription();
|
|
104
|
+
if (!subscription) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return toPushSubscription(subscription);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Convert base64 URL-encoded string to Uint8Array
|
|
115
|
+
*/
|
|
116
|
+
function urlBase64ToUint8Array(base64String) {
|
|
117
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
118
|
+
const base64 = (base64String + padding)
|
|
119
|
+
.replace(/-/g, '+')
|
|
120
|
+
.replace(/_/g, '/');
|
|
121
|
+
const rawData = window.atob(base64);
|
|
122
|
+
const buffer = new ArrayBuffer(rawData.length);
|
|
123
|
+
const outputArray = new Uint8Array(buffer);
|
|
124
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
125
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
126
|
+
}
|
|
127
|
+
return outputArray;
|
|
128
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
// Re-export types
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
// Re-export server utilities (for backend)
|
|
20
|
+
__exportStar(require("./server"), exports);
|
|
21
|
+
// Note: Client utilities should be imported from '@markwharton/pwa-push/client'
|
|
22
|
+
// to avoid including browser-only code in Node.js bundles
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { initPush, initPushFromEnv, getVapidPublicKey, sendPushNotification, sendPushToAll, sendPushWithDetails, isSubscriptionExpired } from './send';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSubscriptionExpired = exports.sendPushWithDetails = exports.sendPushToAll = exports.sendPushNotification = exports.getVapidPublicKey = exports.initPushFromEnv = exports.initPush = void 0;
|
|
4
|
+
var send_1 = require("./send");
|
|
5
|
+
Object.defineProperty(exports, "initPush", { enumerable: true, get: function () { return send_1.initPush; } });
|
|
6
|
+
Object.defineProperty(exports, "initPushFromEnv", { enumerable: true, get: function () { return send_1.initPushFromEnv; } });
|
|
7
|
+
Object.defineProperty(exports, "getVapidPublicKey", { enumerable: true, get: function () { return send_1.getVapidPublicKey; } });
|
|
8
|
+
Object.defineProperty(exports, "sendPushNotification", { enumerable: true, get: function () { return send_1.sendPushNotification; } });
|
|
9
|
+
Object.defineProperty(exports, "sendPushToAll", { enumerable: true, get: function () { return send_1.sendPushToAll; } });
|
|
10
|
+
Object.defineProperty(exports, "sendPushWithDetails", { enumerable: true, get: function () { return send_1.sendPushWithDetails; } });
|
|
11
|
+
Object.defineProperty(exports, "isSubscriptionExpired", { enumerable: true, get: function () { return send_1.isSubscriptionExpired; } });
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PushSubscription, NotificationPayload, SendResult } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Initialize VAPID configuration - call once at startup
|
|
4
|
+
* Fails fast if keys are missing
|
|
5
|
+
*/
|
|
6
|
+
export declare function initPush(config: {
|
|
7
|
+
publicKey?: string;
|
|
8
|
+
privateKey?: string;
|
|
9
|
+
subject?: string;
|
|
10
|
+
}): void;
|
|
11
|
+
/**
|
|
12
|
+
* Auto-initialize from environment variables
|
|
13
|
+
*/
|
|
14
|
+
export declare function initPushFromEnv(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Get the VAPID public key (for client subscription)
|
|
17
|
+
*/
|
|
18
|
+
export declare function getVapidPublicKey(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Send a push notification to a single subscription
|
|
21
|
+
*/
|
|
22
|
+
export declare function sendPushNotification(subscription: PushSubscription, payload: NotificationPayload): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Send a push notification to multiple subscriptions
|
|
25
|
+
* Returns array of results
|
|
26
|
+
*/
|
|
27
|
+
export declare function sendPushToAll(subscriptions: PushSubscription[], payload: NotificationPayload): Promise<SendResult[]>;
|
|
28
|
+
/**
|
|
29
|
+
* Send push notification with detailed error information
|
|
30
|
+
*/
|
|
31
|
+
export declare function sendPushWithDetails(subscription: PushSubscription, payload: NotificationPayload): Promise<SendResult>;
|
|
32
|
+
/**
|
|
33
|
+
* Check if a subscription is expired (410 Gone)
|
|
34
|
+
*/
|
|
35
|
+
export declare function isSubscriptionExpired(statusCode: number | undefined): boolean;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.initPush = initPush;
|
|
7
|
+
exports.initPushFromEnv = initPushFromEnv;
|
|
8
|
+
exports.getVapidPublicKey = getVapidPublicKey;
|
|
9
|
+
exports.sendPushNotification = sendPushNotification;
|
|
10
|
+
exports.sendPushToAll = sendPushToAll;
|
|
11
|
+
exports.sendPushWithDetails = sendPushWithDetails;
|
|
12
|
+
exports.isSubscriptionExpired = isSubscriptionExpired;
|
|
13
|
+
const web_push_1 = __importDefault(require("web-push"));
|
|
14
|
+
/**
|
|
15
|
+
* Server-side Web Push notification utilities
|
|
16
|
+
*/
|
|
17
|
+
let vapidConfig = null;
|
|
18
|
+
/**
|
|
19
|
+
* Initialize VAPID configuration - call once at startup
|
|
20
|
+
* Fails fast if keys are missing
|
|
21
|
+
*/
|
|
22
|
+
function initPush(config) {
|
|
23
|
+
const { publicKey, privateKey, subject } = config;
|
|
24
|
+
if (!publicKey || !privateKey) {
|
|
25
|
+
throw new Error('VAPID keys required. Set VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY.');
|
|
26
|
+
}
|
|
27
|
+
if (!subject) {
|
|
28
|
+
throw new Error('VAPID subject required. Set VAPID_SUBJECT (e.g., mailto:you@example.com).');
|
|
29
|
+
}
|
|
30
|
+
vapidConfig = { publicKey, privateKey, subject };
|
|
31
|
+
web_push_1.default.setVapidDetails(subject, publicKey, privateKey);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Auto-initialize from environment variables
|
|
35
|
+
*/
|
|
36
|
+
function initPushFromEnv() {
|
|
37
|
+
initPush({
|
|
38
|
+
publicKey: process.env.VAPID_PUBLIC_KEY,
|
|
39
|
+
privateKey: process.env.VAPID_PRIVATE_KEY,
|
|
40
|
+
subject: process.env.VAPID_SUBJECT
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get the VAPID public key (for client subscription)
|
|
45
|
+
*/
|
|
46
|
+
function getVapidPublicKey() {
|
|
47
|
+
if (!vapidConfig) {
|
|
48
|
+
throw new Error('Push not initialized. Call initPush() first.');
|
|
49
|
+
}
|
|
50
|
+
return vapidConfig.publicKey;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Send a push notification to a single subscription
|
|
54
|
+
*/
|
|
55
|
+
async function sendPushNotification(subscription, payload) {
|
|
56
|
+
const result = await sendPushWithDetails(subscription, payload);
|
|
57
|
+
return result.success;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Send a push notification to multiple subscriptions
|
|
61
|
+
* Returns array of results
|
|
62
|
+
*/
|
|
63
|
+
async function sendPushToAll(subscriptions, payload) {
|
|
64
|
+
const results = await Promise.all(subscriptions.map(sub => sendPushWithDetails(sub, payload)));
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Send push notification with detailed error information
|
|
69
|
+
*/
|
|
70
|
+
async function sendPushWithDetails(subscription, payload) {
|
|
71
|
+
if (!vapidConfig) {
|
|
72
|
+
return { success: false, error: 'Push not initialized' };
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await web_push_1.default.sendNotification(subscription, JSON.stringify(payload));
|
|
76
|
+
return { success: true };
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const webPushError = error;
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: webPushError.message || 'Unknown error',
|
|
83
|
+
statusCode: webPushError.statusCode
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if a subscription is expired (410 Gone)
|
|
89
|
+
*/
|
|
90
|
+
function isSubscriptionExpired(statusCode) {
|
|
91
|
+
return statusCode === 410;
|
|
92
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push notification types - shared between server and client
|
|
3
|
+
*/
|
|
4
|
+
export interface PushSubscription {
|
|
5
|
+
endpoint: string;
|
|
6
|
+
keys: {
|
|
7
|
+
p256dh: string;
|
|
8
|
+
auth: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface NotificationPayload {
|
|
12
|
+
title: string;
|
|
13
|
+
body: string;
|
|
14
|
+
type?: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
tag?: string;
|
|
17
|
+
data?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface VapidConfig {
|
|
20
|
+
publicKey: string;
|
|
21
|
+
privateKey: string;
|
|
22
|
+
subject: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SendResult {
|
|
25
|
+
success: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
statusCode?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface SubscriptionRequest {
|
|
30
|
+
subscription: PushSubscription;
|
|
31
|
+
deviceId: string;
|
|
32
|
+
basePath?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface RenewalRequest {
|
|
35
|
+
subscription: PushSubscription;
|
|
36
|
+
deviceId: string;
|
|
37
|
+
basePath?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface SubscriptionData {
|
|
40
|
+
publicKey: string;
|
|
41
|
+
deviceId: string;
|
|
42
|
+
basePath: string;
|
|
43
|
+
}
|
|
44
|
+
export type PushPermissionState = 'granted' | 'denied' | 'default';
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@markwharton/pwa-push",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Web push notifications for Azure PWA projects",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./server": "./dist/server/index.js",
|
|
10
|
+
"./client": "./dist/client/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"clean": "rm -rf dist"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"web-push": "^3.6.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^20.10.0",
|
|
21
|
+
"@types/web-push": "^3.6.4",
|
|
22
|
+
"typescript": "^5.3.0",
|
|
23
|
+
"web-push": "^3.6.7"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/MarkWharton/pwa-packages.git",
|
|
31
|
+
"directory": "packages/push"
|
|
32
|
+
},
|
|
33
|
+
"author": "Mark Wharton",
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|