@pushforge/builder 1.1.2 → 2.0.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/README.md CHANGED
@@ -1,198 +1,360 @@
1
- # PushForge Builder
1
+ <div align="center">
2
2
 
3
- A robust, cross-platform Web Push notification library that handles VAPID authentication and payload encryption following the Web Push Protocol standard.
3
+ <img src="https://raw.githubusercontent.com/draphy/pushforge/master/images/logo.webp" alt="PushForge Logo" width="120" />
4
4
 
5
- ## Installation
5
+ # PushForge Builder
6
6
 
7
- Choose your preferred package manager:
7
+ **A lightweight, dependency-free Web Push library built on the standard Web Crypto API.**
8
8
 
9
- ```bash
10
- # NPM
11
- npm install @pushforge/builder
9
+ [![npm version](https://img.shields.io/npm/v/@pushforge/builder.svg)](https://www.npmjs.com/package/@pushforge/builder)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
11
+ [![TypeScript](https://img.shields.io/badge/TypeScript-first--class-blue.svg)](https://www.typescriptlang.org/)
12
12
 
13
- # Yarn
14
- yarn add @pushforge/builder
13
+ Send push notifications from any JavaScript runtime · Zero dependencies
15
14
 
16
- # pnpm
17
- pnpm add @pushforge/builder
18
- ```
15
+ [GitHub](https://github.com/draphy/pushforge) · [npm](https://www.npmjs.com/package/@pushforge/builder) · [Report Bug](https://github.com/draphy/pushforge/issues)
19
16
 
20
- ## Features
17
+ **[Try the Live Demo →](https://pushforge.draphy.org)**
21
18
 
22
- - 🔑 Compliant VAPID authentication
23
- - 🔒 Web Push Protocol encryption
24
- - 🌐 Cross-platform compatibility (Node.js 16+, Browsers, Deno, Bun, Cloudflare Workers)
25
- - 🧩 TypeScript definitions included
26
- - 🛠️ Zero dependencies
19
+ </div>
27
20
 
28
- ## Getting Started
21
+ ---
29
22
 
30
- ### Step 1: Generate VAPID Keys
23
+ ```bash
24
+ npm install @pushforge/builder
25
+ ```
31
26
 
32
- PushForge includes a built-in CLI tool to generate VAPID keys for Web Push Authentication:
27
+ ## Live Demo
33
28
 
34
- ```bash
35
- # Generate VAPID keys using npx
36
- npx @pushforge/builder generate-vapid-keys
29
+ Try PushForge in your browser at **[pushforge.draphy.org](https://pushforge.draphy.org)** — a live test site running on Cloudflare Workers.
37
30
 
38
- # Using yarn
39
- yarn dlx @pushforge/builder generate-vapid-keys
31
+ - Toggle push notifications on, send a test message, and see it arrive in real time
32
+ - Works across all supported browsers — Chrome, Firefox, Edge, Safari 16+
33
+ - The backend is a single Cloudflare Worker using `buildPushHTTPRequest()` with zero additional dependencies
34
+ - Subscriptions auto-expire after 5 minutes — no permanent data stored
40
35
 
41
- # Using pnpm
42
- pnpm dlx @pushforge/builder generate-vapid-keys
43
- ```
36
+ ## Why PushForge?
44
37
 
45
- This will output a public key and private key that you can use for VAPID authentication:
38
+ | | PushForge | web-push |
39
+ |---|:---:|:---:|
40
+ | Dependencies | **0** | 5+ (with nested deps) |
41
+ | Cloudflare Workers | Yes | [No](https://github.com/web-push-libs/web-push/issues/718) |
42
+ | Vercel Edge | Yes | No |
43
+ | Convex | Yes | No |
44
+ | Deno / Bun | Yes | Limited |
45
+ | TypeScript | First-class | @types package |
46
46
 
47
- ```
48
- ┌────────────────────────────────────────────────────────────┐
49
- │ │
50
- │ VAPID Keys Generated Successfully │
51
- │ │
52
- │ Public Key: │
53
- │ BDd0DtL3qQmnI7-JPwKMuGuFBC7VW9GjKP0qR-4C9Y9lJ2LLWR0pSI... │
54
- │ │
55
- │ Private Key (JWK): │
56
- │ { │
57
- │ "alg": "ES256", │
58
- │ "kty": "EC", │
59
- │ "crv": "P-256", │
60
- │ "x": "N3QO0vepCacjv4k_AoyYa4UELtVb0aMo_SpH7gL1j2U", │
61
- │ "y": "ZSdiy1kdKUiOGjuoVgMbp4HwmQDz0nhHxPJLbFYh1j8", │
62
- │ "d": "8M9F5JCaEsXdTU1OpD4ODq-o5qZQcDmCYS6EHrC1o8E" │
63
- │ } │
64
- │ │
65
- │ Store these keys securely. Never expose your private key. │
66
- │ │
67
- └────────────────────────────────────────────────────────────┘
68
- ```
47
+ Traditional web push libraries rely on Node.js-specific APIs (`crypto.createECDH`, `https.request`) that don't work in modern edge runtimes. PushForge uses the standard [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API), making it portable across all JavaScript environments.
69
48
 
70
- **Requirements:**
49
+ ## Quick Start
71
50
 
72
- - Node.js 16.0.0 or later
73
- - The command uses the WebCrypto API which is built-in to Node.js 16+
51
+ ### 1. Generate VAPID Keys
74
52
 
75
- ### Step 2: Set Up Push Notifications in Your Web Application
53
+ ```bash
54
+ npx @pushforge/builder vapid
55
+ ```
76
56
 
77
- To implement push notifications in your web application, you'll need to:
57
+ This outputs a public key (for your frontend) and a private key in JWK format (for your server).
78
58
 
79
- 1. Use the VAPID public key generated in Step 1
80
- 2. Request notification permission from the user
81
- 3. Subscribe to push notifications using the Push API
82
- 4. Save the subscription information in your backend
59
+ ### 2. Subscribe Users (Frontend)
83
60
 
84
- When implementing your service worker, handle push events to display notifications when they arrive:
61
+ Use the VAPID public key to subscribe users to push notifications:
85
62
 
86
- 1. Listen for `push` events
87
- 2. Parse the notification data
88
- 3. Display the notification to the user
89
- 4. Handle notification click events
63
+ ```javascript
64
+ // In your frontend application
65
+ const registration = await navigator.serviceWorker.ready;
90
66
 
91
- Refer to the [Push API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) and [Notifications API documentation](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API) for detailed implementation.
67
+ const subscription = await registration.pushManager.subscribe({
68
+ userVisibleOnly: true,
69
+ applicationServerKey: 'YOUR_VAPID_PUBLIC_KEY' // From step 1
70
+ });
92
71
 
93
- ### Step 3: Send Push Notifications from Your Server
72
+ // Send this subscription to your server
73
+ // subscription.toJSON() returns:
74
+ // {
75
+ // endpoint: "https://fcm.googleapis.com/fcm/send/...",
76
+ // keys: {
77
+ // p256dh: "BNcRd...",
78
+ // auth: "tBHI..."
79
+ // }
80
+ // }
81
+ await fetch('/api/subscribe', {
82
+ method: 'POST',
83
+ body: JSON.stringify(subscription)
84
+ });
85
+ ```
94
86
 
95
- On your backend server, use the VAPID private key to send push notifications:
87
+ ### 3. Send Notifications (Server)
96
88
 
97
89
  ```typescript
98
90
  import { buildPushHTTPRequest } from "@pushforge/builder";
99
91
 
100
- // Load the private key from your secure environment
101
- // This should be the private key from your VAPID key generation
102
- const privateJWK = process.env.VAPID_PRIVATE_KEY;
92
+ // Your VAPID private key (JWK format from step 1)
93
+ const privateJWK = {
94
+ kty: "EC",
95
+ crv: "P-256",
96
+ x: "...",
97
+ y: "...",
98
+ d: "..."
99
+ };
103
100
 
104
- // User subscription from browser push API
101
+ // The subscription object from the user's browser
105
102
  const subscription = {
106
- endpoint: "https://fcm.googleapis.com/fcm/send/DEVICE_TOKEN",
103
+ endpoint: "https://fcm.googleapis.com/fcm/send/...",
107
104
  keys: {
108
- p256dh: "USER_PUBLIC_KEY",
109
- auth: "USER_AUTH_SECRET",
110
- },
111
- };
112
-
113
- // Create message with payload
114
- const message = {
115
- payload: {
116
- title: "New Message",
117
- body: "You have a new message!",
118
- icon: "/images/icon.png",
119
- },
120
- options: {
121
- //Default value is 24 * 60 * 60 (24 hours).
122
- //The VAPID JWT expiration claim (`exp`) must not exceed 24 hours from the time of the request.
123
- ttl: 3600, // Time-to-live in seconds
124
- urgency: "normal", // Options: "very-low", "low", "normal", "high"
125
- topic: "updates", // Optional topic for replacing notifications
126
- },
127
- adminContact: "mailto:admin@example.com", //The contact information of the administrator
105
+ p256dh: "BNcRd...",
106
+ auth: "tBHI..."
107
+ }
128
108
  };
129
109
 
130
- // Build the push notification request
131
- const {endpoint, headers, body} = await buildPushHTTPRequest({
110
+ // Build and send the notification
111
+ const { endpoint, headers, body } = await buildPushHTTPRequest({
132
112
  privateJWK,
133
- message,
134
113
  subscription,
114
+ message: {
115
+ payload: {
116
+ title: "New Message",
117
+ body: "You have a new notification!",
118
+ icon: "/icon.png"
119
+ },
120
+ adminContact: "mailto:admin@example.com"
121
+ }
135
122
  });
136
123
 
137
- // Send the push notification
138
124
  const response = await fetch(endpoint, {
139
125
  method: "POST",
140
126
  headers,
141
- body,
127
+ body
142
128
  });
143
129
 
144
130
  if (response.status === 201) {
145
- console.log("Push notification sent successfully");
146
- } else {
147
- console.error("Failed to send push notification", await response.text());
131
+ console.log("Notification sent");
148
132
  }
149
133
  ```
150
134
 
151
- ## Cross-Platform Support
135
+ ## Understanding Push Subscriptions
152
136
 
153
- PushForge works in all major JavaScript environments:
137
+ When a user subscribes to push notifications, the browser returns a `PushSubscription` object:
154
138
 
155
- ### Node.js 16+
139
+ ```javascript
140
+ {
141
+ // The unique URL for this user's browser push service
142
+ endpoint: "https://fcm.googleapis.com/fcm/send/dAPT...",
156
143
 
157
- ```js
158
- import { buildPushHTTPRequest } from "@pushforge/builder";
159
- // OR
160
- const { buildPushHTTPRequest } = require("@pushforge/builder");
144
+ keys: {
145
+ // Public key for encrypting messages (base64url)
146
+ p256dh: "BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA...",
147
+
148
+ // Authentication secret (base64url)
149
+ auth: "tBHItJI5svbpez7KI4CCXg=="
150
+ }
151
+ }
152
+ ```
153
+
154
+ | Field | Description |
155
+ |-------|-------------|
156
+ | `endpoint` | The push service URL. Each browser vendor has their own (Google FCM, Mozilla autopush, Apple APNs). |
157
+ | `p256dh` | The user's public key for ECDH P-256 message encryption. |
158
+ | `auth` | A shared 16-byte authentication secret. |
159
+
160
+ Store these securely on your server. You'll need them to send notifications to this user.
161
+
162
+ ## API Reference
163
+
164
+ ### `buildPushHTTPRequest(options)`
165
+
166
+ Builds an HTTP request for sending a push notification.
161
167
 
162
- // Use normally - Node.js 16+ has Web Crypto API built-in
168
+ ```typescript
169
+ const { endpoint, headers, body } = await buildPushHTTPRequest({
170
+ privateJWK, // Your VAPID private key (JWK object or JSON string)
171
+ subscription, // User's push subscription
172
+ message: {
173
+ payload, // Any JSON-serializable data
174
+ adminContact, // Contact email (mailto:...) or URL
175
+ options: { // Optional
176
+ ttl, // Time-to-live in seconds (default: 86400)
177
+ urgency, // "very-low" | "low" | "normal" | "high"
178
+ topic // Topic for notification coalescing
179
+ }
180
+ }
181
+ });
182
+ ```
183
+
184
+ **Returns:** `{ endpoint: string, headers: Headers, body: ArrayBuffer }`
185
+
186
+ ## Platform Examples
187
+
188
+ ### Cloudflare Workers
189
+
190
+ ```javascript
191
+ export default {
192
+ async fetch(request, env) {
193
+ const subscription = await request.json();
194
+
195
+ const { endpoint, headers, body } = await buildPushHTTPRequest({
196
+ privateJWK: JSON.parse(env.VAPID_PRIVATE_KEY),
197
+ subscription,
198
+ message: {
199
+ payload: { title: "Hello from the Edge!" },
200
+ adminContact: "mailto:admin@example.com"
201
+ }
202
+ });
203
+
204
+ return fetch(endpoint, { method: "POST", headers, body });
205
+ }
206
+ };
163
207
  ```
164
208
 
165
- ### Browsers
209
+ ### Vercel Edge Functions
166
210
 
167
- ```js
211
+ ```typescript
168
212
  import { buildPushHTTPRequest } from "@pushforge/builder";
169
213
 
170
- // Use in a service worker for push notification handling
214
+ export const config = { runtime: "edge" };
215
+
216
+ export default async function handler(request: Request) {
217
+ const subscription = await request.json();
218
+
219
+ const { endpoint, headers, body } = await buildPushHTTPRequest({
220
+ privateJWK: JSON.parse(process.env.VAPID_PRIVATE_KEY!),
221
+ subscription,
222
+ message: {
223
+ payload: { title: "Edge Notification" },
224
+ adminContact: "mailto:admin@example.com"
225
+ }
226
+ });
227
+
228
+ await fetch(endpoint, { method: "POST", headers, body });
229
+ return new Response("Sent", { status: 200 });
230
+ }
231
+ ```
232
+
233
+ ### Convex
234
+
235
+ ```typescript
236
+ import { action } from "./_generated/server";
237
+ import { buildPushHTTPRequest } from "@pushforge/builder";
238
+ import { v } from "convex/values";
239
+
240
+ export const sendPush = action({
241
+ args: { subscription: v.any(), title: v.string(), body: v.string() },
242
+ handler: async (ctx, { subscription, title, body }) => {
243
+ const { endpoint, headers, body: reqBody } = await buildPushHTTPRequest({
244
+ privateJWK: JSON.parse(process.env.VAPID_PRIVATE_KEY!),
245
+ subscription,
246
+ message: {
247
+ payload: { title, body },
248
+ adminContact: "mailto:admin@example.com"
249
+ }
250
+ });
251
+
252
+ await fetch(endpoint, { method: "POST", headers, body: reqBody });
253
+ }
254
+ });
171
255
  ```
172
256
 
173
257
  ### Deno
174
258
 
175
- ```js
176
- // Import from npm CDN
177
- import { buildPushHTTPRequest } from "https://cdn.jsdelivr.net/npm/@pushforge/builder/dist/lib/main.js";
259
+ ```typescript
260
+ import { buildPushHTTPRequest } from "npm:@pushforge/builder";
261
+
262
+ const { endpoint, headers, body } = await buildPushHTTPRequest({
263
+ privateJWK: JSON.parse(Deno.env.get("VAPID_PRIVATE_KEY")!),
264
+ subscription,
265
+ message: {
266
+ payload: { title: "Hello from Deno!" },
267
+ adminContact: "mailto:admin@example.com"
268
+ }
269
+ });
178
270
 
179
- // Run with --allow-net permissions
271
+ await fetch(endpoint, { method: "POST", headers, body });
180
272
  ```
181
273
 
182
274
  ### Bun
183
275
 
184
- ```js
276
+ ```typescript
185
277
  import { buildPushHTTPRequest } from "@pushforge/builder";
186
278
 
187
- // Works natively in Bun with no special configuration
279
+ const { endpoint, headers, body } = await buildPushHTTPRequest({
280
+ privateJWK: JSON.parse(Bun.env.VAPID_PRIVATE_KEY!),
281
+ subscription,
282
+ message: {
283
+ payload: { title: "Hello from Bun!" },
284
+ adminContact: "mailto:admin@example.com"
285
+ }
286
+ });
287
+
288
+ await fetch(endpoint, { method: "POST", headers, body });
188
289
  ```
189
290
 
190
- ### Cloudflare Workers
291
+ ## Service Worker Setup
292
+
293
+ Handle incoming push notifications in your service worker:
294
+
295
+ ```javascript
296
+ // sw.js
297
+ self.addEventListener('push', (event) => {
298
+ const data = event.data?.json() ?? {};
299
+
300
+ event.waitUntil(
301
+ self.registration.showNotification(data.title, {
302
+ body: data.body,
303
+ icon: data.icon,
304
+ badge: data.badge,
305
+ data: data.url
306
+ })
307
+ );
308
+ });
309
+
310
+ self.addEventListener('notificationclick', (event) => {
311
+ event.notification.close();
312
+
313
+ if (event.notification.data) {
314
+ event.waitUntil(clients.openWindow(event.notification.data));
315
+ }
316
+ });
317
+ ```
318
+
319
+ ## Requirements
320
+
321
+ **Node.js 20+** or any runtime with Web Crypto API support.
322
+
323
+ | Environment | Status |
324
+ |-------------|--------|
325
+ | Node.js 20+ | Fully supported |
326
+ | Cloudflare Workers | Fully supported |
327
+ | Vercel Edge | Fully supported |
328
+ | Deno | Fully supported |
329
+ | Bun | Fully supported |
330
+ | Convex | Fully supported |
331
+ | Modern Browsers | Fully supported |
332
+
333
+ <details>
334
+ <summary>Node.js 18 (requires polyfill)</summary>
335
+
336
+ ```javascript
337
+ import { webcrypto } from "node:crypto";
338
+ globalThis.crypto = webcrypto;
191
339
 
192
- ```js
193
340
  import { buildPushHTTPRequest } from "@pushforge/builder";
194
341
  ```
195
342
 
343
+ Or: `node --experimental-global-webcrypto your-script.js`
344
+
345
+ </details>
346
+
347
+ ## Security
348
+
349
+ PushForge validates all inputs before processing:
350
+
351
+ - VAPID key structure (EC P-256 curve with required x, y, d parameters)
352
+ - Subscription endpoint (must be valid HTTPS URL)
353
+ - p256dh key format (65-byte uncompressed P-256 point)
354
+ - Auth secret length (exactly 16 bytes)
355
+ - Payload size (max 4KB per Web Push spec)
356
+ - TTL bounds (max 24 hours per VAPID spec)
357
+
196
358
  ## License
197
359
 
198
360
  MIT
@@ -1,35 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import { getPublicKeyFromJwk } from '../utils.js';
3
- let webcrypto;
4
- try {
5
- const nodeCrypto = await import('node:crypto');
6
- webcrypto = nodeCrypto.webcrypto;
7
- }
8
- catch {
9
- console.error('Error: This command requires Node.js environment.');
10
- console.error("Please ensure you're running Node.js 16.0.0 or later.");
3
+ if (!globalThis.crypto?.subtle) {
4
+ console.error('Error: Web Crypto API not available.');
5
+ console.error('Please ensure you are running Node.js 20.0.0 or later.');
11
6
  process.exit(1);
12
7
  }
13
8
  async function generateVapidKeys() {
14
9
  try {
15
- console.log('Generating VAPID keys...');
16
- const keypair = await webcrypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']);
17
- const privateJWK = await webcrypto.subtle.exportKey('jwk', keypair.privateKey);
10
+ console.log('Generating VAPID keys...\n');
11
+ const keypair = await globalThis.crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']);
12
+ const privateJWK = await globalThis.crypto.subtle.exportKey('jwk', keypair.privateKey);
18
13
  const privateJWKWithAlg = { alg: 'ES256', ...privateJWK };
19
14
  const publicKey = getPublicKeyFromJwk(privateJWKWithAlg);
20
- // Display in a nice formatted output
21
- const resultText = `
22
- VAPID Keys Generated Successfully
23
-
24
- Public Key:
25
- ${publicKey}
26
-
27
- Private Key (JWK):
28
- ${JSON.stringify(privateJWKWithAlg, null, 2)}
29
-
30
- Store these keys securely. Never expose your private key.
31
- `;
32
- console.log(resultText);
15
+ console.log('VAPID Keys Generated Successfully\n');
16
+ console.log('Public Key:');
17
+ console.log(publicKey);
18
+ console.log('\nPrivate Key (JWK):');
19
+ console.log(JSON.stringify(privateJWKWithAlg));
20
+ console.log('\nStore these keys securely. Never expose your private key.');
33
21
  }
34
22
  catch (error) {
35
23
  console.error('Error generating VAPID keys:');
@@ -39,23 +27,39 @@ Store these keys securely. Never expose your private key.
39
27
  else {
40
28
  console.error('An unknown error occurred.');
41
29
  }
42
- console.error('\nThis tool requires Node.js v16.0.0 or later with WebCrypto API support.');
30
+ console.error('\nThis tool requires Node.js 20.0.0 or later with Web Crypto API support.');
43
31
  process.exit(1);
44
32
  }
45
33
  }
46
- // Simple command parsing
47
- const args = process.argv.slice(2);
48
- const command = args[0];
49
- if (command === 'generate-vapid-keys') {
50
- generateVapidKeys();
51
- }
52
- else {
34
+ function showHelp() {
53
35
  console.log(`
54
- PushForge CLI Tools
36
+ PushForge CLI
37
+
38
+ Usage: npx @pushforge/builder <command>
39
+
40
+ Commands:
41
+ vapid Generate VAPID key pair for Web Push authentication
42
+ help Show this help message
55
43
 
56
- Usage:
57
- npx @pushforge/builder generate-vapid-keys Generate VAPID key pair for Web Push Authentication
44
+ Examples:
45
+ npx @pushforge/builder vapid
58
46
 
59
- For more information, visit: https://github.com/draphy/pushforge
60
- `);
47
+ Documentation: https://github.com/draphy/pushforge#readme
48
+ `);
49
+ }
50
+ // Parse command
51
+ const args = process.argv.slice(2);
52
+ const command = args[0]?.toLowerCase();
53
+ switch (command) {
54
+ case 'vapid':
55
+ generateVapidKeys();
56
+ break;
57
+ case 'help':
58
+ case '--help':
59
+ case '-h':
60
+ showHelp();
61
+ break;
62
+ default:
63
+ showHelp();
64
+ process.exit(command ? 1 : 0);
61
65
  }
@@ -2,28 +2,20 @@
2
2
  /// <reference types="node" />
3
3
  /**
4
4
  * A module that provides a cross-platform cryptographic interface.
5
+ * Uses globalThis.crypto which is available in:
6
+ * - Node.js 20+ (current LTS)
7
+ * - Browsers
8
+ * - Cloudflare Workers
9
+ * - Deno
10
+ * - Bun
11
+ * - Convex
5
12
  *
6
13
  * @module crypto
7
14
  */
8
- let isomorphicCrypto;
9
- // Cloudflare Worker, Deno, Bun and Browser environments have crypto globally available
10
- if (globalThis.crypto?.subtle) {
11
- isomorphicCrypto = globalThis.crypto;
12
- }
13
- // Node.js requires importing the webcrypto module
14
- else if (typeof process !== 'undefined' && process.versions?.node) {
15
- try {
16
- const { webcrypto } = await import('node:crypto');
17
- isomorphicCrypto = webcrypto;
18
- }
19
- catch {
20
- throw new Error('Crypto API not available in this Node.js environment. Please use Node.js 16+ which supports the Web Crypto API.');
21
- }
22
- }
23
- // Fallback error for unsupported environments
24
- else {
25
- throw new Error('No Web Crypto API implementation available in this environment.');
15
+ if (!globalThis.crypto?.subtle) {
16
+ throw new Error('Web Crypto API not available. Ensure you are using Node.js 20+ or a modern runtime with globalThis.crypto support.');
26
17
  }
18
+ const isomorphicCrypto = globalThis.crypto;
27
19
  /**
28
20
  * A cryptographic interface that provides methods for generating random values
29
21
  * and accessing subtle cryptographic operations.
@@ -8,4 +8,4 @@ import type { PushSubscription } from './types.js';
8
8
  * @param {PushSubscription} target - The target push subscription containing client keys.
9
9
  * @returns {Promise<ArrayBuffer>} A promise that resolves to the encrypted payload.
10
10
  */
11
- export declare const encryptPayload: (localKeys: CryptoKeyPair, salt: Uint8Array, payload: string, target: PushSubscription) => Promise<ArrayBuffer>;
11
+ export declare const encryptPayload: (localKeys: CryptoKeyPair, salt: Uint8Array<ArrayBuffer>, payload: string, target: PushSubscription) => Promise<ArrayBuffer>;
@@ -28,6 +28,14 @@ const importClientKeys = async (keys) => {
28
28
  // Node.js environment
29
29
  decodedKey = new Uint8Array(Buffer.from(base64Key, 'base64'));
30
30
  }
31
+ // Validate p256dh key format: must be 65 bytes (uncompressed P-256 point)
32
+ // Format: 0x04 (1 byte) + x coordinate (32 bytes) + y coordinate (32 bytes)
33
+ if (decodedKey.byteLength !== 65) {
34
+ throw new Error(`Invalid p256dh key: expected 65 bytes but got ${decodedKey.byteLength} bytes`);
35
+ }
36
+ if (decodedKey[0] !== 0x04) {
37
+ throw new Error(`Invalid p256dh key: expected uncompressed point format (0x04 prefix) but got 0x${decodedKey[0].toString(16).padStart(2, '0')}`);
38
+ }
31
39
  const p256 = await crypto.subtle.importKey('jwk', {
32
40
  kty: 'EC',
33
41
  crv: 'P-256',
@@ -107,6 +115,17 @@ const deriveContentEncryptionKey = async (pseudoRandomKey, salt, context) => {
107
115
  const bits = await crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt, info }, pseudoRandomKey, 16 * 8);
108
116
  return crypto.subtle.importKey('raw', bits, 'AES-GCM', false, ['encrypt']);
109
117
  };
118
+ /**
119
+ * Maximum payload size after accounting for encryption overhead.
120
+ * Web push payloads have an overall max size of 4KB (4096 bytes).
121
+ * With the required overhead (16 bytes auth tag + 2 bytes padding length),
122
+ * the actual max payload size is 4078 bytes.
123
+ */
124
+ const MAX_PAYLOAD_SIZE = 4078;
125
+ /**
126
+ * Minimum required size for padding length prefix (2 bytes).
127
+ */
128
+ const PADDING_LENGTH_PREFIX_SIZE = 2;
110
129
  /**
111
130
  * Pads the payload to ensure it fits within the maximum allowed size for web push notifications.
112
131
  *
@@ -114,17 +133,22 @@ const deriveContentEncryptionKey = async (pseudoRandomKey, salt, context) => {
114
133
  * required overhead for encryption, the actual max payload size is 4078 bytes.
115
134
  *
116
135
  * @param {Uint8Array} payload - The original payload to be padded.
117
- * @returns {Uint8Array} The padded payload, including length information.
136
+ * @returns {Uint8Array<ArrayBuffer>} The padded payload, including length information.
137
+ * @throws {Error} Throws an error if the payload exceeds the maximum allowed size.
118
138
  */
119
139
  const padPayload = (payload) => {
120
- const MAX_PAYLOAD_SIZE = 4078; // Maximum payload size after encryption overhead
121
- let paddingSize = Math.round(Math.random() * 100); // Random padding size
122
- const payloadSizeWithPadding = payload.byteLength + 2 + paddingSize;
123
- if (payloadSizeWithPadding > MAX_PAYLOAD_SIZE) {
124
- // Adjust padding size if the total exceeds the maximum allowed size
125
- paddingSize -= payloadSizeWithPadding - MAX_PAYLOAD_SIZE;
140
+ const maxPayloadContentSize = MAX_PAYLOAD_SIZE - PADDING_LENGTH_PREFIX_SIZE;
141
+ if (payload.byteLength > maxPayloadContentSize) {
142
+ throw new Error(`Payload too large. Maximum size is ${maxPayloadContentSize} bytes, but received ${payload.byteLength} bytes`);
126
143
  }
127
- const paddingArray = new ArrayBuffer(2 + paddingSize);
144
+ // Calculate available space for padding
145
+ const availableSpace = MAX_PAYLOAD_SIZE - PADDING_LENGTH_PREFIX_SIZE - payload.byteLength;
146
+ // Generate random padding size, clamped to available space
147
+ const maxRandomPadding = Math.min(100, availableSpace);
148
+ const paddingSize = maxRandomPadding > 0
149
+ ? Math.floor(Math.random() * (maxRandomPadding + 1))
150
+ : 0;
151
+ const paddingArray = new ArrayBuffer(PADDING_LENGTH_PREFIX_SIZE + paddingSize);
128
152
  new DataView(paddingArray).setUint16(0, paddingSize); // Store the length of the padding
129
153
  // Return the new payload with padding added
130
154
  return concatTypedArrays([new Uint8Array(paddingArray), payload]);
@@ -1,6 +1,47 @@
1
1
  import { crypto } from './crypto.js';
2
2
  import { encryptPayload } from './payload.js';
3
3
  import { vapidHeaders } from './vapid.js';
4
+ /**
5
+ * Validates that a JWK has the required properties for ECDSA P-256.
6
+ *
7
+ * @param {JsonWebKey} jwk - The JSON Web Key to validate.
8
+ * @throws {Error} Throws if the JWK is missing required properties or has invalid values.
9
+ */
10
+ const validatePrivateJWK = (jwk) => {
11
+ if (jwk.kty !== 'EC') {
12
+ throw new Error(`Invalid JWK: 'kty' must be 'EC', received '${jwk.kty ?? 'undefined'}'`);
13
+ }
14
+ if (jwk.crv !== 'P-256') {
15
+ throw new Error(`Invalid JWK: 'crv' must be 'P-256', received '${jwk.crv ?? 'undefined'}'`);
16
+ }
17
+ if (!jwk.x || typeof jwk.x !== 'string') {
18
+ throw new Error("Invalid JWK: missing or invalid 'x' coordinate");
19
+ }
20
+ if (!jwk.y || typeof jwk.y !== 'string') {
21
+ throw new Error("Invalid JWK: missing or invalid 'y' coordinate");
22
+ }
23
+ if (!jwk.d || typeof jwk.d !== 'string') {
24
+ throw new Error("Invalid JWK: missing or invalid 'd' (private key)");
25
+ }
26
+ };
27
+ /**
28
+ * Validates that the subscription endpoint is a valid HTTPS URL.
29
+ *
30
+ * @param {string} endpoint - The push subscription endpoint URL.
31
+ * @throws {Error} Throws if the endpoint is not a valid HTTPS URL.
32
+ */
33
+ const validateEndpoint = (endpoint) => {
34
+ let url;
35
+ try {
36
+ url = new URL(endpoint);
37
+ }
38
+ catch {
39
+ throw new Error(`Invalid subscription endpoint: '${endpoint}' is not a valid URL`);
40
+ }
41
+ if (url.protocol !== 'https:') {
42
+ throw new Error(`Invalid subscription endpoint: push endpoints must use HTTPS, received '${url.protocol}'`);
43
+ }
44
+ };
4
45
  /**
5
46
  * Builds an HTTP request body and headers for sending a push notification.
6
47
  *
@@ -57,7 +98,17 @@ import { vapidHeaders } from './vapid.js';
57
98
  */
58
99
  export async function buildPushHTTPRequest({ privateJWK, message, subscription, }) {
59
100
  // Parse the private JWK if it's a string
60
- const jwk = typeof privateJWK === 'string' ? JSON.parse(privateJWK) : privateJWK;
101
+ let jwk;
102
+ try {
103
+ jwk = typeof privateJWK === 'string' ? JSON.parse(privateJWK) : privateJWK;
104
+ }
105
+ catch {
106
+ throw new Error('Invalid privateJWK: failed to parse JSON string');
107
+ }
108
+ // Validate the JWK structure
109
+ validatePrivateJWK(jwk);
110
+ // Validate the subscription endpoint
111
+ validateEndpoint(subscription.endpoint);
61
112
  const MAX_TTL = 24 * 60 * 60;
62
113
  if (message.options?.ttl && message.options.ttl > MAX_TTL) {
63
114
  throw new Error('TTL must be less than 24 hours');
@@ -48,6 +48,6 @@ export declare const getPublicKeyFromJwk: (jwk: JsonWebKey) => string;
48
48
  * Concatenates multiple Uint8Array instances into a single Uint8Array.
49
49
  *
50
50
  * @param {Uint8Array[]} arrays - An array of Uint8Array instances to concatenate.
51
- * @returns {Uint8Array} A new Uint8Array containing all the concatenated data.
51
+ * @returns {Uint8Array<ArrayBuffer>} A new Uint8Array containing all the concatenated data.
52
52
  */
53
- export declare const concatTypedArrays: (arrays: Uint8Array[]) => Uint8Array;
53
+ export declare const concatTypedArrays: (arrays: readonly Uint8Array[]) => Uint8Array<ArrayBuffer>;
package/dist/lib/utils.js CHANGED
@@ -60,7 +60,7 @@ export const getPublicKeyFromJwk = (jwk) => base64UrlEncode(`\x04${base64Decode(
60
60
  * Concatenates multiple Uint8Array instances into a single Uint8Array.
61
61
  *
62
62
  * @param {Uint8Array[]} arrays - An array of Uint8Array instances to concatenate.
63
- * @returns {Uint8Array} A new Uint8Array containing all the concatenated data.
63
+ * @returns {Uint8Array<ArrayBuffer>} A new Uint8Array containing all the concatenated data.
64
64
  */
65
65
  export const concatTypedArrays = (arrays) => {
66
66
  const length = arrays.reduce((accumulator, current) => accumulator + current.byteLength, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushforge/builder",
3
- "version": "1.1.2",
3
+ "version": "2.0.1",
4
4
  "description": "A robust, cross-platform Web Push notification library that handles VAPID authentication and payload encryption following the Web Push Protocol standard. Works in Node.js 16+, Browsers, Deno, Bun and Cloudflare Workers.",
5
5
  "private": false,
6
6
  "main": "dist/lib/main.js",
@@ -14,7 +14,7 @@
14
14
  "url": "https://github.com/draphy/pushforge/issues"
15
15
  },
16
16
  "engines": {
17
- "node": ">=16.0.0"
17
+ "node": ">=20.0.0"
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
@@ -45,7 +45,7 @@
45
45
  "semantic-release": "semantic-release"
46
46
  },
47
47
  "bin": {
48
- "generate-vapid-keys": "./dist/lib/commandLine/keys.js"
48
+ "pushforge": "./dist/lib/commandLine/keys.js"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@semantic-release/commit-analyzer": "^11.1.0",