@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.
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export * from './server';
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
+ }
@@ -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
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * Push notification types - shared between server and client
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+ }