@pushforge/builder 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/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/lib/base64.d.ts +31 -0
- package/dist/lib/base64.js +67 -0
- package/dist/lib/commandLine/keys.d.ts +2 -0
- package/dist/lib/commandLine/keys.js +61 -0
- package/dist/lib/crypto.d.ts +20 -0
- package/dist/lib/crypto.js +48 -0
- package/dist/lib/jwt.d.ts +19 -0
- package/dist/lib/jwt.js +38 -0
- package/dist/lib/main.d.ts +2 -0
- package/dist/lib/main.js +1 -0
- package/dist/lib/payload.d.ts +11 -0
- package/dist/lib/payload.js +151 -0
- package/dist/lib/request.d.ts +60 -0
- package/dist/lib/request.js +98 -0
- package/dist/lib/shared-secret.d.ts +13 -0
- package/dist/lib/shared-secret.js +17 -0
- package/dist/lib/types.d.ts +137 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/utils.d.ts +53 -0
- package/dist/lib/utils.js +74 -0
- package/dist/lib/vapid.d.ts +16 -0
- package/dist/lib/vapid.js +54 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 David Raphi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# PushForge Builder
|
|
2
|
+
|
|
3
|
+
A robust, cross-platform Web Push notification library that handles VAPID authentication and payload encryption following the Web Push Protocol standard.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Choose your preferred package manager:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# NPM
|
|
11
|
+
npm install @pushforge/builder
|
|
12
|
+
|
|
13
|
+
# Yarn
|
|
14
|
+
yarn add @pushforge/builder
|
|
15
|
+
|
|
16
|
+
# pnpm
|
|
17
|
+
pnpm add @pushforge/builder
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
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
|
|
27
|
+
|
|
28
|
+
## Getting Started
|
|
29
|
+
|
|
30
|
+
### Step 1: Generate VAPID Keys
|
|
31
|
+
|
|
32
|
+
PushForge includes a built-in CLI tool to generate VAPID keys for Web Push Authentication:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Generate VAPID keys using npx
|
|
36
|
+
npx @pushforge/builder generate-vapid-keys
|
|
37
|
+
|
|
38
|
+
# Using yarn
|
|
39
|
+
yarn dlx @pushforge/builder generate-vapid-keys
|
|
40
|
+
|
|
41
|
+
# Using pnpm
|
|
42
|
+
pnpm dlx @pushforge/builder generate-vapid-keys
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This will output a public key and private key that you can use for VAPID authentication:
|
|
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
|
+
```
|
|
69
|
+
|
|
70
|
+
**Requirements:**
|
|
71
|
+
|
|
72
|
+
- Node.js 16.0.0 or later
|
|
73
|
+
- The command uses the WebCrypto API which is built-in to Node.js 16+
|
|
74
|
+
|
|
75
|
+
### Step 2: Set Up Push Notifications in Your Web Application
|
|
76
|
+
|
|
77
|
+
To implement push notifications in your web application, you'll need to:
|
|
78
|
+
|
|
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
|
|
83
|
+
|
|
84
|
+
When implementing your service worker, handle push events to display notifications when they arrive:
|
|
85
|
+
|
|
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
|
|
90
|
+
|
|
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.
|
|
92
|
+
|
|
93
|
+
### Step 3: Send Push Notifications from Your Server
|
|
94
|
+
|
|
95
|
+
On your backend server, use the VAPID private key to send push notifications:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { buildPushHTTPRequest } from "@pushforge/builder";
|
|
99
|
+
|
|
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;
|
|
103
|
+
|
|
104
|
+
// User subscription from browser push API
|
|
105
|
+
const subscription = {
|
|
106
|
+
endpoint: "https://fcm.googleapis.com/fcm/send/DEVICE_TOKEN",
|
|
107
|
+
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
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Build the push notification request
|
|
131
|
+
const {endpoint, headers, body} = await buildPushHTTPRequest({
|
|
132
|
+
privateJWK,
|
|
133
|
+
message,
|
|
134
|
+
subscription,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Send the push notification
|
|
138
|
+
const response = await fetch(endpoint, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers,
|
|
141
|
+
body,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (response.status === 201) {
|
|
145
|
+
console.log("Push notification sent successfully");
|
|
146
|
+
} else {
|
|
147
|
+
console.error("Failed to send push notification", await response.text());
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Cross-Platform Support
|
|
152
|
+
|
|
153
|
+
PushForge works in all major JavaScript environments:
|
|
154
|
+
|
|
155
|
+
### Node.js 16+
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
import { buildPushHTTPRequest } from "@pushforge/builder";
|
|
159
|
+
// OR
|
|
160
|
+
const { buildPushHTTPRequest } = require("@pushforge/builder");
|
|
161
|
+
|
|
162
|
+
// Use normally - Node.js 16+ has Web Crypto API built-in
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Browsers
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
import { buildPushHTTPRequest } from "@pushforge/builder";
|
|
169
|
+
|
|
170
|
+
// Use in a service worker for push notification handling
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Deno
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
// Import from npm CDN
|
|
177
|
+
import { buildPushHTTPRequest } from "https://cdn.jsdelivr.net/npm/@pushforge/builder/dist/lib/main.js";
|
|
178
|
+
|
|
179
|
+
// Run with --allow-net permissions
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Bun
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
import { buildPushHTTPRequest } from "@pushforge/builder";
|
|
186
|
+
|
|
187
|
+
// Works natively in Bun with no special configuration
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Cloudflare Workers
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
import { buildPushHTTPRequest } from "@pushforge/builder";
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encodes a string, ArrayBuffer, or Uint8Array into a Base64 URL format.
|
|
3
|
+
*
|
|
4
|
+
* This function handles both browser and Node.js environments by using
|
|
5
|
+
* the appropriate encoding method based on the environment.
|
|
6
|
+
*
|
|
7
|
+
* @param {string | ArrayBuffer | Uint8Array} input - The input to encode.
|
|
8
|
+
* @returns {string} The Base64 URL encoded string.
|
|
9
|
+
*/
|
|
10
|
+
export declare const base64UrlEncode: (input: string | ArrayBuffer | Uint8Array) => string;
|
|
11
|
+
/**
|
|
12
|
+
* Decodes a Base64 URL encoded string back to its original format.
|
|
13
|
+
*
|
|
14
|
+
* This function handles both browser and Node.js environments by using
|
|
15
|
+
* the appropriate decoding method based on the environment.
|
|
16
|
+
*
|
|
17
|
+
* @param {string | undefined} s - The Base64 URL encoded string to decode.
|
|
18
|
+
* @returns {string} The decoded data as a string.
|
|
19
|
+
* @throws {Error} Throws an error if the input string is invalid.
|
|
20
|
+
*/
|
|
21
|
+
export declare const base64UrlDecodeString: (s: string | undefined) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Decodes a Base64 URL encoded string into an ArrayBuffer.
|
|
24
|
+
*
|
|
25
|
+
* This function handles both browser and Node.js environments by using
|
|
26
|
+
* the appropriate decoding method based on the environment.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} input - The Base64 URL encoded string to decode.
|
|
29
|
+
* @returns {ArrayBuffer} The decoded data as an ArrayBuffer.
|
|
30
|
+
*/
|
|
31
|
+
export declare const base64UrlDecode: (input: string) => ArrayBuffer;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { stringFromArrayBuffer } from './utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Encodes a string, ArrayBuffer, or Uint8Array into a Base64 URL format.
|
|
4
|
+
*
|
|
5
|
+
* This function handles both browser and Node.js environments by using
|
|
6
|
+
* the appropriate encoding method based on the environment.
|
|
7
|
+
*
|
|
8
|
+
* @param {string | ArrayBuffer | Uint8Array} input - The input to encode.
|
|
9
|
+
* @returns {string} The Base64 URL encoded string.
|
|
10
|
+
*/
|
|
11
|
+
export const base64UrlEncode = (input) => {
|
|
12
|
+
// Convert input to string if it's binary
|
|
13
|
+
const text = typeof input === 'string' ? input : stringFromArrayBuffer(input);
|
|
14
|
+
// Use environment-specific encoding
|
|
15
|
+
let base64;
|
|
16
|
+
if (typeof globalThis !== 'undefined' && 'btoa' in globalThis) {
|
|
17
|
+
// Browser environment
|
|
18
|
+
base64 = globalThis.btoa(text);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// Node.js environment
|
|
22
|
+
base64 = Buffer.from(text, 'binary').toString('base64');
|
|
23
|
+
}
|
|
24
|
+
// Convert base64 to base64url format
|
|
25
|
+
return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Decodes a Base64 URL encoded string back to its original format.
|
|
29
|
+
*
|
|
30
|
+
* This function handles both browser and Node.js environments by using
|
|
31
|
+
* the appropriate decoding method based on the environment.
|
|
32
|
+
*
|
|
33
|
+
* @param {string | undefined} s - The Base64 URL encoded string to decode.
|
|
34
|
+
* @returns {string} The decoded data as a string.
|
|
35
|
+
* @throws {Error} Throws an error if the input string is invalid.
|
|
36
|
+
*/
|
|
37
|
+
export const base64UrlDecodeString = (s) => {
|
|
38
|
+
if (!s)
|
|
39
|
+
throw new Error('Invalid input');
|
|
40
|
+
return (s.replace(/-/g, '+').replace(/_/g, '/') +
|
|
41
|
+
'='.repeat((4 - (s.length % 4)) % 4));
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Decodes a Base64 URL encoded string into an ArrayBuffer.
|
|
45
|
+
*
|
|
46
|
+
* This function handles both browser and Node.js environments by using
|
|
47
|
+
* the appropriate decoding method based on the environment.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} input - The Base64 URL encoded string to decode.
|
|
50
|
+
* @returns {ArrayBuffer} The decoded data as an ArrayBuffer.
|
|
51
|
+
*/
|
|
52
|
+
export const base64UrlDecode = (input) => {
|
|
53
|
+
// Convert from base64url to base64
|
|
54
|
+
const base64 = base64UrlDecodeString(input);
|
|
55
|
+
// Decode based on environment
|
|
56
|
+
if (typeof globalThis !== 'undefined' && 'atob' in globalThis) {
|
|
57
|
+
// Browser environment
|
|
58
|
+
const binaryString = globalThis.atob(base64);
|
|
59
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
60
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
61
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
62
|
+
}
|
|
63
|
+
return bytes.buffer;
|
|
64
|
+
}
|
|
65
|
+
// Node.js environment
|
|
66
|
+
return Buffer.from(base64, 'base64').buffer;
|
|
67
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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.");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
async function generateVapidKeys() {
|
|
14
|
+
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);
|
|
18
|
+
const privateJWKWithAlg = { alg: 'ES256', ...privateJWK };
|
|
19
|
+
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);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error('Error generating VAPID keys:');
|
|
36
|
+
if (error instanceof Error) {
|
|
37
|
+
console.error(error.message);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.error('An unknown error occurred.');
|
|
41
|
+
}
|
|
42
|
+
console.error('\nThis tool requires Node.js v16.0.0 or later with WebCrypto API support.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
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 {
|
|
53
|
+
console.log(`
|
|
54
|
+
PushForge CLI Tools
|
|
55
|
+
|
|
56
|
+
Usage:
|
|
57
|
+
npx @pushforge/builder generate-vapid-keys Generate VAPID key pair for Web Push Authentication
|
|
58
|
+
|
|
59
|
+
For more information, visit: https://github.com/draphy/pushforge
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cryptographic interface that provides methods for generating random values
|
|
3
|
+
* and accessing subtle cryptographic operations.
|
|
4
|
+
*/
|
|
5
|
+
export declare const crypto: {
|
|
6
|
+
/**
|
|
7
|
+
* Fills the given typed array with cryptographically secure random values.
|
|
8
|
+
*
|
|
9
|
+
* @param {T} array - The typed array to fill with random values.
|
|
10
|
+
* @returns {T} The filled typed array.
|
|
11
|
+
* @template T - The type of the typed array (e.g., Uint8Array).
|
|
12
|
+
*/
|
|
13
|
+
getRandomValues<T extends Uint8Array>(array: T): T;
|
|
14
|
+
/**
|
|
15
|
+
* Provides access to subtle cryptographic operations.
|
|
16
|
+
*
|
|
17
|
+
* @type {SubtleCrypto} The subtle cryptographic interface.
|
|
18
|
+
*/
|
|
19
|
+
subtle: SubtleCrypto;
|
|
20
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/// <reference lib="webworker" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
/**
|
|
4
|
+
* A module that provides a cross-platform cryptographic interface.
|
|
5
|
+
*
|
|
6
|
+
* @module crypto
|
|
7
|
+
*/
|
|
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.');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A cryptographic interface that provides methods for generating random values
|
|
29
|
+
* and accessing subtle cryptographic operations.
|
|
30
|
+
*/
|
|
31
|
+
export const crypto = {
|
|
32
|
+
/**
|
|
33
|
+
* Fills the given typed array with cryptographically secure random values.
|
|
34
|
+
*
|
|
35
|
+
* @param {T} array - The typed array to fill with random values.
|
|
36
|
+
* @returns {T} The filled typed array.
|
|
37
|
+
* @template T - The type of the typed array (e.g., Uint8Array).
|
|
38
|
+
*/
|
|
39
|
+
getRandomValues(array) {
|
|
40
|
+
return isomorphicCrypto.getRandomValues(array);
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Provides access to subtle cryptographic operations.
|
|
44
|
+
*
|
|
45
|
+
* @type {SubtleCrypto} The subtle cryptographic interface.
|
|
46
|
+
*/
|
|
47
|
+
subtle: isomorphicCrypto.subtle,
|
|
48
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JwtData } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a JSON Web Token (JWT) using the ECDSA algorithm.
|
|
4
|
+
*
|
|
5
|
+
* This function takes a JSON Web Key (JWK) and JWT data, encodes them,
|
|
6
|
+
* and signs the token using the specified algorithm and hash function.
|
|
7
|
+
*
|
|
8
|
+
* In the Web Push protocol, the VAPID JWT includes an `exp` (expiration) claim
|
|
9
|
+
* that specifies the token's validity period. According to the VAPID specification,
|
|
10
|
+
* the `exp` value must not exceed 24 hours from the time of the request. If it does,
|
|
11
|
+
* the push service (like FCM) will reject the request with a 403 Forbidden error.
|
|
12
|
+
*
|
|
13
|
+
* @param {JsonWebKey} jwk - The JSON Web Key used for signing the JWT.
|
|
14
|
+
* @param {JwtData} jwtData - The data to be included in the JWT payload.
|
|
15
|
+
* @returns {Promise<string>} A promise that resolves to the signed JWT as a string.
|
|
16
|
+
*
|
|
17
|
+
* @throws {Error} Throws an error if the key import or signing process fails.
|
|
18
|
+
*/
|
|
19
|
+
export declare const createJwt: (jwk: JsonWebKey, jwtData: JwtData) => Promise<string>;
|
package/dist/lib/jwt.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { base64UrlEncode } from './base64.js';
|
|
2
|
+
import { crypto } from './crypto.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a JSON Web Token (JWT) using the ECDSA algorithm.
|
|
5
|
+
*
|
|
6
|
+
* This function takes a JSON Web Key (JWK) and JWT data, encodes them,
|
|
7
|
+
* and signs the token using the specified algorithm and hash function.
|
|
8
|
+
*
|
|
9
|
+
* In the Web Push protocol, the VAPID JWT includes an `exp` (expiration) claim
|
|
10
|
+
* that specifies the token's validity period. According to the VAPID specification,
|
|
11
|
+
* the `exp` value must not exceed 24 hours from the time of the request. If it does,
|
|
12
|
+
* the push service (like FCM) will reject the request with a 403 Forbidden error.
|
|
13
|
+
*
|
|
14
|
+
* @param {JsonWebKey} jwk - The JSON Web Key used for signing the JWT.
|
|
15
|
+
* @param {JwtData} jwtData - The data to be included in the JWT payload.
|
|
16
|
+
* @returns {Promise<string>} A promise that resolves to the signed JWT as a string.
|
|
17
|
+
*
|
|
18
|
+
* @throws {Error} Throws an error if the key import or signing process fails.
|
|
19
|
+
*/
|
|
20
|
+
export const createJwt = async (jwk, jwtData) => {
|
|
21
|
+
// JWT header information
|
|
22
|
+
const jwtInfo = {
|
|
23
|
+
typ: 'JWT', // Type of the token
|
|
24
|
+
alg: 'ES256', // Algorithm used for signing
|
|
25
|
+
};
|
|
26
|
+
// Encode the JWT header and payload
|
|
27
|
+
const base64JwtInfo = base64UrlEncode(JSON.stringify(jwtInfo));
|
|
28
|
+
const base64JwtData = base64UrlEncode(JSON.stringify(jwtData));
|
|
29
|
+
const unsignedToken = `${base64JwtInfo}.${base64JwtData}`;
|
|
30
|
+
// Import the private key from the JWK
|
|
31
|
+
const privateKey = await crypto.subtle.importKey('jwk', jwk, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign']);
|
|
32
|
+
// Sign the token using the ECDSA algorithm and SHA-256 hash
|
|
33
|
+
const signature = await crypto.subtle
|
|
34
|
+
.sign({ name: 'ECDSA', hash: { name: 'SHA-256' } }, privateKey, new TextEncoder().encode(unsignedToken))
|
|
35
|
+
.then((token) => base64UrlEncode(token));
|
|
36
|
+
// Return the complete JWT
|
|
37
|
+
return `${base64JwtInfo}.${base64JwtData}.${signature}`;
|
|
38
|
+
};
|
package/dist/lib/main.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildPushHTTPRequest } from './request.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PushSubscription } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Encrypts the payload for a push notification using the provided keys and context.
|
|
4
|
+
*
|
|
5
|
+
* @param {CryptoKeyPair} localKeys - The local key pair used for encryption.
|
|
6
|
+
* @param {Uint8Array} salt - The salt used in the encryption process.
|
|
7
|
+
* @param {string} payload - The original payload to encrypt.
|
|
8
|
+
* @param {PushSubscription} target - The target push subscription containing client keys.
|
|
9
|
+
* @returns {Promise<ArrayBuffer>} A promise that resolves to the encrypted payload.
|
|
10
|
+
*/
|
|
11
|
+
export declare const encryptPayload: (localKeys: CryptoKeyPair, salt: Uint8Array, payload: string, target: PushSubscription) => Promise<ArrayBuffer>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { base64UrlDecode, base64UrlDecodeString, base64UrlEncode, } from './base64.js';
|
|
2
|
+
import { crypto } from './crypto.js';
|
|
3
|
+
import { deriveSharedSecret } from './shared-secret.js';
|
|
4
|
+
import { concatTypedArrays } from './utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Imports and validates the client's public and authentication keys from a PushSubscriptionKey.
|
|
7
|
+
*
|
|
8
|
+
* @param {PushSubscriptionKey} keys - The keys associated with the push subscription.
|
|
9
|
+
* @returns {Promise<{ auth: ArrayBuffer, p256: CryptoKey }>} A promise that resolves to an object containing the authentication key and the imported public key.
|
|
10
|
+
* @throws {Error} Throws an error if the authentication key length is incorrect.
|
|
11
|
+
*/
|
|
12
|
+
const importClientKeys = async (keys) => {
|
|
13
|
+
const auth = base64UrlDecode(keys.auth);
|
|
14
|
+
if (auth.byteLength !== 16) {
|
|
15
|
+
throw new Error(`Incorrect auth length, expected 16 bytes but got ${auth.byteLength}`);
|
|
16
|
+
}
|
|
17
|
+
let decodedKey;
|
|
18
|
+
const base64Key = base64UrlDecodeString(keys.p256dh);
|
|
19
|
+
if (typeof globalThis !== 'undefined' && 'atob' in globalThis) {
|
|
20
|
+
// Browser environment
|
|
21
|
+
const binaryStr = globalThis.atob(base64Key);
|
|
22
|
+
decodedKey = new Uint8Array(binaryStr.length);
|
|
23
|
+
for (let i = 0; i < binaryStr.length; i++) {
|
|
24
|
+
decodedKey[i] = binaryStr.charCodeAt(i);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Node.js environment
|
|
29
|
+
decodedKey = new Uint8Array(Buffer.from(base64Key, 'base64'));
|
|
30
|
+
}
|
|
31
|
+
const p256 = await crypto.subtle.importKey('jwk', {
|
|
32
|
+
kty: 'EC',
|
|
33
|
+
crv: 'P-256',
|
|
34
|
+
x: base64UrlEncode(decodedKey.slice(1, 33)),
|
|
35
|
+
y: base64UrlEncode(decodedKey.slice(33, 65)),
|
|
36
|
+
ext: true,
|
|
37
|
+
}, { name: 'ECDH', namedCurve: 'P-256' }, true, []);
|
|
38
|
+
return { auth, p256 };
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Derives a pseudo-random key using HKDF from the shared secret and authentication key.
|
|
42
|
+
*
|
|
43
|
+
* @param {ArrayBuffer} auth - The authentication key used as salt in the derivation process.
|
|
44
|
+
* @param {CryptoKey} sharedSecret - The shared secret derived from the client's public key and local private key.
|
|
45
|
+
* @returns {Promise<CryptoKey>} A promise that resolves to the derived pseudo-random key.
|
|
46
|
+
*/
|
|
47
|
+
const derivePseudoRandomKey = async (auth, sharedSecret) => {
|
|
48
|
+
const pseudoRandomKeyBytes = await crypto.subtle.deriveBits({
|
|
49
|
+
name: 'HKDF',
|
|
50
|
+
hash: 'SHA-256',
|
|
51
|
+
salt: auth,
|
|
52
|
+
// Adding Content-Encoding data info here is required by the Web Push API
|
|
53
|
+
info: new TextEncoder().encode('Content-Encoding: auth\0'),
|
|
54
|
+
}, sharedSecret, 256);
|
|
55
|
+
return crypto.subtle.importKey('raw', pseudoRandomKeyBytes, 'HKDF', false, [
|
|
56
|
+
'deriveBits',
|
|
57
|
+
]);
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Creates a context for the ECDH key exchange using the client's and local public keys.
|
|
61
|
+
*
|
|
62
|
+
* @param {CryptoKey} clientPublicKey - The client's public key.
|
|
63
|
+
* @param {CryptoKey} localPublicKey - The local public key.
|
|
64
|
+
* @returns {Promise<Uint8Array>} A promise that resolves to a concatenated context array.
|
|
65
|
+
*/
|
|
66
|
+
const createContext = async (clientPublicKey, localPublicKey) => {
|
|
67
|
+
const [clientKeyBytes, localKeyBytes] = await Promise.all([
|
|
68
|
+
crypto.subtle.exportKey('raw', clientPublicKey),
|
|
69
|
+
crypto.subtle.exportKey('raw', localPublicKey),
|
|
70
|
+
]);
|
|
71
|
+
return concatTypedArrays([
|
|
72
|
+
new TextEncoder().encode('P-256\0'),
|
|
73
|
+
new Uint8Array([0, clientKeyBytes.byteLength]),
|
|
74
|
+
new Uint8Array(clientKeyBytes),
|
|
75
|
+
new Uint8Array([0, localKeyBytes.byteLength]),
|
|
76
|
+
new Uint8Array(localKeyBytes),
|
|
77
|
+
]);
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Derives a nonce for encryption using HKDF from the pseudo-random key, salt, and context.
|
|
81
|
+
*
|
|
82
|
+
* @param {CryptoKey} pseudoRandomKey - The pseudo-random key derived from the shared secret.
|
|
83
|
+
* @param {Uint8Array} salt - The salt used in the derivation process.
|
|
84
|
+
* @param {Uint8Array} context - The context for the nonce derivation.
|
|
85
|
+
* @returns {Promise<ArrayBuffer>} A promise that resolves to the derived nonce.
|
|
86
|
+
*/
|
|
87
|
+
const deriveNonce = async (pseudoRandomKey, salt, context) => {
|
|
88
|
+
const nonceInfo = concatTypedArrays([
|
|
89
|
+
new TextEncoder().encode('Content-Encoding: nonce\0'),
|
|
90
|
+
context,
|
|
91
|
+
]);
|
|
92
|
+
return crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt, info: nonceInfo }, pseudoRandomKey, 12 * 8);
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Derives a content encryption key using HKDF from the pseudo-random key, salt, and context.
|
|
96
|
+
*
|
|
97
|
+
* @param {CryptoKey} pseudoRandomKey - The pseudo-random key derived from the shared secret.
|
|
98
|
+
* @param {Uint8Array} salt - The salt used in the derivation process.
|
|
99
|
+
* @param {Uint8Array} context - The context for the key derivation.
|
|
100
|
+
* @returns {Promise<CryptoKey>} A promise that resolves to the derived content encryption key.
|
|
101
|
+
*/
|
|
102
|
+
const deriveContentEncryptionKey = async (pseudoRandomKey, salt, context) => {
|
|
103
|
+
const info = concatTypedArrays([
|
|
104
|
+
new TextEncoder().encode('Content-Encoding: aesgcm\0'),
|
|
105
|
+
context,
|
|
106
|
+
]);
|
|
107
|
+
const bits = await crypto.subtle.deriveBits({ name: 'HKDF', hash: 'SHA-256', salt, info }, pseudoRandomKey, 16 * 8);
|
|
108
|
+
return crypto.subtle.importKey('raw', bits, 'AES-GCM', false, ['encrypt']);
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Pads the payload to ensure it fits within the maximum allowed size for web push notifications.
|
|
112
|
+
*
|
|
113
|
+
* Web push payloads have an overall max size of 4KB (4096 bytes). With the
|
|
114
|
+
* required overhead for encryption, the actual max payload size is 4078 bytes.
|
|
115
|
+
*
|
|
116
|
+
* @param {Uint8Array} payload - The original payload to be padded.
|
|
117
|
+
* @returns {Uint8Array} The padded payload, including length information.
|
|
118
|
+
*/
|
|
119
|
+
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;
|
|
126
|
+
}
|
|
127
|
+
const paddingArray = new ArrayBuffer(2 + paddingSize);
|
|
128
|
+
new DataView(paddingArray).setUint16(0, paddingSize); // Store the length of the padding
|
|
129
|
+
// Return the new payload with padding added
|
|
130
|
+
return concatTypedArrays([new Uint8Array(paddingArray), payload]);
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Encrypts the payload for a push notification using the provided keys and context.
|
|
134
|
+
*
|
|
135
|
+
* @param {CryptoKeyPair} localKeys - The local key pair used for encryption.
|
|
136
|
+
* @param {Uint8Array} salt - The salt used in the encryption process.
|
|
137
|
+
* @param {string} payload - The original payload to encrypt.
|
|
138
|
+
* @param {PushSubscription} target - The target push subscription containing client keys.
|
|
139
|
+
* @returns {Promise<ArrayBuffer>} A promise that resolves to the encrypted payload.
|
|
140
|
+
*/
|
|
141
|
+
export const encryptPayload = async (localKeys, salt, payload, target) => {
|
|
142
|
+
const clientKeys = await importClientKeys(target.keys);
|
|
143
|
+
const sharedSecret = await deriveSharedSecret(clientKeys.p256, localKeys.privateKey);
|
|
144
|
+
const pseudoRandomKey = await derivePseudoRandomKey(clientKeys.auth, sharedSecret);
|
|
145
|
+
const context = await createContext(clientKeys.p256, localKeys.publicKey);
|
|
146
|
+
const nonce = await deriveNonce(pseudoRandomKey, salt, context);
|
|
147
|
+
const contentEncryptionKey = await deriveContentEncryptionKey(pseudoRandomKey, salt, context);
|
|
148
|
+
const encodedPayload = new TextEncoder().encode(payload);
|
|
149
|
+
const paddedPayload = padPayload(encodedPayload);
|
|
150
|
+
return crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, contentEncryptionKey, paddedPayload);
|
|
151
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { BuilderOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds an HTTP request body and headers for sending a push notification.
|
|
4
|
+
*
|
|
5
|
+
* This function constructs the necessary components for a push notification request,
|
|
6
|
+
* including the payload, headers, and any required cryptographic operations.
|
|
7
|
+
*
|
|
8
|
+
* @param {BuilderOptions} options - The options for building the push notification request.
|
|
9
|
+
* @param {JsonWebKey | string} options.privateJWK - The private JSON Web Key (JWK) used for signing.
|
|
10
|
+
* @param {PushMessage} options.message - The message to be sent in the push notification, including user-defined options.
|
|
11
|
+
* @param {PushSubscription} options.subscription - The subscription details for the push notification.
|
|
12
|
+
* @returns {Promise<{ endpoint: string, body: ArrayBuffer, headers: Record<string, string> | Headers }>} A promise that resolves to an object containing the endpoint, encrypted body, and headers for the push notification.
|
|
13
|
+
*
|
|
14
|
+
* @throws {Error} Throws an error if the privateJWK is invalid, if the request fails, or if the payload encryption fails.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Example usage
|
|
18
|
+
* const privateJWK = '{"kty":"EC","crv":"P-256","d":"_eQ..."}'; // Your private VAPID key
|
|
19
|
+
*
|
|
20
|
+
* const message = {
|
|
21
|
+
* payload: {
|
|
22
|
+
* title: "New Message",
|
|
23
|
+
* body: "You have a new message!",
|
|
24
|
+
* icon: "/images/icon.png"
|
|
25
|
+
* },
|
|
26
|
+
* options: {
|
|
27
|
+
* ttl: 3600, // 1 hour in seconds
|
|
28
|
+
* urgency: "high",
|
|
29
|
+
* topic: "new-messages"
|
|
30
|
+
* },
|
|
31
|
+
* adminContact: "mailto:admin@example.com"
|
|
32
|
+
* };
|
|
33
|
+
*
|
|
34
|
+
* const subscription = {
|
|
35
|
+
* endpoint: "https://fcm.googleapis.com/fcm/send/...",
|
|
36
|
+
* keys: {
|
|
37
|
+
* p256dh: "BNn5....",
|
|
38
|
+
* auth: "tBHI...."
|
|
39
|
+
* }
|
|
40
|
+
* };
|
|
41
|
+
*
|
|
42
|
+
* // Build the request
|
|
43
|
+
* const request = await buildPushHTTPRequest({
|
|
44
|
+
* privateJWK,
|
|
45
|
+
* message,
|
|
46
|
+
* subscription
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* // Send the push notification
|
|
50
|
+
* const response = await fetch(request.endpoint, {
|
|
51
|
+
* method: 'POST',
|
|
52
|
+
* headers: request.headers,
|
|
53
|
+
* body: request.body
|
|
54
|
+
* });
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildPushHTTPRequest({ privateJWK, message, subscription, }: BuilderOptions): Promise<{
|
|
57
|
+
endpoint: string;
|
|
58
|
+
body: ArrayBuffer;
|
|
59
|
+
headers: Record<string, string> | Headers;
|
|
60
|
+
}>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { crypto } from './crypto.js';
|
|
2
|
+
import { encryptPayload } from './payload.js';
|
|
3
|
+
import { vapidHeaders } from './vapid.js';
|
|
4
|
+
/**
|
|
5
|
+
* Builds an HTTP request body and headers for sending a push notification.
|
|
6
|
+
*
|
|
7
|
+
* This function constructs the necessary components for a push notification request,
|
|
8
|
+
* including the payload, headers, and any required cryptographic operations.
|
|
9
|
+
*
|
|
10
|
+
* @param {BuilderOptions} options - The options for building the push notification request.
|
|
11
|
+
* @param {JsonWebKey | string} options.privateJWK - The private JSON Web Key (JWK) used for signing.
|
|
12
|
+
* @param {PushMessage} options.message - The message to be sent in the push notification, including user-defined options.
|
|
13
|
+
* @param {PushSubscription} options.subscription - The subscription details for the push notification.
|
|
14
|
+
* @returns {Promise<{ endpoint: string, body: ArrayBuffer, headers: Record<string, string> | Headers }>} A promise that resolves to an object containing the endpoint, encrypted body, and headers for the push notification.
|
|
15
|
+
*
|
|
16
|
+
* @throws {Error} Throws an error if the privateJWK is invalid, if the request fails, or if the payload encryption fails.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Example usage
|
|
20
|
+
* const privateJWK = '{"kty":"EC","crv":"P-256","d":"_eQ..."}'; // Your private VAPID key
|
|
21
|
+
*
|
|
22
|
+
* const message = {
|
|
23
|
+
* payload: {
|
|
24
|
+
* title: "New Message",
|
|
25
|
+
* body: "You have a new message!",
|
|
26
|
+
* icon: "/images/icon.png"
|
|
27
|
+
* },
|
|
28
|
+
* options: {
|
|
29
|
+
* ttl: 3600, // 1 hour in seconds
|
|
30
|
+
* urgency: "high",
|
|
31
|
+
* topic: "new-messages"
|
|
32
|
+
* },
|
|
33
|
+
* adminContact: "mailto:admin@example.com"
|
|
34
|
+
* };
|
|
35
|
+
*
|
|
36
|
+
* const subscription = {
|
|
37
|
+
* endpoint: "https://fcm.googleapis.com/fcm/send/...",
|
|
38
|
+
* keys: {
|
|
39
|
+
* p256dh: "BNn5....",
|
|
40
|
+
* auth: "tBHI...."
|
|
41
|
+
* }
|
|
42
|
+
* };
|
|
43
|
+
*
|
|
44
|
+
* // Build the request
|
|
45
|
+
* const request = await buildPushHTTPRequest({
|
|
46
|
+
* privateJWK,
|
|
47
|
+
* message,
|
|
48
|
+
* subscription
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* // Send the push notification
|
|
52
|
+
* const response = await fetch(request.endpoint, {
|
|
53
|
+
* method: 'POST',
|
|
54
|
+
* headers: request.headers,
|
|
55
|
+
* body: request.body
|
|
56
|
+
* });
|
|
57
|
+
*/
|
|
58
|
+
export async function buildPushHTTPRequest({ privateJWK, message, subscription, }) {
|
|
59
|
+
// Parse the private JWK if it's a string
|
|
60
|
+
const jwk = typeof privateJWK === 'string' ? JSON.parse(privateJWK) : privateJWK;
|
|
61
|
+
const MAX_TTL = 24 * 60 * 60;
|
|
62
|
+
if (message.options?.ttl && message.options.ttl > MAX_TTL) {
|
|
63
|
+
throw new Error('TTL must be less than 24 hours');
|
|
64
|
+
}
|
|
65
|
+
// Determine the time-to-live (TTL) for the push notification
|
|
66
|
+
const ttl = message.options?.ttl && message.options.ttl > 0
|
|
67
|
+
? message.options.ttl
|
|
68
|
+
: MAX_TTL; // Default to 24 hours
|
|
69
|
+
// Create the JWT payload
|
|
70
|
+
const jwt = {
|
|
71
|
+
aud: new URL(subscription.endpoint).origin,
|
|
72
|
+
exp: Math.floor(Date.now() / 1000) + ttl,
|
|
73
|
+
sub: message.adminContact,
|
|
74
|
+
};
|
|
75
|
+
// Construct the options for the push notification
|
|
76
|
+
const options = {
|
|
77
|
+
jwk,
|
|
78
|
+
jwt,
|
|
79
|
+
payload: JSON.stringify(message.payload),
|
|
80
|
+
ttl,
|
|
81
|
+
...(message.options?.urgency && {
|
|
82
|
+
urgency: message.options.urgency,
|
|
83
|
+
}),
|
|
84
|
+
...(message.options?.topic && {
|
|
85
|
+
topic: message.options.topic,
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
// Generate a random salt for encryption
|
|
89
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
90
|
+
// Generate local keys for encryption
|
|
91
|
+
const localKeys = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
|
|
92
|
+
// Encrypt the payload for the push notification
|
|
93
|
+
const body = await encryptPayload(localKeys, salt, options.payload, subscription);
|
|
94
|
+
// Construct the VAPID headers for the push notification request
|
|
95
|
+
const headers = await vapidHeaders(options, body.byteLength, salt, localKeys.publicKey);
|
|
96
|
+
// Return the constructed request components
|
|
97
|
+
return { endpoint: subscription.endpoint, body, headers };
|
|
98
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives a shared secret from a client's public key and a local private key using the ECDH algorithm.
|
|
3
|
+
*
|
|
4
|
+
* This function uses the Web Crypto API to derive a shared secret, which can then be used
|
|
5
|
+
* for further cryptographic operations, such as key derivation using HKDF.
|
|
6
|
+
*
|
|
7
|
+
* @param {CryptoKey} clientPublicKey - The public key of the client, used to derive the shared secret.
|
|
8
|
+
* @param {CryptoKey} localPrivateKey - The local private key used in the derivation process.
|
|
9
|
+
* @returns {Promise<CryptoKey>} A promise that resolves to a CryptoKey representing the derived shared secret.
|
|
10
|
+
*
|
|
11
|
+
* @throws {Error} Throws an error if the key derivation fails.
|
|
12
|
+
*/
|
|
13
|
+
export declare const deriveSharedSecret: (clientPublicKey: CryptoKey, localPrivateKey: CryptoKey) => Promise<CryptoKey>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { crypto } from './crypto.js';
|
|
2
|
+
/**
|
|
3
|
+
* Derives a shared secret from a client's public key and a local private key using the ECDH algorithm.
|
|
4
|
+
*
|
|
5
|
+
* This function uses the Web Crypto API to derive a shared secret, which can then be used
|
|
6
|
+
* for further cryptographic operations, such as key derivation using HKDF.
|
|
7
|
+
*
|
|
8
|
+
* @param {CryptoKey} clientPublicKey - The public key of the client, used to derive the shared secret.
|
|
9
|
+
* @param {CryptoKey} localPrivateKey - The local private key used in the derivation process.
|
|
10
|
+
* @returns {Promise<CryptoKey>} A promise that resolves to a CryptoKey representing the derived shared secret.
|
|
11
|
+
*
|
|
12
|
+
* @throws {Error} Throws an error if the key derivation fails.
|
|
13
|
+
*/
|
|
14
|
+
export const deriveSharedSecret = async (clientPublicKey, localPrivateKey) => {
|
|
15
|
+
const sharedSecretBytes = await crypto.subtle.deriveBits({ name: 'ECDH', public: clientPublicKey }, localPrivateKey, 256);
|
|
16
|
+
return crypto.subtle.importKey('raw', sharedSecretBytes, { name: 'HKDF' }, false, ['deriveBits', 'deriveKey']);
|
|
17
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Jsonifiable, RequireAtLeastOne } from './utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for configuring the builder.
|
|
4
|
+
*/
|
|
5
|
+
export interface BuilderOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The private JSON Web Key (JWK) used for signing.
|
|
8
|
+
*/
|
|
9
|
+
privateJWK: JsonWebKey | string;
|
|
10
|
+
/**
|
|
11
|
+
* The subscription details for push notifications.
|
|
12
|
+
*/
|
|
13
|
+
subscription: PushSubscription;
|
|
14
|
+
/**
|
|
15
|
+
* The message to be sent in the push notification.
|
|
16
|
+
*/
|
|
17
|
+
message: PushMessage;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Represents a push message to be sent.
|
|
21
|
+
*
|
|
22
|
+
* @template T - The type of the payload, which must be JSON-compatible.
|
|
23
|
+
*/
|
|
24
|
+
export interface PushMessage<T extends Jsonifiable = Jsonifiable> {
|
|
25
|
+
/**
|
|
26
|
+
* The payload of the push message, which can be any JSON-compatible value.
|
|
27
|
+
*/
|
|
28
|
+
payload: T;
|
|
29
|
+
/**
|
|
30
|
+
* The contact information of the administrator, typically a URL or email address.
|
|
31
|
+
*/
|
|
32
|
+
adminContact: JwtData['sub'];
|
|
33
|
+
/**
|
|
34
|
+
* Optional settings for the push message.
|
|
35
|
+
* At least one of the specified options must be provided.
|
|
36
|
+
*/
|
|
37
|
+
options?: RequireAtLeastOne<{
|
|
38
|
+
/**
|
|
39
|
+
* The time-to-live for the push message in seconds.
|
|
40
|
+
* Must be a positive number greater than 0.
|
|
41
|
+
* Default value is 24 * 60 * 60 (24 hours).
|
|
42
|
+
* If set to 0, undefined, or a negative value, it will default to 24 hours.
|
|
43
|
+
* The VAPID JWT expiration claim (`exp`) must not exceed 24 hours from the time of the request.
|
|
44
|
+
* If it does, the push service (like FCM) will reject the request with a 403 Forbidden error.
|
|
45
|
+
*/
|
|
46
|
+
ttl?: PushOptions['ttl'];
|
|
47
|
+
/**
|
|
48
|
+
* The topic of the push message.
|
|
49
|
+
*/
|
|
50
|
+
topic?: PushOptions['topic'];
|
|
51
|
+
/**
|
|
52
|
+
* The urgency level of the push message.
|
|
53
|
+
*/
|
|
54
|
+
urgency?: PushOptions['urgency'];
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Represents the data contained in a JSON Web Token (JWT).
|
|
59
|
+
*/
|
|
60
|
+
export interface JwtData {
|
|
61
|
+
/**
|
|
62
|
+
* The audience for the JWT, typically the origin of the push service.
|
|
63
|
+
*/
|
|
64
|
+
aud: string;
|
|
65
|
+
/**
|
|
66
|
+
* The expiration time of the JWT, in seconds.
|
|
67
|
+
* This prevents reuse of the JWT after it has expired.
|
|
68
|
+
* The `exp` value must not exceed 24 hours from the time of the request.
|
|
69
|
+
* If it does, the push service (like FCM) will reject the request with a 403 Forbidden error.
|
|
70
|
+
*/
|
|
71
|
+
exp: number;
|
|
72
|
+
/**
|
|
73
|
+
* The subject of the JWT, which should be a URL or a mailto email address.
|
|
74
|
+
* This is used for contact information if the push service needs to reach out to the sender.
|
|
75
|
+
*/
|
|
76
|
+
sub: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Options for configuring a push notification.
|
|
80
|
+
*/
|
|
81
|
+
export interface PushOptions {
|
|
82
|
+
/**
|
|
83
|
+
* The JSON Web Key (JWK) used for signing the push notification.
|
|
84
|
+
*/
|
|
85
|
+
jwk: JsonWebKey;
|
|
86
|
+
/**
|
|
87
|
+
* The JWT data associated with the push notification.
|
|
88
|
+
*/
|
|
89
|
+
jwt: JwtData;
|
|
90
|
+
/**
|
|
91
|
+
* The payload of the push notification, typically a string.
|
|
92
|
+
*/
|
|
93
|
+
payload: string;
|
|
94
|
+
/**
|
|
95
|
+
* The time-to-live for the push notification in seconds.
|
|
96
|
+
* Must be a positive number greater than 0.
|
|
97
|
+
* Default value is 24 * 60 * 60 (24 hours).
|
|
98
|
+
* If set to 0, undefined, or a negative value, it will default to 24 hours.
|
|
99
|
+
* The VAPID JWT expiration claim (`exp`) must not exceed 24 hours from the time of the request.
|
|
100
|
+
* If it does, the push service (like FCM) will reject the request with a 403 Forbidden error.
|
|
101
|
+
*/
|
|
102
|
+
ttl: number;
|
|
103
|
+
/**
|
|
104
|
+
* The topic of the push notification (optional).
|
|
105
|
+
*/
|
|
106
|
+
topic?: string;
|
|
107
|
+
/**
|
|
108
|
+
* The urgency level of the push notification.
|
|
109
|
+
*/
|
|
110
|
+
urgency?: 'very-low' | 'low' | 'normal' | 'high';
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Represents the keys used for push subscription encryption.
|
|
114
|
+
*/
|
|
115
|
+
export interface PushSubscriptionKey {
|
|
116
|
+
/**
|
|
117
|
+
* The public key used for encrypting messages.
|
|
118
|
+
*/
|
|
119
|
+
p256dh: string;
|
|
120
|
+
/**
|
|
121
|
+
* The authentication secret used for encrypting messages.
|
|
122
|
+
*/
|
|
123
|
+
auth: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Represents a push subscription, which includes the endpoint and keys.
|
|
127
|
+
*/
|
|
128
|
+
export interface PushSubscription {
|
|
129
|
+
/**
|
|
130
|
+
* The endpoint URL for the push service to send notifications.
|
|
131
|
+
*/
|
|
132
|
+
endpoint: string;
|
|
133
|
+
/**
|
|
134
|
+
* The keys used for encrypting message data sent with the push notification.
|
|
135
|
+
*/
|
|
136
|
+
keys: PushSubscriptionKey;
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents any value that can be handled by JSON.stringify without loss.
|
|
3
|
+
* This includes primitive types such as strings, numbers, booleans, and null.
|
|
4
|
+
*/
|
|
5
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
6
|
+
/**
|
|
7
|
+
* Represents a JSON-compatible value, which can be a primitive,
|
|
8
|
+
* an array of Jsonifiable values, or an object with string keys
|
|
9
|
+
* and Jsonifiable values.
|
|
10
|
+
*/
|
|
11
|
+
export type Jsonifiable = JsonPrimitive | Jsonifiable[] | {
|
|
12
|
+
[key: string]: Jsonifiable;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A utility type that requires at least one of the specified keys from type T.
|
|
16
|
+
* This is useful for creating types that enforce the presence of certain properties
|
|
17
|
+
* while allowing others to be optional.
|
|
18
|
+
*
|
|
19
|
+
* @template T - The base type from which keys are required.
|
|
20
|
+
* @template Keys - The keys of T that are required.
|
|
21
|
+
*/
|
|
22
|
+
export type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = {
|
|
23
|
+
[K in Keys]: Required<Pick<T, K>> & Partial<Omit<T, K>>;
|
|
24
|
+
}[Keys] & Omit<T, Keys>;
|
|
25
|
+
/**
|
|
26
|
+
* Converts an ArrayBuffer or Uint8Array to a string.
|
|
27
|
+
*
|
|
28
|
+
* @param {ArrayBuffer | Uint8Array} s - The input array to convert.
|
|
29
|
+
* @returns {string} The resulting string representation of the input.
|
|
30
|
+
*/
|
|
31
|
+
export declare const stringFromArrayBuffer: (s: ArrayBuffer | Uint8Array) => string;
|
|
32
|
+
/**
|
|
33
|
+
* Cross-platform function to decode a Base64 string into a binary string.
|
|
34
|
+
* Works in both browser and Node.js environments.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} base64String - The Base64 encoded string to decode.
|
|
37
|
+
* @returns {string} The decoded binary string.
|
|
38
|
+
*/
|
|
39
|
+
export declare const base64Decode: (base64String: string) => string;
|
|
40
|
+
/**
|
|
41
|
+
* Extracts the public key from a JSON Web Key (JWK) and encodes it in base64 URL format.
|
|
42
|
+
*
|
|
43
|
+
* @param {JsonWebKey} jwk - The JSON Web Key from which to extract the public key.
|
|
44
|
+
* @returns {string} The base64 URL encoded public key.
|
|
45
|
+
*/
|
|
46
|
+
export declare const getPublicKeyFromJwk: (jwk: JsonWebKey) => string;
|
|
47
|
+
/**
|
|
48
|
+
* Concatenates multiple Uint8Array instances into a single Uint8Array.
|
|
49
|
+
*
|
|
50
|
+
* @param {Uint8Array[]} arrays - An array of Uint8Array instances to concatenate.
|
|
51
|
+
* @returns {Uint8Array} A new Uint8Array containing all the concatenated data.
|
|
52
|
+
*/
|
|
53
|
+
export declare const concatTypedArrays: (arrays: Uint8Array[]) => Uint8Array;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { base64UrlDecodeString, base64UrlEncode } from './base64.js';
|
|
2
|
+
/**
|
|
3
|
+
* Converts an ArrayBuffer or Uint8Array to a string.
|
|
4
|
+
*
|
|
5
|
+
* @param {ArrayBuffer | Uint8Array} s - The input array to convert.
|
|
6
|
+
* @returns {string} The resulting string representation of the input.
|
|
7
|
+
*/
|
|
8
|
+
export const stringFromArrayBuffer = (s) => {
|
|
9
|
+
let result = '';
|
|
10
|
+
for (const code of new Uint8Array(s))
|
|
11
|
+
result += String.fromCharCode(code);
|
|
12
|
+
return result;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Cross-platform function to decode a Base64 string into a binary string.
|
|
16
|
+
* Works in both browser and Node.js environments.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} base64String - The Base64 encoded string to decode.
|
|
19
|
+
* @returns {string} The decoded binary string.
|
|
20
|
+
*/
|
|
21
|
+
export const base64Decode = (base64String) => {
|
|
22
|
+
// Add padding if needed
|
|
23
|
+
const paddedBase64 = base64String.padEnd(base64String.length + ((4 - (base64String.length % 4 || 4)) % 4), '=');
|
|
24
|
+
// Node.js environment
|
|
25
|
+
if (typeof Buffer !== 'undefined') {
|
|
26
|
+
return Buffer.from(paddedBase64, 'base64').toString('binary');
|
|
27
|
+
}
|
|
28
|
+
// Browser environment
|
|
29
|
+
if (typeof atob === 'function') {
|
|
30
|
+
return atob(paddedBase64);
|
|
31
|
+
}
|
|
32
|
+
// Pure JavaScript implementation for environments without atob or Buffer
|
|
33
|
+
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
|
34
|
+
let result = '';
|
|
35
|
+
let i = 0;
|
|
36
|
+
while (i < paddedBase64.length) {
|
|
37
|
+
const enc1 = characters.indexOf(paddedBase64.charAt(i++));
|
|
38
|
+
const enc2 = characters.indexOf(paddedBase64.charAt(i++));
|
|
39
|
+
const enc3 = characters.indexOf(paddedBase64.charAt(i++));
|
|
40
|
+
const enc4 = characters.indexOf(paddedBase64.charAt(i++));
|
|
41
|
+
const char1 = (enc1 << 2) | (enc2 >> 4);
|
|
42
|
+
const char2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
|
43
|
+
const char3 = ((enc3 & 3) << 6) | enc4;
|
|
44
|
+
result += String.fromCharCode(char1);
|
|
45
|
+
if (enc3 !== 64)
|
|
46
|
+
result += String.fromCharCode(char2);
|
|
47
|
+
if (enc4 !== 64)
|
|
48
|
+
result += String.fromCharCode(char3);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Extracts the public key from a JSON Web Key (JWK) and encodes it in base64 URL format.
|
|
54
|
+
*
|
|
55
|
+
* @param {JsonWebKey} jwk - The JSON Web Key from which to extract the public key.
|
|
56
|
+
* @returns {string} The base64 URL encoded public key.
|
|
57
|
+
*/
|
|
58
|
+
export const getPublicKeyFromJwk = (jwk) => base64UrlEncode(`\x04${base64Decode(base64UrlDecodeString(jwk.x))}${base64Decode(base64UrlDecodeString(jwk.y))}`);
|
|
59
|
+
/**
|
|
60
|
+
* Concatenates multiple Uint8Array instances into a single Uint8Array.
|
|
61
|
+
*
|
|
62
|
+
* @param {Uint8Array[]} arrays - An array of Uint8Array instances to concatenate.
|
|
63
|
+
* @returns {Uint8Array} A new Uint8Array containing all the concatenated data.
|
|
64
|
+
*/
|
|
65
|
+
export const concatTypedArrays = (arrays) => {
|
|
66
|
+
const length = arrays.reduce((accumulator, current) => accumulator + current.byteLength, 0);
|
|
67
|
+
let index = 0;
|
|
68
|
+
const targetArray = new Uint8Array(length);
|
|
69
|
+
for (const array of arrays) {
|
|
70
|
+
targetArray.set(array, index);
|
|
71
|
+
index += array.byteLength;
|
|
72
|
+
}
|
|
73
|
+
return targetArray;
|
|
74
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PushOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Constructs the VAPID headers for a push notification request.
|
|
4
|
+
*
|
|
5
|
+
* This function generates the necessary headers for sending a push notification
|
|
6
|
+
* using the VAPID protocol, including authentication and encryption information.
|
|
7
|
+
*
|
|
8
|
+
* @param {PushOptions} options - The options for the push notification, including the JSON Web Key (JWK) and JWT data.
|
|
9
|
+
* @param {number} payloadLength - The length of the payload being sent in the push notification.
|
|
10
|
+
* @param {Uint8Array} salt - A salt value used in the encryption process.
|
|
11
|
+
* @param {CryptoKey} localPublicKey - The local public key used for encryption.
|
|
12
|
+
* @returns {Promise<Record<string, string> | Headers>} A promise that resolves to an object containing the VAPID headers.
|
|
13
|
+
*
|
|
14
|
+
* @throws {Error} Throws an error if the JWT creation fails or if key export fails.
|
|
15
|
+
*/
|
|
16
|
+
export declare const vapidHeaders: (options: PushOptions, payloadLength: number, salt: Uint8Array, localPublicKey: CryptoKey) => Promise<Record<string, string> | Headers>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { base64UrlEncode } from './base64.js';
|
|
2
|
+
import { crypto } from './crypto.js';
|
|
3
|
+
import { createJwt } from './jwt.js';
|
|
4
|
+
import { getPublicKeyFromJwk } from './utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Constructs the VAPID headers for a push notification request.
|
|
7
|
+
*
|
|
8
|
+
* This function generates the necessary headers for sending a push notification
|
|
9
|
+
* using the VAPID protocol, including authentication and encryption information.
|
|
10
|
+
*
|
|
11
|
+
* @param {PushOptions} options - The options for the push notification, including the JSON Web Key (JWK) and JWT data.
|
|
12
|
+
* @param {number} payloadLength - The length of the payload being sent in the push notification.
|
|
13
|
+
* @param {Uint8Array} salt - A salt value used in the encryption process.
|
|
14
|
+
* @param {CryptoKey} localPublicKey - The local public key used for encryption.
|
|
15
|
+
* @returns {Promise<Record<string, string> | Headers>} A promise that resolves to an object containing the VAPID headers.
|
|
16
|
+
*
|
|
17
|
+
* @throws {Error} Throws an error if the JWT creation fails or if key export fails.
|
|
18
|
+
*/
|
|
19
|
+
export const vapidHeaders = async (options, payloadLength, salt, localPublicKey) => {
|
|
20
|
+
// Export the local public key to a raw format and encode it in Base64 URL format
|
|
21
|
+
const localPublicKeyBase64 = await crypto.subtle
|
|
22
|
+
.exportKey('raw', localPublicKey)
|
|
23
|
+
.then((bytes) => base64UrlEncode(bytes));
|
|
24
|
+
// Get the server public key from the JWK
|
|
25
|
+
const serverPublicKey = getPublicKeyFromJwk(options.jwk);
|
|
26
|
+
// Create the JWT for authentication
|
|
27
|
+
const jwt = await createJwt(options.jwk, options.jwt);
|
|
28
|
+
// Construct the header values for the VAPID request
|
|
29
|
+
const headerValues = {
|
|
30
|
+
Encryption: `salt=${base64UrlEncode(salt)}`,
|
|
31
|
+
'Crypto-Key': `dh=${localPublicKeyBase64}`,
|
|
32
|
+
'Content-Length': payloadLength.toString(),
|
|
33
|
+
'Content-Type': 'application/octet-stream',
|
|
34
|
+
'Content-Encoding': 'aesgcm',
|
|
35
|
+
Authorization: `vapid t=${jwt}, k=${serverPublicKey}`,
|
|
36
|
+
};
|
|
37
|
+
let headers;
|
|
38
|
+
// Add optional headers if they are defined
|
|
39
|
+
if (options.ttl !== undefined)
|
|
40
|
+
headerValues.TTL = options.ttl.toString();
|
|
41
|
+
if (options.topic !== undefined)
|
|
42
|
+
headerValues.Topic = options.topic;
|
|
43
|
+
if (options.urgency !== undefined)
|
|
44
|
+
headerValues.Urgency = options.urgency;
|
|
45
|
+
// Create Headers object if available (for browser or Node.js 18+)
|
|
46
|
+
if (typeof Headers !== 'undefined') {
|
|
47
|
+
headers = new Headers(headerValues);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Fallback for Node.js < 18 without polyfill
|
|
51
|
+
headers = headerValues;
|
|
52
|
+
}
|
|
53
|
+
return headers; // Return the constructed headers
|
|
54
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pushforge/builder",
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
"private": false,
|
|
6
|
+
"main": "dist/lib/main.js",
|
|
7
|
+
"module": "dist/lib/main.js",
|
|
8
|
+
"types": "dist/lib/main.d.ts",
|
|
9
|
+
"author": "David Raphi",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/draphy/pushforge/issues"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/draphy/pushforge"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/draphy/pushforge",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/lib/main.d.ts",
|
|
23
|
+
"default": "./dist/lib/main.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist/lib/**/*.js",
|
|
28
|
+
"dist/lib/**/*.d.ts",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc --build",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"semantic-release": "semantic-release"
|
|
40
|
+
},
|
|
41
|
+
"bin": {
|
|
42
|
+
"generate-vapid-keys": "./dist/lib/commandLine/keys.js"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@semantic-release/commit-analyzer": "^11.1.0",
|
|
46
|
+
"@semantic-release/github": "^9.2.6",
|
|
47
|
+
"@semantic-release/npm": "^11.0.3",
|
|
48
|
+
"@semantic-release/release-notes-generator": "^12.1.0",
|
|
49
|
+
"@types/node": "^22.14.1",
|
|
50
|
+
"semantic-release": "^24.2.3",
|
|
51
|
+
"vitest": "^3.1.2"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"web-push",
|
|
55
|
+
"webcrypto",
|
|
56
|
+
"vapid",
|
|
57
|
+
"web-push-protocol",
|
|
58
|
+
"typescript",
|
|
59
|
+
"node",
|
|
60
|
+
"cloudflare",
|
|
61
|
+
"browser",
|
|
62
|
+
"ecdh",
|
|
63
|
+
"hkdf",
|
|
64
|
+
"push-service"
|
|
65
|
+
]
|
|
66
|
+
}
|