easy-ping 0.1.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 +374 -0
- package/dist/adapter-f0OR2DhY.d.cts +200 -0
- package/dist/adapter-f0OR2DhY.d.ts +200 -0
- package/dist/adapters/drizzle.cjs +440 -0
- package/dist/adapters/drizzle.cjs.map +1 -0
- package/dist/adapters/drizzle.d.cts +991 -0
- package/dist/adapters/drizzle.d.ts +991 -0
- package/dist/adapters/drizzle.js +409 -0
- package/dist/adapters/drizzle.js.map +1 -0
- package/dist/adapters/mongodb.cjs +458 -0
- package/dist/adapters/mongodb.cjs.map +1 -0
- package/dist/adapters/mongodb.d.cts +32 -0
- package/dist/adapters/mongodb.d.ts +32 -0
- package/dist/adapters/mongodb.js +376 -0
- package/dist/adapters/mongodb.js.map +1 -0
- package/dist/browser.cjs +96 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +45 -0
- package/dist/browser.d.ts +45 -0
- package/dist/browser.js +89 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-BV3U542P.js +11 -0
- package/dist/chunk-BV3U542P.js.map +1 -0
- package/dist/chunk-NYDVNMKG.js +62 -0
- package/dist/chunk-NYDVNMKG.js.map +1 -0
- package/dist/chunk-OZSOIX23.js +120 -0
- package/dist/chunk-OZSOIX23.js.map +1 -0
- package/dist/chunk-PTGHMCCG.js +23 -0
- package/dist/chunk-PTGHMCCG.js.map +1 -0
- package/dist/chunk-W3BMEPPQ.js +50 -0
- package/dist/chunk-W3BMEPPQ.js.map +1 -0
- package/dist/client.cjs +160 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +42 -0
- package/dist/client.d.ts +42 -0
- package/dist/client.js +158 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +920 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +203 -0
- package/dist/index.d.ts +203 -0
- package/dist/index.js +723 -0
- package/dist/index.js.map +1 -0
- package/dist/node.cjs +49 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +8 -0
- package/dist/node.d.ts +8 -0
- package/dist/node.js +46 -0
- package/dist/node.js.map +1 -0
- package/dist/plugin-Bqj9xAOS.d.cts +217 -0
- package/dist/plugin-bTJKmnlq.d.ts +217 -0
- package/dist/plugins/digests.cjs +206 -0
- package/dist/plugins/digests.cjs.map +1 -0
- package/dist/plugins/digests.d.cts +68 -0
- package/dist/plugins/digests.d.ts +68 -0
- package/dist/plugins/digests.js +201 -0
- package/dist/plugins/digests.js.map +1 -0
- package/dist/plugins/preferences-client.cjs +23 -0
- package/dist/plugins/preferences-client.cjs.map +1 -0
- package/dist/plugins/preferences-client.d.cts +29 -0
- package/dist/plugins/preferences-client.d.ts +29 -0
- package/dist/plugins/preferences-client.js +18 -0
- package/dist/plugins/preferences-client.js.map +1 -0
- package/dist/plugins/preferences-react.cjs +125 -0
- package/dist/plugins/preferences-react.cjs.map +1 -0
- package/dist/plugins/preferences-react.d.cts +40 -0
- package/dist/plugins/preferences-react.d.ts +40 -0
- package/dist/plugins/preferences-react.js +101 -0
- package/dist/plugins/preferences-react.js.map +1 -0
- package/dist/plugins/preferences.cjs +147 -0
- package/dist/plugins/preferences.cjs.map +1 -0
- package/dist/plugins/preferences.d.cts +22 -0
- package/dist/plugins/preferences.d.ts +22 -0
- package/dist/plugins/preferences.js +119 -0
- package/dist/plugins/preferences.js.map +1 -0
- package/dist/plugins/push.cjs +146 -0
- package/dist/plugins/push.cjs.map +1 -0
- package/dist/plugins/push.d.cts +60 -0
- package/dist/plugins/push.d.ts +60 -0
- package/dist/plugins/push.js +144 -0
- package/dist/plugins/push.js.map +1 -0
- package/dist/provider-CA45WVqt.d.cts +23 -0
- package/dist/provider-CA45WVqt.d.ts +23 -0
- package/dist/providers/resend.cjs +69 -0
- package/dist/providers/resend.cjs.map +1 -0
- package/dist/providers/resend.d.cts +20 -0
- package/dist/providers/resend.d.ts +20 -0
- package/dist/providers/resend.js +66 -0
- package/dist/providers/resend.js.map +1 -0
- package/dist/providers/web-push.cjs +211 -0
- package/dist/providers/web-push.cjs.map +1 -0
- package/dist/providers/web-push.d.cts +33 -0
- package/dist/providers/web-push.d.ts +33 -0
- package/dist/providers/web-push.js +193 -0
- package/dist/providers/web-push.js.map +1 -0
- package/dist/react.cjs +218 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +57 -0
- package/dist/react.d.ts +57 -0
- package/dist/react.js +193 -0
- package/dist/react.js.map +1 -0
- package/dist/schema.cjs +263 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +180 -0
- package/dist/schema.d.ts +180 -0
- package/dist/schema.js +200 -0
- package/dist/schema.js.map +1 -0
- package/dist/testing.cjs +342 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +38 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +340 -0
- package/dist/testing.js.map +1 -0
- package/package.json +168 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/base64url.ts
|
|
4
|
+
function encodeBase64Url(input) {
|
|
5
|
+
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
6
|
+
let binary = "";
|
|
7
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
8
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
9
|
+
}
|
|
10
|
+
function decodeBase64UrlBytes(value) {
|
|
11
|
+
try {
|
|
12
|
+
const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
|
|
13
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/providers/web-push/crypto.ts
|
|
20
|
+
var utf8 = (value) => new TextEncoder().encode(value);
|
|
21
|
+
function concat(...parts) {
|
|
22
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
23
|
+
const out = new Uint8Array(total);
|
|
24
|
+
let offset = 0;
|
|
25
|
+
for (const part of parts) {
|
|
26
|
+
out.set(part, offset);
|
|
27
|
+
offset += part.length;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function requireBytes(value, label, length) {
|
|
32
|
+
const bytes = decodeBase64UrlBytes(value);
|
|
33
|
+
if (!bytes) throw new Error(`${label} is not valid base64url`);
|
|
34
|
+
if (length !== void 0 && bytes.length !== length) {
|
|
35
|
+
throw new Error(`${label} must be ${length} bytes, got ${bytes.length}`);
|
|
36
|
+
}
|
|
37
|
+
return bytes;
|
|
38
|
+
}
|
|
39
|
+
async function hkdf(salt, ikm, info, length) {
|
|
40
|
+
const key = await crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
|
|
41
|
+
const bits = await crypto.subtle.deriveBits(
|
|
42
|
+
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
43
|
+
key,
|
|
44
|
+
length * 8
|
|
45
|
+
);
|
|
46
|
+
return new Uint8Array(bits);
|
|
47
|
+
}
|
|
48
|
+
var RECORD_SIZE = 4096;
|
|
49
|
+
var MAX_PAYLOAD_BYTES = RECORD_SIZE - 17;
|
|
50
|
+
async function encryptPayload(plaintext, userAgentPublicKey, authSecret, overrides) {
|
|
51
|
+
if (plaintext.length > MAX_PAYLOAD_BYTES) {
|
|
52
|
+
throw new Error(`push payload is ${plaintext.length} bytes; the limit is ${MAX_PAYLOAD_BYTES}`);
|
|
53
|
+
}
|
|
54
|
+
const uaPublic = requireBytes(userAgentPublicKey, "p256dh", 65);
|
|
55
|
+
const auth = requireBytes(authSecret, "auth", 16);
|
|
56
|
+
const salt = overrides?.salt ? new Uint8Array(overrides.salt) : crypto.getRandomValues(new Uint8Array(16));
|
|
57
|
+
const serverKeys = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
58
|
+
const serverPublic = new Uint8Array(
|
|
59
|
+
await crypto.subtle.exportKey("raw", serverKeys.publicKey)
|
|
60
|
+
);
|
|
61
|
+
const uaKey = await crypto.subtle.importKey(
|
|
62
|
+
"raw",
|
|
63
|
+
uaPublic,
|
|
64
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
65
|
+
false,
|
|
66
|
+
[]
|
|
67
|
+
);
|
|
68
|
+
const shared = new Uint8Array(
|
|
69
|
+
await crypto.subtle.deriveBits({ name: "ECDH", public: uaKey }, serverKeys.privateKey, 256)
|
|
70
|
+
);
|
|
71
|
+
const keyInfo = concat(utf8("WebPush: info"), new Uint8Array([0]), uaPublic, serverPublic);
|
|
72
|
+
const ikm = await hkdf(auth, shared, keyInfo, 32);
|
|
73
|
+
const cek = await hkdf(
|
|
74
|
+
salt,
|
|
75
|
+
ikm,
|
|
76
|
+
concat(utf8("Content-Encoding: aes128gcm"), new Uint8Array([0])),
|
|
77
|
+
16
|
|
78
|
+
);
|
|
79
|
+
const nonce = await hkdf(
|
|
80
|
+
salt,
|
|
81
|
+
ikm,
|
|
82
|
+
concat(utf8("Content-Encoding: nonce"), new Uint8Array([0])),
|
|
83
|
+
12
|
|
84
|
+
);
|
|
85
|
+
const aesKey = await crypto.subtle.importKey("raw", cek, "AES-GCM", false, ["encrypt"]);
|
|
86
|
+
const padded = concat(plaintext, new Uint8Array([2]));
|
|
87
|
+
const ciphertext = new Uint8Array(
|
|
88
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, aesKey, padded)
|
|
89
|
+
);
|
|
90
|
+
const header = new Uint8Array(21 + serverPublic.length);
|
|
91
|
+
header.set(salt, 0);
|
|
92
|
+
new DataView(header.buffer).setUint32(16, RECORD_SIZE, false);
|
|
93
|
+
header[20] = serverPublic.length;
|
|
94
|
+
header.set(serverPublic, 21);
|
|
95
|
+
return { body: concat(header, ciphertext), salt, serverPublicKey: serverPublic };
|
|
96
|
+
}
|
|
97
|
+
async function generateVapidKeys() {
|
|
98
|
+
const keys = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
|
|
99
|
+
"sign",
|
|
100
|
+
"verify"
|
|
101
|
+
]);
|
|
102
|
+
const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keys.publicKey));
|
|
103
|
+
const jwk = await crypto.subtle.exportKey("jwk", keys.privateKey);
|
|
104
|
+
if (!jwk.d) throw new Error("generated key has no private component");
|
|
105
|
+
return { publicKey: encodeBase64Url(publicKey), privateKey: jwk.d };
|
|
106
|
+
}
|
|
107
|
+
async function importVapidKey(keys) {
|
|
108
|
+
const publicKey = requireBytes(keys.publicKey, "vapid publicKey", 65);
|
|
109
|
+
if (publicKey[0] !== 4) throw new Error("vapid publicKey must be an uncompressed EC point");
|
|
110
|
+
return crypto.subtle.importKey(
|
|
111
|
+
"jwk",
|
|
112
|
+
{
|
|
113
|
+
kty: "EC",
|
|
114
|
+
crv: "P-256",
|
|
115
|
+
x: encodeBase64Url(publicKey.slice(1, 33)),
|
|
116
|
+
y: encodeBase64Url(publicKey.slice(33, 65)),
|
|
117
|
+
d: keys.privateKey,
|
|
118
|
+
ext: true
|
|
119
|
+
},
|
|
120
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
121
|
+
false,
|
|
122
|
+
["sign"]
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
async function vapidAuthorization(endpoint, options) {
|
|
126
|
+
if (!/^(mailto:|https:)/.test(options.subject)) {
|
|
127
|
+
throw new Error('vapid subject must be a "mailto:" or "https:" URL');
|
|
128
|
+
}
|
|
129
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
130
|
+
const header = { typ: "JWT", alg: "ES256" };
|
|
131
|
+
const claims = {
|
|
132
|
+
// Origin only: a token scoped to the full endpoint is rejected.
|
|
133
|
+
aud: new URL(endpoint).origin,
|
|
134
|
+
exp: Math.floor(now.getTime() / 1e3) + (options.ttlSeconds ?? 12 * 3600),
|
|
135
|
+
sub: options.subject
|
|
136
|
+
};
|
|
137
|
+
const signingInput = `${encodeBase64Url(JSON.stringify(header))}.${encodeBase64Url(
|
|
138
|
+
JSON.stringify(claims)
|
|
139
|
+
)}`;
|
|
140
|
+
const signature = await crypto.subtle.sign(
|
|
141
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
142
|
+
await importVapidKey(options.keys),
|
|
143
|
+
utf8(signingInput)
|
|
144
|
+
);
|
|
145
|
+
const jwt = `${signingInput}.${encodeBase64Url(new Uint8Array(signature))}`;
|
|
146
|
+
return `vapid t=${jwt}, k=${options.keys.publicKey}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/providers/web-push/index.ts
|
|
150
|
+
function webPush(options) {
|
|
151
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
152
|
+
const ttl = options.ttlSeconds ?? 12 * 3600;
|
|
153
|
+
return {
|
|
154
|
+
name: "web-push",
|
|
155
|
+
async send(message) {
|
|
156
|
+
const payload = new TextEncoder().encode(
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
title: message.title,
|
|
159
|
+
body: message.body,
|
|
160
|
+
data: message.data ?? {}
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
if (payload.length > MAX_PAYLOAD_BYTES) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`push payload is ${payload.length} bytes, over the ${MAX_PAYLOAD_BYTES} byte limit`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const encrypted = await encryptPayload(
|
|
169
|
+
payload,
|
|
170
|
+
message.subscription.keys.p256dh,
|
|
171
|
+
message.subscription.keys.auth
|
|
172
|
+
);
|
|
173
|
+
const authorization = await vapidAuthorization(message.subscription.endpoint, {
|
|
174
|
+
subject: options.subject,
|
|
175
|
+
keys: options.vapid
|
|
176
|
+
});
|
|
177
|
+
let response;
|
|
178
|
+
try {
|
|
179
|
+
response = await doFetch(message.subscription.endpoint, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: {
|
|
182
|
+
Authorization: authorization,
|
|
183
|
+
"Content-Encoding": "aes128gcm",
|
|
184
|
+
"Content-Type": "application/octet-stream",
|
|
185
|
+
TTL: String(ttl),
|
|
186
|
+
...options.urgency ? { Urgency: options.urgency } : {}
|
|
187
|
+
},
|
|
188
|
+
body: encrypted.body,
|
|
189
|
+
...message.signal ? { signal: message.signal } : {}
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
193
|
+
}
|
|
194
|
+
if (response.ok) return {};
|
|
195
|
+
if (response.status === 404 || response.status === 410) return { expired: true };
|
|
196
|
+
if (response.status === 429 || response.status === 408 || response.status >= 500) {
|
|
197
|
+
return { retryable: true };
|
|
198
|
+
}
|
|
199
|
+
const detail = await response.text().catch(() => "");
|
|
200
|
+
throw new Error(
|
|
201
|
+
`push service responded ${response.status}${detail ? `: ${detail.slice(0, 300)}` : ""}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
exports.MAX_PAYLOAD_BYTES = MAX_PAYLOAD_BYTES;
|
|
208
|
+
exports.generateVapidKeys = generateVapidKeys;
|
|
209
|
+
exports.webPush = webPush;
|
|
210
|
+
//# sourceMappingURL=web-push.cjs.map
|
|
211
|
+
//# sourceMappingURL=web-push.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/base64url.ts","../../src/providers/web-push/crypto.ts","../../src/providers/web-push/index.ts"],"names":[],"mappings":"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","file":"web-push.cjs","sourcesContent":["// Web APIs, not Buffer: this runs on Workers and Vercel Edge.\n\nexport function encodeBase64Url(input: string | Uint8Array): string {\n const bytes = typeof input === \"string\" ? new TextEncoder().encode(input) : input;\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n// Uint8Array<ArrayBuffer>: bare Uint8Array widens and is not a BufferSource.\nexport function decodeBase64UrlBytes(value: string): Uint8Array<ArrayBuffer> | null {\n try {\n const binary = atob(value.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n return Uint8Array.from(binary, (char) => char.charCodeAt(0));\n } catch {\n return null;\n }\n}\n\nexport function decodeBase64Url(value: string): string | null {\n const bytes = decodeBase64UrlBytes(value);\n return bytes ? new TextDecoder().decode(bytes) : null;\n}\n","import { decodeBase64UrlBytes, encodeBase64Url } from \"../../core/base64url\";\n\n/**\n * VAPID (RFC 8292) and aes128gcm payload encryption (RFC 8291 over RFC 8188),\n * on Web Crypto.\n *\n * The `web-push` package is node:crypto only, so it cannot run on Cloudflare\n * Workers or Vercel Edge — the runtimes this library targets. Everything here\n * uses crypto.subtle and therefore runs anywhere the rest of the package does.\n */\n\nconst utf8 = (value: string) => new TextEncoder().encode(value);\n\nfunction concat(...parts: Uint8Array[]): Uint8Array<ArrayBuffer> {\n const total = parts.reduce((sum, part) => sum + part.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\nfunction requireBytes(value: string, label: string, length?: number): Uint8Array<ArrayBuffer> {\n const bytes = decodeBase64UrlBytes(value);\n if (!bytes) throw new Error(`${label} is not valid base64url`);\n if (length !== undefined && bytes.length !== length) {\n throw new Error(`${label} must be ${length} bytes, got ${bytes.length}`);\n }\n return bytes;\n}\n\n/** Web Crypto's HKDF does extract and expand in one call. */\nasync function hkdf(\n // Uint8Array<ArrayBuffer>, not bare Uint8Array: the latter widens to\n // ArrayBufferLike, which Web Crypto will not accept as a BufferSource.\n salt: Uint8Array<ArrayBuffer>,\n ikm: Uint8Array<ArrayBuffer>,\n info: Uint8Array<ArrayBuffer>,\n length: number,\n): Promise<Uint8Array<ArrayBuffer>> {\n const key = await crypto.subtle.importKey(\"raw\", ikm, \"HKDF\", false, [\"deriveBits\"]);\n const bits = await crypto.subtle.deriveBits(\n { name: \"HKDF\", hash: \"SHA-256\", salt, info },\n key,\n length * 8,\n );\n return new Uint8Array(bits);\n}\n\nexport const RECORD_SIZE = 4096;\n/** One record: record size less the padding delimiter and the GCM tag. */\nexport const MAX_PAYLOAD_BYTES = RECORD_SIZE - 17;\n\nexport type EncryptedPush = {\n body: Uint8Array<ArrayBuffer>;\n salt: Uint8Array<ArrayBuffer>;\n serverPublicKey: Uint8Array<ArrayBuffer>;\n};\n\n/**\n * RFC 8291 §3.4 then RFC 8188 §2.\n *\n * `salt` and `serverKeys` are injectable so tests can pin them; production\n * always generates fresh ones, and reusing a salt with the same key would be\n * catastrophic for GCM.\n */\nexport async function encryptPayload(\n plaintext: Uint8Array<ArrayBuffer>,\n userAgentPublicKey: string,\n authSecret: string,\n overrides?: { salt?: Uint8Array<ArrayBuffer>; serverKeys?: CryptoKeyPair },\n): Promise<EncryptedPush> {\n if (plaintext.length > MAX_PAYLOAD_BYTES) {\n throw new Error(`push payload is ${plaintext.length} bytes; the limit is ${MAX_PAYLOAD_BYTES}`);\n }\n\n const uaPublic = requireBytes(userAgentPublicKey, \"p256dh\", 65);\n const auth = requireBytes(authSecret, \"auth\", 16);\n\n const salt = overrides?.salt\n ? new Uint8Array(overrides.salt)\n : crypto.getRandomValues(new Uint8Array(16));\n\n const serverKeys =\n overrides?.serverKeys ??\n (await crypto.subtle.generateKey({ name: \"ECDH\", namedCurve: \"P-256\" }, true, [\"deriveBits\"]));\n\n const serverPublic = new Uint8Array(\n await crypto.subtle.exportKey(\"raw\", serverKeys.publicKey),\n ) as Uint8Array<ArrayBuffer>;\n\n const uaKey = await crypto.subtle.importKey(\n \"raw\",\n uaPublic,\n { name: \"ECDH\", namedCurve: \"P-256\" },\n false,\n [],\n );\n\n const shared = new Uint8Array(\n await crypto.subtle.deriveBits({ name: \"ECDH\", public: uaKey }, serverKeys.privateKey, 256),\n );\n\n // The receiver's key comes first; swapping the order yields a key the\n // browser cannot derive, and the push simply never decrypts.\n const keyInfo = concat(utf8(\"WebPush: info\"), new Uint8Array([0]), uaPublic, serverPublic);\n const ikm = await hkdf(auth, shared, keyInfo, 32);\n\n const cek = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: aes128gcm\"), new Uint8Array([0])),\n 16,\n );\n const nonce = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: nonce\"), new Uint8Array([0])),\n 12,\n );\n\n const aesKey = await crypto.subtle.importKey(\"raw\", cek, \"AES-GCM\", false, [\"encrypt\"]);\n\n // 0x02 marks the final record. 0x01 would mean \"more records follow\" and the\n // receiver would wait for one that never arrives.\n const padded = concat(plaintext, new Uint8Array([2]));\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt({ name: \"AES-GCM\", iv: nonce }, aesKey, padded),\n );\n\n // RFC 8188 header: salt(16) | rs(4, big endian) | idlen(1) | keyid\n const header = new Uint8Array(21 + serverPublic.length);\n header.set(salt, 0);\n new DataView(header.buffer).setUint32(16, RECORD_SIZE, false);\n header[20] = serverPublic.length;\n header.set(serverPublic, 21);\n\n return { body: concat(header, ciphertext), salt, serverPublicKey: serverPublic };\n}\n\nexport type VapidKeys = { publicKey: string; privateKey: string };\n\n/** A VAPID keypair in the base64url form every push library uses. */\nexport async function generateVapidKeys(): Promise<VapidKeys> {\n const keys = await crypto.subtle.generateKey({ name: \"ECDSA\", namedCurve: \"P-256\" }, true, [\n \"sign\",\n \"verify\",\n ]);\n\n const publicKey = new Uint8Array(await crypto.subtle.exportKey(\"raw\", keys.publicKey));\n const jwk = await crypto.subtle.exportKey(\"jwk\", keys.privateKey);\n\n if (!jwk.d) throw new Error(\"generated key has no private component\");\n return { publicKey: encodeBase64Url(publicKey), privateKey: jwk.d };\n}\n\n/**\n * VAPID keys are distributed as a raw public point plus a bare scalar, but Web\n * Crypto will only import a private EC key as JWK — which needs x and y. They\n * are recovered by slicing the uncompressed public point.\n */\nasync function importVapidKey(keys: VapidKeys): Promise<CryptoKey> {\n const publicKey = requireBytes(keys.publicKey, \"vapid publicKey\", 65);\n if (publicKey[0] !== 0x04) throw new Error(\"vapid publicKey must be an uncompressed EC point\");\n\n return crypto.subtle.importKey(\n \"jwk\",\n {\n kty: \"EC\",\n crv: \"P-256\",\n x: encodeBase64Url(publicKey.slice(1, 33)),\n y: encodeBase64Url(publicKey.slice(33, 65)),\n d: keys.privateKey,\n ext: true,\n },\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n false,\n [\"sign\"],\n );\n}\n\nexport type VapidOptions = {\n subject: string;\n keys: VapidKeys;\n /** Injectable for tests; VAPID tokens are short-lived. */\n now?: Date;\n ttlSeconds?: number;\n};\n\n/** RFC 8292 §2: an ES256 JWT scoped to the push service origin. */\nexport async function vapidAuthorization(endpoint: string, options: VapidOptions): Promise<string> {\n if (!/^(mailto:|https:)/.test(options.subject)) {\n throw new Error('vapid subject must be a \"mailto:\" or \"https:\" URL');\n }\n\n const now = options.now ?? new Date();\n const header = { typ: \"JWT\", alg: \"ES256\" };\n const claims = {\n // Origin only: a token scoped to the full endpoint is rejected.\n aud: new URL(endpoint).origin,\n exp: Math.floor(now.getTime() / 1000) + (options.ttlSeconds ?? 12 * 3600),\n sub: options.subject,\n };\n\n const signingInput = `${encodeBase64Url(JSON.stringify(header))}.${encodeBase64Url(\n JSON.stringify(claims),\n )}`;\n\n const signature = await crypto.subtle.sign(\n { name: \"ECDSA\", hash: \"SHA-256\" },\n await importVapidKey(options.keys),\n utf8(signingInput),\n );\n\n // Web Crypto emits raw r||s, which is what JWS ES256 wants — no DER unwrap.\n const jwt = `${signingInput}.${encodeBase64Url(new Uint8Array(signature))}`;\n return `vapid t=${jwt}, k=${options.keys.publicKey}`;\n}\n","import type { PushMessage, PushProvider, PushSendResult } from \"../../plugins/push\";\nimport { encryptPayload, MAX_PAYLOAD_BYTES, type VapidKeys, vapidAuthorization } from \"./crypto\";\n\nexport type WebPushOptions = {\n /** \"mailto:you@example.com\" or an https URL. Push services require it. */\n subject: string;\n vapid: VapidKeys;\n /** How long the service should hold an undelivered message. */\n ttlSeconds?: number;\n urgency?: \"very-low\" | \"low\" | \"normal\" | \"high\";\n fetch?: typeof globalThis.fetch;\n};\n\n/**\n * Web push over Web Crypto, so it runs on Workers and Edge as well as Node.\n *\n * Endpoints that report themselves gone are surfaced as `expired`, which the\n * push plugin turns into an immediate row deletion. Everything transient is\n * `retryable`; anything the push service will reject identically forever is\n * neither, so it fails on the first attempt.\n */\nexport function webPush(options: WebPushOptions): PushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const ttl = options.ttlSeconds ?? 12 * 3600;\n\n return {\n name: \"web-push\",\n\n async send(message: PushMessage): Promise<PushSendResult> {\n const payload = new TextEncoder().encode(\n JSON.stringify({\n title: message.title,\n body: message.body,\n data: message.data ?? {},\n }),\n );\n\n if (payload.length > MAX_PAYLOAD_BYTES) {\n // Trimming silently would deliver a truncated notification; a bigger\n // payload will never fit, so retrying cannot help either.\n throw new Error(\n `push payload is ${payload.length} bytes, over the ${MAX_PAYLOAD_BYTES} byte limit`,\n );\n }\n\n const encrypted = await encryptPayload(\n payload,\n message.subscription.keys.p256dh,\n message.subscription.keys.auth,\n );\n\n const authorization = await vapidAuthorization(message.subscription.endpoint, {\n subject: options.subject,\n keys: options.vapid,\n });\n\n let response: Response;\n try {\n response = await doFetch(message.subscription.endpoint, {\n method: \"POST\",\n headers: {\n Authorization: authorization,\n \"Content-Encoding\": \"aes128gcm\",\n \"Content-Type\": \"application/octet-stream\",\n TTL: String(ttl),\n ...(options.urgency ? { Urgency: options.urgency } : {}),\n },\n body: encrypted.body,\n ...(message.signal ? { signal: message.signal } : {}),\n });\n } catch (error) {\n // Never reached the service: DNS, socket, timeout.\n throw new Error(error instanceof Error ? error.message : String(error));\n }\n\n // 201 is the documented success; 200/202 appear in the wild.\n if (response.ok) return {};\n\n // The subscription is permanently gone and must be pruned.\n if (response.status === 404 || response.status === 410) return { expired: true };\n\n if (response.status === 429 || response.status === 408 || response.status >= 500) {\n return { retryable: true };\n }\n\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `push service responded ${response.status}${detail ? `: ${detail.slice(0, 300)}` : \"\"}`,\n );\n },\n };\n}\n\nexport type { VapidKeys } from \"./crypto\";\nexport { generateVapidKeys, MAX_PAYLOAD_BYTES } from \"./crypto\";\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PushProvider } from '../plugins/push.cjs';
|
|
2
|
+
import '../plugin-Bqj9xAOS.cjs';
|
|
3
|
+
import '../adapter-f0OR2DhY.cjs';
|
|
4
|
+
|
|
5
|
+
/** One record: record size less the padding delimiter and the GCM tag. */
|
|
6
|
+
declare const MAX_PAYLOAD_BYTES: number;
|
|
7
|
+
type VapidKeys = {
|
|
8
|
+
publicKey: string;
|
|
9
|
+
privateKey: string;
|
|
10
|
+
};
|
|
11
|
+
/** A VAPID keypair in the base64url form every push library uses. */
|
|
12
|
+
declare function generateVapidKeys(): Promise<VapidKeys>;
|
|
13
|
+
|
|
14
|
+
type WebPushOptions = {
|
|
15
|
+
/** "mailto:you@example.com" or an https URL. Push services require it. */
|
|
16
|
+
subject: string;
|
|
17
|
+
vapid: VapidKeys;
|
|
18
|
+
/** How long the service should hold an undelivered message. */
|
|
19
|
+
ttlSeconds?: number;
|
|
20
|
+
urgency?: "very-low" | "low" | "normal" | "high";
|
|
21
|
+
fetch?: typeof globalThis.fetch;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Web push over Web Crypto, so it runs on Workers and Edge as well as Node.
|
|
25
|
+
*
|
|
26
|
+
* Endpoints that report themselves gone are surfaced as `expired`, which the
|
|
27
|
+
* push plugin turns into an immediate row deletion. Everything transient is
|
|
28
|
+
* `retryable`; anything the push service will reject identically forever is
|
|
29
|
+
* neither, so it fails on the first attempt.
|
|
30
|
+
*/
|
|
31
|
+
declare function webPush(options: WebPushOptions): PushProvider;
|
|
32
|
+
|
|
33
|
+
export { MAX_PAYLOAD_BYTES, type VapidKeys, type WebPushOptions, generateVapidKeys, webPush };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PushProvider } from '../plugins/push.js';
|
|
2
|
+
import '../plugin-bTJKmnlq.js';
|
|
3
|
+
import '../adapter-f0OR2DhY.js';
|
|
4
|
+
|
|
5
|
+
/** One record: record size less the padding delimiter and the GCM tag. */
|
|
6
|
+
declare const MAX_PAYLOAD_BYTES: number;
|
|
7
|
+
type VapidKeys = {
|
|
8
|
+
publicKey: string;
|
|
9
|
+
privateKey: string;
|
|
10
|
+
};
|
|
11
|
+
/** A VAPID keypair in the base64url form every push library uses. */
|
|
12
|
+
declare function generateVapidKeys(): Promise<VapidKeys>;
|
|
13
|
+
|
|
14
|
+
type WebPushOptions = {
|
|
15
|
+
/** "mailto:you@example.com" or an https URL. Push services require it. */
|
|
16
|
+
subject: string;
|
|
17
|
+
vapid: VapidKeys;
|
|
18
|
+
/** How long the service should hold an undelivered message. */
|
|
19
|
+
ttlSeconds?: number;
|
|
20
|
+
urgency?: "very-low" | "low" | "normal" | "high";
|
|
21
|
+
fetch?: typeof globalThis.fetch;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Web push over Web Crypto, so it runs on Workers and Edge as well as Node.
|
|
25
|
+
*
|
|
26
|
+
* Endpoints that report themselves gone are surfaced as `expired`, which the
|
|
27
|
+
* push plugin turns into an immediate row deletion. Everything transient is
|
|
28
|
+
* `retryable`; anything the push service will reject identically forever is
|
|
29
|
+
* neither, so it fails on the first attempt.
|
|
30
|
+
*/
|
|
31
|
+
declare function webPush(options: WebPushOptions): PushProvider;
|
|
32
|
+
|
|
33
|
+
export { MAX_PAYLOAD_BYTES, type VapidKeys, type WebPushOptions, generateVapidKeys, webPush };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { encodeBase64Url, decodeBase64UrlBytes } from '../chunk-PTGHMCCG.js';
|
|
2
|
+
|
|
3
|
+
// src/providers/web-push/crypto.ts
|
|
4
|
+
var utf8 = (value) => new TextEncoder().encode(value);
|
|
5
|
+
function concat(...parts) {
|
|
6
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
7
|
+
const out = new Uint8Array(total);
|
|
8
|
+
let offset = 0;
|
|
9
|
+
for (const part of parts) {
|
|
10
|
+
out.set(part, offset);
|
|
11
|
+
offset += part.length;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
function requireBytes(value, label, length) {
|
|
16
|
+
const bytes = decodeBase64UrlBytes(value);
|
|
17
|
+
if (!bytes) throw new Error(`${label} is not valid base64url`);
|
|
18
|
+
if (length !== void 0 && bytes.length !== length) {
|
|
19
|
+
throw new Error(`${label} must be ${length} bytes, got ${bytes.length}`);
|
|
20
|
+
}
|
|
21
|
+
return bytes;
|
|
22
|
+
}
|
|
23
|
+
async function hkdf(salt, ikm, info, length) {
|
|
24
|
+
const key = await crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
|
|
25
|
+
const bits = await crypto.subtle.deriveBits(
|
|
26
|
+
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
27
|
+
key,
|
|
28
|
+
length * 8
|
|
29
|
+
);
|
|
30
|
+
return new Uint8Array(bits);
|
|
31
|
+
}
|
|
32
|
+
var RECORD_SIZE = 4096;
|
|
33
|
+
var MAX_PAYLOAD_BYTES = RECORD_SIZE - 17;
|
|
34
|
+
async function encryptPayload(plaintext, userAgentPublicKey, authSecret, overrides) {
|
|
35
|
+
if (plaintext.length > MAX_PAYLOAD_BYTES) {
|
|
36
|
+
throw new Error(`push payload is ${plaintext.length} bytes; the limit is ${MAX_PAYLOAD_BYTES}`);
|
|
37
|
+
}
|
|
38
|
+
const uaPublic = requireBytes(userAgentPublicKey, "p256dh", 65);
|
|
39
|
+
const auth = requireBytes(authSecret, "auth", 16);
|
|
40
|
+
const salt = overrides?.salt ? new Uint8Array(overrides.salt) : crypto.getRandomValues(new Uint8Array(16));
|
|
41
|
+
const serverKeys = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
42
|
+
const serverPublic = new Uint8Array(
|
|
43
|
+
await crypto.subtle.exportKey("raw", serverKeys.publicKey)
|
|
44
|
+
);
|
|
45
|
+
const uaKey = await crypto.subtle.importKey(
|
|
46
|
+
"raw",
|
|
47
|
+
uaPublic,
|
|
48
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
49
|
+
false,
|
|
50
|
+
[]
|
|
51
|
+
);
|
|
52
|
+
const shared = new Uint8Array(
|
|
53
|
+
await crypto.subtle.deriveBits({ name: "ECDH", public: uaKey }, serverKeys.privateKey, 256)
|
|
54
|
+
);
|
|
55
|
+
const keyInfo = concat(utf8("WebPush: info"), new Uint8Array([0]), uaPublic, serverPublic);
|
|
56
|
+
const ikm = await hkdf(auth, shared, keyInfo, 32);
|
|
57
|
+
const cek = await hkdf(
|
|
58
|
+
salt,
|
|
59
|
+
ikm,
|
|
60
|
+
concat(utf8("Content-Encoding: aes128gcm"), new Uint8Array([0])),
|
|
61
|
+
16
|
|
62
|
+
);
|
|
63
|
+
const nonce = await hkdf(
|
|
64
|
+
salt,
|
|
65
|
+
ikm,
|
|
66
|
+
concat(utf8("Content-Encoding: nonce"), new Uint8Array([0])),
|
|
67
|
+
12
|
|
68
|
+
);
|
|
69
|
+
const aesKey = await crypto.subtle.importKey("raw", cek, "AES-GCM", false, ["encrypt"]);
|
|
70
|
+
const padded = concat(plaintext, new Uint8Array([2]));
|
|
71
|
+
const ciphertext = new Uint8Array(
|
|
72
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, aesKey, padded)
|
|
73
|
+
);
|
|
74
|
+
const header = new Uint8Array(21 + serverPublic.length);
|
|
75
|
+
header.set(salt, 0);
|
|
76
|
+
new DataView(header.buffer).setUint32(16, RECORD_SIZE, false);
|
|
77
|
+
header[20] = serverPublic.length;
|
|
78
|
+
header.set(serverPublic, 21);
|
|
79
|
+
return { body: concat(header, ciphertext), salt, serverPublicKey: serverPublic };
|
|
80
|
+
}
|
|
81
|
+
async function generateVapidKeys() {
|
|
82
|
+
const keys = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
|
|
83
|
+
"sign",
|
|
84
|
+
"verify"
|
|
85
|
+
]);
|
|
86
|
+
const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keys.publicKey));
|
|
87
|
+
const jwk = await crypto.subtle.exportKey("jwk", keys.privateKey);
|
|
88
|
+
if (!jwk.d) throw new Error("generated key has no private component");
|
|
89
|
+
return { publicKey: encodeBase64Url(publicKey), privateKey: jwk.d };
|
|
90
|
+
}
|
|
91
|
+
async function importVapidKey(keys) {
|
|
92
|
+
const publicKey = requireBytes(keys.publicKey, "vapid publicKey", 65);
|
|
93
|
+
if (publicKey[0] !== 4) throw new Error("vapid publicKey must be an uncompressed EC point");
|
|
94
|
+
return crypto.subtle.importKey(
|
|
95
|
+
"jwk",
|
|
96
|
+
{
|
|
97
|
+
kty: "EC",
|
|
98
|
+
crv: "P-256",
|
|
99
|
+
x: encodeBase64Url(publicKey.slice(1, 33)),
|
|
100
|
+
y: encodeBase64Url(publicKey.slice(33, 65)),
|
|
101
|
+
d: keys.privateKey,
|
|
102
|
+
ext: true
|
|
103
|
+
},
|
|
104
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
105
|
+
false,
|
|
106
|
+
["sign"]
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
async function vapidAuthorization(endpoint, options) {
|
|
110
|
+
if (!/^(mailto:|https:)/.test(options.subject)) {
|
|
111
|
+
throw new Error('vapid subject must be a "mailto:" or "https:" URL');
|
|
112
|
+
}
|
|
113
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
114
|
+
const header = { typ: "JWT", alg: "ES256" };
|
|
115
|
+
const claims = {
|
|
116
|
+
// Origin only: a token scoped to the full endpoint is rejected.
|
|
117
|
+
aud: new URL(endpoint).origin,
|
|
118
|
+
exp: Math.floor(now.getTime() / 1e3) + (options.ttlSeconds ?? 12 * 3600),
|
|
119
|
+
sub: options.subject
|
|
120
|
+
};
|
|
121
|
+
const signingInput = `${encodeBase64Url(JSON.stringify(header))}.${encodeBase64Url(
|
|
122
|
+
JSON.stringify(claims)
|
|
123
|
+
)}`;
|
|
124
|
+
const signature = await crypto.subtle.sign(
|
|
125
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
126
|
+
await importVapidKey(options.keys),
|
|
127
|
+
utf8(signingInput)
|
|
128
|
+
);
|
|
129
|
+
const jwt = `${signingInput}.${encodeBase64Url(new Uint8Array(signature))}`;
|
|
130
|
+
return `vapid t=${jwt}, k=${options.keys.publicKey}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/providers/web-push/index.ts
|
|
134
|
+
function webPush(options) {
|
|
135
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
136
|
+
const ttl = options.ttlSeconds ?? 12 * 3600;
|
|
137
|
+
return {
|
|
138
|
+
name: "web-push",
|
|
139
|
+
async send(message) {
|
|
140
|
+
const payload = new TextEncoder().encode(
|
|
141
|
+
JSON.stringify({
|
|
142
|
+
title: message.title,
|
|
143
|
+
body: message.body,
|
|
144
|
+
data: message.data ?? {}
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
if (payload.length > MAX_PAYLOAD_BYTES) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`push payload is ${payload.length} bytes, over the ${MAX_PAYLOAD_BYTES} byte limit`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const encrypted = await encryptPayload(
|
|
153
|
+
payload,
|
|
154
|
+
message.subscription.keys.p256dh,
|
|
155
|
+
message.subscription.keys.auth
|
|
156
|
+
);
|
|
157
|
+
const authorization = await vapidAuthorization(message.subscription.endpoint, {
|
|
158
|
+
subject: options.subject,
|
|
159
|
+
keys: options.vapid
|
|
160
|
+
});
|
|
161
|
+
let response;
|
|
162
|
+
try {
|
|
163
|
+
response = await doFetch(message.subscription.endpoint, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: {
|
|
166
|
+
Authorization: authorization,
|
|
167
|
+
"Content-Encoding": "aes128gcm",
|
|
168
|
+
"Content-Type": "application/octet-stream",
|
|
169
|
+
TTL: String(ttl),
|
|
170
|
+
...options.urgency ? { Urgency: options.urgency } : {}
|
|
171
|
+
},
|
|
172
|
+
body: encrypted.body,
|
|
173
|
+
...message.signal ? { signal: message.signal } : {}
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
177
|
+
}
|
|
178
|
+
if (response.ok) return {};
|
|
179
|
+
if (response.status === 404 || response.status === 410) return { expired: true };
|
|
180
|
+
if (response.status === 429 || response.status === 408 || response.status >= 500) {
|
|
181
|
+
return { retryable: true };
|
|
182
|
+
}
|
|
183
|
+
const detail = await response.text().catch(() => "");
|
|
184
|
+
throw new Error(
|
|
185
|
+
`push service responded ${response.status}${detail ? `: ${detail.slice(0, 300)}` : ""}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export { MAX_PAYLOAD_BYTES, generateVapidKeys, webPush };
|
|
192
|
+
//# sourceMappingURL=web-push.js.map
|
|
193
|
+
//# sourceMappingURL=web-push.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/web-push/crypto.ts","../../src/providers/web-push/index.ts"],"names":[],"mappings":"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","file":"web-push.js","sourcesContent":["import { decodeBase64UrlBytes, encodeBase64Url } from \"../../core/base64url\";\n\n/**\n * VAPID (RFC 8292) and aes128gcm payload encryption (RFC 8291 over RFC 8188),\n * on Web Crypto.\n *\n * The `web-push` package is node:crypto only, so it cannot run on Cloudflare\n * Workers or Vercel Edge — the runtimes this library targets. Everything here\n * uses crypto.subtle and therefore runs anywhere the rest of the package does.\n */\n\nconst utf8 = (value: string) => new TextEncoder().encode(value);\n\nfunction concat(...parts: Uint8Array[]): Uint8Array<ArrayBuffer> {\n const total = parts.reduce((sum, part) => sum + part.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\nfunction requireBytes(value: string, label: string, length?: number): Uint8Array<ArrayBuffer> {\n const bytes = decodeBase64UrlBytes(value);\n if (!bytes) throw new Error(`${label} is not valid base64url`);\n if (length !== undefined && bytes.length !== length) {\n throw new Error(`${label} must be ${length} bytes, got ${bytes.length}`);\n }\n return bytes;\n}\n\n/** Web Crypto's HKDF does extract and expand in one call. */\nasync function hkdf(\n // Uint8Array<ArrayBuffer>, not bare Uint8Array: the latter widens to\n // ArrayBufferLike, which Web Crypto will not accept as a BufferSource.\n salt: Uint8Array<ArrayBuffer>,\n ikm: Uint8Array<ArrayBuffer>,\n info: Uint8Array<ArrayBuffer>,\n length: number,\n): Promise<Uint8Array<ArrayBuffer>> {\n const key = await crypto.subtle.importKey(\"raw\", ikm, \"HKDF\", false, [\"deriveBits\"]);\n const bits = await crypto.subtle.deriveBits(\n { name: \"HKDF\", hash: \"SHA-256\", salt, info },\n key,\n length * 8,\n );\n return new Uint8Array(bits);\n}\n\nexport const RECORD_SIZE = 4096;\n/** One record: record size less the padding delimiter and the GCM tag. */\nexport const MAX_PAYLOAD_BYTES = RECORD_SIZE - 17;\n\nexport type EncryptedPush = {\n body: Uint8Array<ArrayBuffer>;\n salt: Uint8Array<ArrayBuffer>;\n serverPublicKey: Uint8Array<ArrayBuffer>;\n};\n\n/**\n * RFC 8291 §3.4 then RFC 8188 §2.\n *\n * `salt` and `serverKeys` are injectable so tests can pin them; production\n * always generates fresh ones, and reusing a salt with the same key would be\n * catastrophic for GCM.\n */\nexport async function encryptPayload(\n plaintext: Uint8Array<ArrayBuffer>,\n userAgentPublicKey: string,\n authSecret: string,\n overrides?: { salt?: Uint8Array<ArrayBuffer>; serverKeys?: CryptoKeyPair },\n): Promise<EncryptedPush> {\n if (plaintext.length > MAX_PAYLOAD_BYTES) {\n throw new Error(`push payload is ${plaintext.length} bytes; the limit is ${MAX_PAYLOAD_BYTES}`);\n }\n\n const uaPublic = requireBytes(userAgentPublicKey, \"p256dh\", 65);\n const auth = requireBytes(authSecret, \"auth\", 16);\n\n const salt = overrides?.salt\n ? new Uint8Array(overrides.salt)\n : crypto.getRandomValues(new Uint8Array(16));\n\n const serverKeys =\n overrides?.serverKeys ??\n (await crypto.subtle.generateKey({ name: \"ECDH\", namedCurve: \"P-256\" }, true, [\"deriveBits\"]));\n\n const serverPublic = new Uint8Array(\n await crypto.subtle.exportKey(\"raw\", serverKeys.publicKey),\n ) as Uint8Array<ArrayBuffer>;\n\n const uaKey = await crypto.subtle.importKey(\n \"raw\",\n uaPublic,\n { name: \"ECDH\", namedCurve: \"P-256\" },\n false,\n [],\n );\n\n const shared = new Uint8Array(\n await crypto.subtle.deriveBits({ name: \"ECDH\", public: uaKey }, serverKeys.privateKey, 256),\n );\n\n // The receiver's key comes first; swapping the order yields a key the\n // browser cannot derive, and the push simply never decrypts.\n const keyInfo = concat(utf8(\"WebPush: info\"), new Uint8Array([0]), uaPublic, serverPublic);\n const ikm = await hkdf(auth, shared, keyInfo, 32);\n\n const cek = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: aes128gcm\"), new Uint8Array([0])),\n 16,\n );\n const nonce = await hkdf(\n salt,\n ikm,\n concat(utf8(\"Content-Encoding: nonce\"), new Uint8Array([0])),\n 12,\n );\n\n const aesKey = await crypto.subtle.importKey(\"raw\", cek, \"AES-GCM\", false, [\"encrypt\"]);\n\n // 0x02 marks the final record. 0x01 would mean \"more records follow\" and the\n // receiver would wait for one that never arrives.\n const padded = concat(plaintext, new Uint8Array([2]));\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt({ name: \"AES-GCM\", iv: nonce }, aesKey, padded),\n );\n\n // RFC 8188 header: salt(16) | rs(4, big endian) | idlen(1) | keyid\n const header = new Uint8Array(21 + serverPublic.length);\n header.set(salt, 0);\n new DataView(header.buffer).setUint32(16, RECORD_SIZE, false);\n header[20] = serverPublic.length;\n header.set(serverPublic, 21);\n\n return { body: concat(header, ciphertext), salt, serverPublicKey: serverPublic };\n}\n\nexport type VapidKeys = { publicKey: string; privateKey: string };\n\n/** A VAPID keypair in the base64url form every push library uses. */\nexport async function generateVapidKeys(): Promise<VapidKeys> {\n const keys = await crypto.subtle.generateKey({ name: \"ECDSA\", namedCurve: \"P-256\" }, true, [\n \"sign\",\n \"verify\",\n ]);\n\n const publicKey = new Uint8Array(await crypto.subtle.exportKey(\"raw\", keys.publicKey));\n const jwk = await crypto.subtle.exportKey(\"jwk\", keys.privateKey);\n\n if (!jwk.d) throw new Error(\"generated key has no private component\");\n return { publicKey: encodeBase64Url(publicKey), privateKey: jwk.d };\n}\n\n/**\n * VAPID keys are distributed as a raw public point plus a bare scalar, but Web\n * Crypto will only import a private EC key as JWK — which needs x and y. They\n * are recovered by slicing the uncompressed public point.\n */\nasync function importVapidKey(keys: VapidKeys): Promise<CryptoKey> {\n const publicKey = requireBytes(keys.publicKey, \"vapid publicKey\", 65);\n if (publicKey[0] !== 0x04) throw new Error(\"vapid publicKey must be an uncompressed EC point\");\n\n return crypto.subtle.importKey(\n \"jwk\",\n {\n kty: \"EC\",\n crv: \"P-256\",\n x: encodeBase64Url(publicKey.slice(1, 33)),\n y: encodeBase64Url(publicKey.slice(33, 65)),\n d: keys.privateKey,\n ext: true,\n },\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n false,\n [\"sign\"],\n );\n}\n\nexport type VapidOptions = {\n subject: string;\n keys: VapidKeys;\n /** Injectable for tests; VAPID tokens are short-lived. */\n now?: Date;\n ttlSeconds?: number;\n};\n\n/** RFC 8292 §2: an ES256 JWT scoped to the push service origin. */\nexport async function vapidAuthorization(endpoint: string, options: VapidOptions): Promise<string> {\n if (!/^(mailto:|https:)/.test(options.subject)) {\n throw new Error('vapid subject must be a \"mailto:\" or \"https:\" URL');\n }\n\n const now = options.now ?? new Date();\n const header = { typ: \"JWT\", alg: \"ES256\" };\n const claims = {\n // Origin only: a token scoped to the full endpoint is rejected.\n aud: new URL(endpoint).origin,\n exp: Math.floor(now.getTime() / 1000) + (options.ttlSeconds ?? 12 * 3600),\n sub: options.subject,\n };\n\n const signingInput = `${encodeBase64Url(JSON.stringify(header))}.${encodeBase64Url(\n JSON.stringify(claims),\n )}`;\n\n const signature = await crypto.subtle.sign(\n { name: \"ECDSA\", hash: \"SHA-256\" },\n await importVapidKey(options.keys),\n utf8(signingInput),\n );\n\n // Web Crypto emits raw r||s, which is what JWS ES256 wants — no DER unwrap.\n const jwt = `${signingInput}.${encodeBase64Url(new Uint8Array(signature))}`;\n return `vapid t=${jwt}, k=${options.keys.publicKey}`;\n}\n","import type { PushMessage, PushProvider, PushSendResult } from \"../../plugins/push\";\nimport { encryptPayload, MAX_PAYLOAD_BYTES, type VapidKeys, vapidAuthorization } from \"./crypto\";\n\nexport type WebPushOptions = {\n /** \"mailto:you@example.com\" or an https URL. Push services require it. */\n subject: string;\n vapid: VapidKeys;\n /** How long the service should hold an undelivered message. */\n ttlSeconds?: number;\n urgency?: \"very-low\" | \"low\" | \"normal\" | \"high\";\n fetch?: typeof globalThis.fetch;\n};\n\n/**\n * Web push over Web Crypto, so it runs on Workers and Edge as well as Node.\n *\n * Endpoints that report themselves gone are surfaced as `expired`, which the\n * push plugin turns into an immediate row deletion. Everything transient is\n * `retryable`; anything the push service will reject identically forever is\n * neither, so it fails on the first attempt.\n */\nexport function webPush(options: WebPushOptions): PushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const ttl = options.ttlSeconds ?? 12 * 3600;\n\n return {\n name: \"web-push\",\n\n async send(message: PushMessage): Promise<PushSendResult> {\n const payload = new TextEncoder().encode(\n JSON.stringify({\n title: message.title,\n body: message.body,\n data: message.data ?? {},\n }),\n );\n\n if (payload.length > MAX_PAYLOAD_BYTES) {\n // Trimming silently would deliver a truncated notification; a bigger\n // payload will never fit, so retrying cannot help either.\n throw new Error(\n `push payload is ${payload.length} bytes, over the ${MAX_PAYLOAD_BYTES} byte limit`,\n );\n }\n\n const encrypted = await encryptPayload(\n payload,\n message.subscription.keys.p256dh,\n message.subscription.keys.auth,\n );\n\n const authorization = await vapidAuthorization(message.subscription.endpoint, {\n subject: options.subject,\n keys: options.vapid,\n });\n\n let response: Response;\n try {\n response = await doFetch(message.subscription.endpoint, {\n method: \"POST\",\n headers: {\n Authorization: authorization,\n \"Content-Encoding\": \"aes128gcm\",\n \"Content-Type\": \"application/octet-stream\",\n TTL: String(ttl),\n ...(options.urgency ? { Urgency: options.urgency } : {}),\n },\n body: encrypted.body,\n ...(message.signal ? { signal: message.signal } : {}),\n });\n } catch (error) {\n // Never reached the service: DNS, socket, timeout.\n throw new Error(error instanceof Error ? error.message : String(error));\n }\n\n // 201 is the documented success; 200/202 appear in the wild.\n if (response.ok) return {};\n\n // The subscription is permanently gone and must be pruned.\n if (response.status === 404 || response.status === 410) return { expired: true };\n\n if (response.status === 429 || response.status === 408 || response.status >= 500) {\n return { retryable: true };\n }\n\n const detail = await response.text().catch(() => \"\");\n throw new Error(\n `push service responded ${response.status}${detail ? `: ${detail.slice(0, 300)}` : \"\"}`,\n );\n },\n };\n}\n\nexport type { VapidKeys } from \"./crypto\";\nexport { generateVapidKeys, MAX_PAYLOAD_BYTES } from \"./crypto\";\n"]}
|