easy-ping 0.3.0 → 0.4.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.
Files changed (79) hide show
  1. package/README.md +15 -4
  2. package/dist/adapters/drizzle.cjs +3 -2
  3. package/dist/adapters/drizzle.cjs.map +1 -1
  4. package/dist/adapters/drizzle.d.cts +34 -0
  5. package/dist/adapters/drizzle.d.ts +34 -0
  6. package/dist/adapters/drizzle.js +4 -3
  7. package/dist/adapters/drizzle.js.map +1 -1
  8. package/dist/adapters/mongodb.cjs +6 -2
  9. package/dist/adapters/mongodb.cjs.map +1 -1
  10. package/dist/adapters/mongodb.d.cts +1 -1
  11. package/dist/adapters/mongodb.d.ts +1 -1
  12. package/dist/adapters/mongodb.js +5 -2
  13. package/dist/adapters/mongodb.js.map +1 -1
  14. package/dist/adapters/postgres.cjs +3 -2
  15. package/dist/adapters/postgres.cjs.map +1 -1
  16. package/dist/adapters/postgres.d.cts +1 -1
  17. package/dist/adapters/postgres.d.ts +1 -1
  18. package/dist/adapters/postgres.js +3 -3
  19. package/dist/{chunk-OZSOIX23.js → chunk-3Y2MBUAW.js} +53 -12
  20. package/dist/chunk-3Y2MBUAW.js.map +1 -0
  21. package/dist/chunk-CYKAM7LE.js +226 -0
  22. package/dist/chunk-CYKAM7LE.js.map +1 -0
  23. package/dist/chunk-DG3ZL5NP.js +315 -0
  24. package/dist/chunk-DG3ZL5NP.js.map +1 -0
  25. package/dist/{chunk-NYDVNMKG.js → chunk-PSJHXZPR.js} +4 -3
  26. package/dist/chunk-PSJHXZPR.js.map +1 -0
  27. package/dist/{chunk-BV3U542P.js → chunk-W6I3B2CI.js} +2 -2
  28. package/dist/chunk-W6I3B2CI.js.map +1 -0
  29. package/dist/{chunk-RBDW4S6F.js → chunk-YEMRRMJQ.js} +3 -3
  30. package/dist/{chunk-RBDW4S6F.js.map → chunk-YEMRRMJQ.js.map} +1 -1
  31. package/dist/index.cjs +360 -62
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +84 -20
  34. package/dist/index.d.ts +84 -20
  35. package/dist/index.js +119 -357
  36. package/dist/index.js.map +1 -1
  37. package/dist/node.cjs +27 -6
  38. package/dist/node.cjs.map +1 -1
  39. package/dist/node.d.cts +8 -2
  40. package/dist/node.d.ts +8 -2
  41. package/dist/node.js +26 -6
  42. package/dist/node.js.map +1 -1
  43. package/dist/{plugin-CmPOFVmn.d.ts → plugin-C9Hoxus2.d.ts} +37 -9
  44. package/dist/{plugin-f5KaFWSN.d.cts → plugin-CiMsGlQe.d.cts} +37 -9
  45. package/dist/plugins/digests.d.cts +1 -1
  46. package/dist/plugins/digests.d.ts +1 -1
  47. package/dist/plugins/preferences-client.cjs.map +1 -1
  48. package/dist/plugins/preferences-client.d.cts +1 -1
  49. package/dist/plugins/preferences-client.d.ts +1 -1
  50. package/dist/plugins/preferences-client.js +1 -1
  51. package/dist/plugins/preferences.cjs +100 -15
  52. package/dist/plugins/preferences.cjs.map +1 -1
  53. package/dist/plugins/preferences.d.cts +3 -1
  54. package/dist/plugins/preferences.d.ts +3 -1
  55. package/dist/plugins/preferences.js +35 -7
  56. package/dist/plugins/preferences.js.map +1 -1
  57. package/dist/plugins/push.cjs +197 -38
  58. package/dist/plugins/push.cjs.map +1 -1
  59. package/dist/plugins/push.d.cts +32 -10
  60. package/dist/plugins/push.d.ts +32 -10
  61. package/dist/plugins/push.js +140 -39
  62. package/dist/plugins/push.js.map +1 -1
  63. package/dist/providers/web-push.cjs +37 -11
  64. package/dist/providers/web-push.cjs.map +1 -1
  65. package/dist/providers/web-push.d.cts +8 -2
  66. package/dist/providers/web-push.d.ts +8 -2
  67. package/dist/providers/web-push.js +37 -12
  68. package/dist/providers/web-push.js.map +1 -1
  69. package/dist/schema.cjs +2 -1
  70. package/dist/schema.cjs.map +1 -1
  71. package/dist/schema.d.cts +6 -1
  72. package/dist/schema.d.ts +6 -1
  73. package/dist/schema.js +2 -2
  74. package/package.json +1 -1
  75. package/dist/chunk-BV3U542P.js.map +0 -1
  76. package/dist/chunk-NYDVNMKG.js.map +0 -1
  77. package/dist/chunk-OZSOIX23.js.map +0 -1
  78. package/dist/chunk-W3BMEPPQ.js +0 -50
  79. package/dist/chunk-W3BMEPPQ.js.map +0 -1
@@ -1,3 +1,7 @@
1
+ import { redactErrorMessage } from '../chunk-CYKAM7LE.js';
2
+ import { readJsonBody } from '../chunk-DG3ZL5NP.js';
3
+ import { decodeBase64UrlBytes } from '../chunk-PTGHMCCG.js';
4
+
1
5
  // src/plugins/push/index.ts
2
6
  var DEVICE = "notification_push_device";
3
7
  var pushSchema = {
@@ -22,9 +26,58 @@ var pushSchema = {
22
26
  ]
23
27
  }
24
28
  };
29
+ var PRIVATE_IPV4 = /^(0\.|10\.|127\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/;
30
+ function isInternalHost(hostname) {
31
+ const host = hostname.toLowerCase();
32
+ if (host === "localhost" || /\.(localhost|local|internal|lan)$/.test(host)) return true;
33
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) return PRIVATE_IPV4.test(host);
34
+ if (host.startsWith("[")) {
35
+ const v6 = host.slice(1, -1);
36
+ return v6 === "::1" || v6 === "::" || /^(fc|fd|fe[89ab])/.test(v6) || v6.startsWith("::ffff:");
37
+ }
38
+ return false;
39
+ }
40
+ var hostAllowed = (host, allowed) => allowed.some(
41
+ (entry) => entry.startsWith("*.") ? host.endsWith(entry.slice(1)) : host === entry.toLowerCase()
42
+ );
43
+ function validateEndpoint(endpoint, allowedHosts, allowInsecure = false) {
44
+ let url;
45
+ try {
46
+ url = new URL(endpoint);
47
+ } catch {
48
+ return "endpoint is not a valid URL";
49
+ }
50
+ if (url.username || url.password) return "endpoint must not carry credentials";
51
+ if (!allowInsecure) {
52
+ if (url.protocol !== "https:") return "endpoint must use https";
53
+ if (isInternalHost(url.hostname)) return "endpoint must be a public host";
54
+ } else if (url.protocol !== "https:" && url.protocol !== "http:") {
55
+ return "endpoint must be an http(s) URL";
56
+ }
57
+ if (allowedHosts && !hostAllowed(url.hostname.toLowerCase(), allowedHosts)) {
58
+ return "endpoint host is not an allowed push service";
59
+ }
60
+ return null;
61
+ }
62
+ function validateSubscriptionKeys(keys) {
63
+ const p256dh = decodeBase64UrlBytes(keys.p256dh);
64
+ if (p256dh?.length !== 65 || p256dh[0] !== 4) {
65
+ return "keys.p256dh must be a 65-byte uncompressed EC point";
66
+ }
67
+ const auth = decodeBase64UrlBytes(keys.auth);
68
+ if (auth?.length !== 16) return "keys.auth must be 16 bytes";
69
+ return null;
70
+ }
71
+ var isDuplicate = (error) => {
72
+ const code = error?.code;
73
+ if (code === "23505" || code === 11e3) return true;
74
+ const text = error instanceof Error ? error.message : String(error);
75
+ return /duplicate|unique/i.test(text);
76
+ };
25
77
  function push(options) {
26
78
  let ctx;
27
79
  const staleAfterDays = options.staleAfterDays ?? 180;
80
+ const maxDevices = Math.max(1, options.maxDevicesPerUser ?? 20);
28
81
  return {
29
82
  id: "push",
30
83
  channels: ["push"],
@@ -44,13 +97,9 @@ function push(options) {
44
97
  type: context.notification.type,
45
98
  payload: context.notification.payload
46
99
  });
47
- const expired = [];
48
- let delivered = 0;
49
- let retryable = false;
50
- let lastError = "";
51
- for (const device of devices) {
52
- try {
53
- const result = await options.provider.send({
100
+ const results = await Promise.allSettled(
101
+ devices.map(
102
+ (device) => options.provider.send({
54
103
  subscription: {
55
104
  endpoint: device.endpoint,
56
105
  keys: { p256dh: device.p256dh, auth: device.auth }
@@ -58,20 +107,30 @@ function push(options) {
58
107
  ...content,
59
108
  data: { notificationId: context.notification.id, type: context.notification.type },
60
109
  signal: context.signal
61
- });
62
- if (result.expired) expired.push(device.id);
63
- else {
64
- delivered += 1;
65
- if (result.retryable) retryable = true;
66
- }
67
- } catch (error) {
68
- lastError = error instanceof Error ? error.message : String(error);
110
+ })
111
+ )
112
+ );
113
+ const prune = [];
114
+ let delivered = 0;
115
+ let retryable = false;
116
+ let lastError = "";
117
+ results.forEach((result, index) => {
118
+ const device = devices[index];
119
+ if (!device) return;
120
+ if (result.status === "rejected") {
121
+ lastError = redactErrorMessage(
122
+ result.reason instanceof Error ? result.reason.message : String(result.reason)
123
+ );
69
124
  retryable = true;
125
+ return;
70
126
  }
71
- }
72
- if (expired.length > 0) await ctx.store.remove(DEVICE, { id: { in: expired } });
127
+ if (result.value.expired || result.value.invalid) prune.push(device.id);
128
+ else if (result.value.retryable) retryable = true;
129
+ else delivered += 1;
130
+ });
131
+ if (prune.length > 0) await ctx.store.remove(DEVICE, { id: { in: prune } });
73
132
  if (delivered > 0) return { result: "sent" };
74
- if (expired.length === devices.length) {
133
+ if (prune.length === devices.length) {
75
134
  return { result: "failed", error: "all devices expired", retryable: false };
76
135
  }
77
136
  return {
@@ -87,26 +146,69 @@ function push(options) {
87
146
  method: "POST",
88
147
  scope: { type: "user" },
89
148
  handler: async ({ request, userId }) => {
90
- const body = await request.json().catch(() => null);
91
- if (!body?.endpoint || !body.keys?.p256dh || !body.keys?.auth) {
149
+ const parsed = await readJsonBody(request);
150
+ if ("error" in parsed) return parsed.error;
151
+ const { endpoint, keys } = parsed.body;
152
+ if (typeof endpoint !== "string" || typeof keys?.p256dh !== "string" || typeof keys?.auth !== "string") {
92
153
  return Response.json({ error: "endpoint and keys are required" }, { status: 400 });
93
154
  }
94
- await ctx.store.upsert(
155
+ const problem = validateEndpoint(
156
+ endpoint,
157
+ options.allowedEndpointHosts,
158
+ options.allowInsecureEndpoints ?? false
159
+ ) ?? validateSubscriptionKeys(keys);
160
+ if (problem) return Response.json({ error: problem }, { status: 400 });
161
+ const owner = userId ?? "";
162
+ const now = /* @__PURE__ */ new Date();
163
+ const userAgent = request.headers.get("user-agent")?.slice(0, 512) ?? null;
164
+ const [existing] = await ctx.store.find(DEVICE, { endpoint }, { limit: 1 });
165
+ if (existing && existing.userId !== owner) {
166
+ return Response.json(
167
+ { error: "endpoint is registered to another account" },
168
+ { status: 409 }
169
+ );
170
+ }
171
+ if (existing) {
172
+ await ctx.store.update(
173
+ DEVICE,
174
+ { id: existing.id },
175
+ { p256dh: keys.p256dh, auth: keys.auth, userAgent, lastSeenAt: now }
176
+ );
177
+ return Response.json({ ok: true });
178
+ }
179
+ const devices = await ctx.store.find(
95
180
  DEVICE,
96
- [
181
+ { userId: owner },
182
+ { orderBy: { field: "lastSeenAt", direction: "asc" } }
183
+ );
184
+ const excess = devices.length - maxDevices + 1;
185
+ if (excess > 0) {
186
+ await ctx.store.remove(DEVICE, {
187
+ id: { in: devices.slice(0, excess).map((device) => device.id) }
188
+ });
189
+ }
190
+ try {
191
+ await ctx.store.insert(DEVICE, [
97
192
  {
98
193
  id: crypto.randomUUID(),
99
- userId: userId ?? "",
100
- endpoint: body.endpoint,
101
- p256dh: body.keys.p256dh,
102
- auth: body.keys.auth,
103
- userAgent: request.headers.get("user-agent"),
104
- createdAt: /* @__PURE__ */ new Date(),
105
- lastSeenAt: /* @__PURE__ */ new Date()
194
+ userId: owner,
195
+ endpoint,
196
+ p256dh: keys.p256dh,
197
+ auth: keys.auth,
198
+ userAgent,
199
+ createdAt: now,
200
+ lastSeenAt: now
106
201
  }
107
- ],
108
- { onConflict: ["endpoint"] }
109
- );
202
+ ]);
203
+ } catch (error) {
204
+ if (isDuplicate(error)) {
205
+ return Response.json(
206
+ { error: "endpoint is registered to another account" },
207
+ { status: 409 }
208
+ );
209
+ }
210
+ throw error;
211
+ }
110
212
  return Response.json({ ok: true });
111
213
  }
112
214
  },
@@ -115,14 +217,13 @@ function push(options) {
115
217
  method: "POST",
116
218
  scope: { type: "user" },
117
219
  handler: async ({ request, userId }) => {
118
- const body = await request.json().catch(() => null);
119
- if (!body?.endpoint) {
220
+ const parsed = await readJsonBody(request);
221
+ if ("error" in parsed) return parsed.error;
222
+ const { endpoint } = parsed.body;
223
+ if (typeof endpoint !== "string" || endpoint === "") {
120
224
  return Response.json({ error: "endpoint is required" }, { status: 400 });
121
225
  }
122
- const removed = await ctx.store.remove(DEVICE, {
123
- userId: userId ?? "",
124
- endpoint: body.endpoint
125
- });
226
+ const removed = await ctx.store.remove(DEVICE, { userId: userId ?? "", endpoint });
126
227
  return removed === 0 ? new Response(null, { status: 404 }) : Response.json({ ok: true });
127
228
  }
128
229
  },
@@ -140,6 +241,6 @@ function push(options) {
140
241
  };
141
242
  }
142
243
 
143
- export { push, pushSchema };
244
+ export { push, pushSchema, validateEndpoint, validateSubscriptionKeys };
144
245
  //# sourceMappingURL=push.js.map
145
246
  //# sourceMappingURL=push.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/plugins/push/index.ts"],"names":[],"mappings":"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","file":"push.js","sourcesContent":["import type { DeliveryOutcome } from \"../../core/adapter\";\nimport type {\n DeliverContext,\n EasyPingPlugin,\n PluginInitContext,\n SchemaDeclaration,\n} from \"../../core/plugin\";\n\nconst DEVICE = \"notification_push_device\";\n\nexport type PushSubscription = {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n};\n\nexport type PushMessage = {\n subscription: PushSubscription;\n title: string;\n body: string;\n data?: Record<string, unknown> | undefined;\n signal?: AbortSignal | undefined;\n};\n\nexport type PushSendResult = {\n /** True when the endpoint is permanently gone (404/410) and must be pruned. */\n expired?: boolean;\n retryable?: boolean;\n};\n\nexport type PushProvider = {\n name: string;\n send(message: PushMessage): Promise<PushSendResult>;\n};\n\nexport type PushDevice = {\n id: string;\n userId: string;\n endpoint: string;\n p256dh: string;\n auth: string;\n userAgent: string | null;\n createdAt: Date;\n lastSeenAt: Date;\n};\n\nexport type PushOptions = {\n provider: PushProvider;\n /** Builds the notification shown on the device. */\n render: (input: { type: string; payload: unknown }) => { title: string; body: string };\n /** Devices untouched for this long are pruned on next delivery. */\n staleAfterDays?: number;\n};\n\n/**\n * Web push over the device registry.\n *\n * Tokens are per device, not per user, so a delivery fans out and succeeds if\n * any endpoint accepts. Endpoints that report themselves gone are deleted\n * immediately: an unpruned registry accumulates dead subscriptions forever and\n * every send slows down behind them.\n */\n/**\n * The tables this plugin owns, as a standalone value.\n *\n * Exported separately from `push()` so DDL can be rendered without building a\n * plugin instance — otherwise creating the tables means inventing a provider\n * and a render function you never intend to call.\n */\nexport const pushSchema = {\n pushDevice: {\n tableName: DEVICE,\n fields: {\n id: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n endpoint: { type: \"string\", required: true },\n p256dh: { type: \"string\", required: true },\n auth: { type: \"string\", required: true },\n userAgent: { type: \"string\" },\n createdAt: { type: \"date\", required: true, defaultNow: true },\n lastSeenAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"userId\"], name: \"push_device_user_idx\" },\n // One row per endpoint: re-registering the same browser must update,\n // not duplicate, or every send goes out N times to one device.\n { on: [\"endpoint\"], unique: true, name: \"push_device_endpoint_idx\" },\n ],\n },\n} as const satisfies SchemaDeclaration;\n\nexport function push(options: PushOptions): EasyPingPlugin<\"push\"> {\n let ctx: PluginInitContext;\n const staleAfterDays = options.staleAfterDays ?? 180;\n\n return {\n id: \"push\",\n channels: [\"push\"],\n\n init: (context) => {\n ctx = context;\n },\n\n schema: pushSchema,\n\n hooks: {\n deliver: async (context: DeliverContext): Promise<DeliveryOutcome> => {\n const devices = await ctx.store.find<PushDevice>(DEVICE, {\n userId: context.notification.userId,\n });\n\n if (devices.length === 0) {\n // Not a failure: this person has simply not enabled push anywhere.\n return { result: \"skipped\", reason: \"no registered devices\" };\n }\n\n const content = options.render({\n type: context.notification.type,\n payload: context.notification.payload,\n });\n\n const expired: string[] = [];\n let delivered = 0;\n let retryable = false;\n let lastError = \"\";\n\n for (const device of devices) {\n try {\n const result = await options.provider.send({\n subscription: {\n endpoint: device.endpoint,\n keys: { p256dh: device.p256dh, auth: device.auth },\n },\n ...content,\n data: { notificationId: context.notification.id, type: context.notification.type },\n signal: context.signal,\n });\n\n if (result.expired) expired.push(device.id);\n else {\n delivered += 1;\n if (result.retryable) retryable = true;\n }\n } catch (error) {\n lastError = error instanceof Error ? error.message : String(error);\n retryable = true;\n }\n }\n\n if (expired.length > 0) await ctx.store.remove(DEVICE, { id: { in: expired } });\n\n if (delivered > 0) return { result: \"sent\" };\n\n // Every endpoint was gone: retrying cannot help, and the rows are\n // already deleted.\n if (expired.length === devices.length) {\n return { result: \"failed\", error: \"all devices expired\", retryable: false };\n }\n\n return {\n result: \"failed\",\n error: lastError || \"no device accepted the push\",\n retryable,\n };\n },\n },\n\n routes: [\n {\n path: \"/push/devices\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ request, userId }) => {\n const body = (await request.json().catch(() => null)) as PushSubscription | null;\n if (!body?.endpoint || !body.keys?.p256dh || !body.keys?.auth) {\n return Response.json({ error: \"endpoint and keys are required\" }, { status: 400 });\n }\n\n // Upsert on endpoint: the same browser re-registering after a\n // service-worker update must not create a second row.\n await ctx.store.upsert(\n DEVICE,\n [\n {\n id: crypto.randomUUID(),\n userId: userId ?? \"\",\n endpoint: body.endpoint,\n p256dh: body.keys.p256dh,\n auth: body.keys.auth,\n userAgent: request.headers.get(\"user-agent\"),\n createdAt: new Date(),\n lastSeenAt: new Date(),\n },\n ],\n { onConflict: [\"endpoint\"] },\n );\n\n return Response.json({ ok: true });\n },\n },\n\n {\n path: \"/push/devices/remove\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ request, userId }) => {\n const body = (await request.json().catch(() => null)) as { endpoint?: string } | null;\n if (!body?.endpoint) {\n return Response.json({ error: \"endpoint is required\" }, { status: 400 });\n }\n\n // Scoped by userId as well as endpoint: an endpoint string is not a\n // secret, and must not let one user unregister another's device.\n const removed = await ctx.store.remove(DEVICE, {\n userId: userId ?? \"\",\n endpoint: body.endpoint,\n });\n\n return removed === 0 ? new Response(null, { status: 404 }) : Response.json({ ok: true });\n },\n },\n\n {\n path: \"/push/prune\",\n method: \"POST\",\n scope: { type: \"machine\" },\n handler: async () => {\n const cutoff = new Date(Date.now() - staleAfterDays * 86_400_000);\n const removed = await ctx.store.remove(DEVICE, { lastSeenAt: { lt: cutoff } });\n return Response.json({ removed });\n },\n },\n ],\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/plugins/push/index.ts"],"names":[],"mappings":"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","file":"push.js","sourcesContent":["import type { DeliveryOutcome } from \"../../core/adapter\";\nimport { decodeBase64UrlBytes } from \"../../core/base64url\";\nimport { readJsonBody } from \"../../core/handler\";\nimport type {\n DeliverContext,\n EasyPingPlugin,\n PluginInitContext,\n SchemaDeclaration,\n} from \"../../core/plugin\";\nimport { redactErrorMessage } from \"../../core/runner\";\n\nconst DEVICE = \"notification_push_device\";\n\nexport type PushSubscription = {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n};\n\nexport type PushMessage = {\n subscription: PushSubscription;\n title: string;\n body: string;\n data?: Record<string, unknown> | undefined;\n signal?: AbortSignal | undefined;\n};\n\nexport type PushSendResult = {\n /** True when the endpoint is permanently gone (404/410) and must be pruned. */\n expired?: boolean;\n retryable?: boolean;\n /** True when the subscription can never work — bad keys, unusable endpoint. Pruned like expired. */\n invalid?: boolean;\n};\n\nexport type PushProvider = {\n name: string;\n send(message: PushMessage): Promise<PushSendResult>;\n};\n\nexport type PushDevice = {\n id: string;\n userId: string;\n endpoint: string;\n p256dh: string;\n auth: string;\n userAgent: string | null;\n createdAt: Date;\n lastSeenAt: Date;\n};\n\nexport type PushOptions = {\n provider: PushProvider;\n /** Builds the notification shown on the device. */\n render: (input: { type: string; payload: unknown }) => { title: string; body: string };\n /** Devices untouched for this long are pruned on next delivery. */\n staleAfterDays?: number;\n /** Registering past this evicts the user's least recently seen device. Default 20. */\n maxDevicesPerUser?: number;\n /**\n * Hostnames an endpoint may point at, e.g. `[\"fcm.googleapis.com\",\n * \"*.notify.windows.com\"]`. Unset accepts any public https host, which is\n * what self-hosted UnifiedPush distributors need.\n */\n allowedEndpointHosts?: readonly string[];\n /** Accepts http and private hosts. For a local fake push service only; never in production. */\n allowInsecureEndpoints?: boolean;\n};\n\n/**\n * The tables this plugin owns, as a standalone value.\n *\n * Exported separately from `push()` so DDL can be rendered without building a\n * plugin instance — otherwise creating the tables means inventing a provider\n * and a render function you never intend to call.\n */\nexport const pushSchema = {\n pushDevice: {\n tableName: DEVICE,\n fields: {\n id: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n endpoint: { type: \"string\", required: true },\n p256dh: { type: \"string\", required: true },\n auth: { type: \"string\", required: true },\n userAgent: { type: \"string\" },\n createdAt: { type: \"date\", required: true, defaultNow: true },\n lastSeenAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"userId\"], name: \"push_device_user_idx\" },\n // One row per endpoint: re-registering the same browser must update,\n // not duplicate, or every send goes out N times to one device.\n { on: [\"endpoint\"], unique: true, name: \"push_device_endpoint_idx\" },\n ],\n },\n} as const satisfies SchemaDeclaration;\n\nconst PRIVATE_IPV4 =\n /^(0\\.|10\\.|127\\.|169\\.254\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.|100\\.(6[4-9]|[7-9]\\d|1[01]\\d|12[0-7])\\.)/;\n\nfunction isInternalHost(hostname: string): boolean {\n const host = hostname.toLowerCase();\n if (host === \"localhost\" || /\\.(localhost|local|internal|lan)$/.test(host)) return true;\n if (/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host)) return PRIVATE_IPV4.test(host);\n if (host.startsWith(\"[\")) {\n const v6 = host.slice(1, -1);\n return v6 === \"::1\" || v6 === \"::\" || /^(fc|fd|fe[89ab])/.test(v6) || v6.startsWith(\"::ffff:\");\n }\n return false;\n}\n\nconst hostAllowed = (host: string, allowed: readonly string[]) =>\n allowed.some((entry) =>\n entry.startsWith(\"*.\") ? host.endsWith(entry.slice(1)) : host === entry.toLowerCase(),\n );\n\n/**\n * The server will POST to whatever is stored here, so this is the SSRF\n * boundary: public https only, optionally pinned to known push services.\n */\nexport function validateEndpoint(\n endpoint: string,\n allowedHosts?: readonly string[] | undefined,\n allowInsecure = false,\n): string | null {\n let url: URL;\n try {\n url = new URL(endpoint);\n } catch {\n return \"endpoint is not a valid URL\";\n }\n if (url.username || url.password) return \"endpoint must not carry credentials\";\n if (!allowInsecure) {\n if (url.protocol !== \"https:\") return \"endpoint must use https\";\n if (isInternalHost(url.hostname)) return \"endpoint must be a public host\";\n } else if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n return \"endpoint must be an http(s) URL\";\n }\n if (allowedHosts && !hostAllowed(url.hostname.toLowerCase(), allowedHosts)) {\n return \"endpoint host is not an allowed push service\";\n }\n return null;\n}\n\n/** RFC 8291: an uncompressed P-256 point and a 16-byte auth secret. */\nexport function validateSubscriptionKeys(keys: { p256dh: string; auth: string }): string | null {\n const p256dh = decodeBase64UrlBytes(keys.p256dh);\n if (p256dh?.length !== 65 || p256dh[0] !== 0x04) {\n return \"keys.p256dh must be a 65-byte uncompressed EC point\";\n }\n const auth = decodeBase64UrlBytes(keys.auth);\n if (auth?.length !== 16) return \"keys.auth must be 16 bytes\";\n return null;\n}\n\nconst isDuplicate = (error: unknown): boolean => {\n const code = (error as { code?: unknown })?.code;\n if (code === \"23505\" || code === 11000) return true;\n const text = error instanceof Error ? error.message : String(error);\n return /duplicate|unique/i.test(text);\n};\n\n/**\n * Web push over the device registry.\n *\n * Tokens are per device, not per user, so a delivery fans out and succeeds if\n * any endpoint accepts. Endpoints that report themselves gone are deleted\n * immediately: an unpruned registry accumulates dead subscriptions forever and\n * every send slows down behind them.\n */\nexport function push(options: PushOptions): EasyPingPlugin<\"push\"> {\n let ctx: PluginInitContext;\n const staleAfterDays = options.staleAfterDays ?? 180;\n const maxDevices = Math.max(1, options.maxDevicesPerUser ?? 20);\n\n return {\n id: \"push\",\n channels: [\"push\"],\n\n init: (context) => {\n ctx = context;\n },\n\n schema: pushSchema,\n\n hooks: {\n deliver: async (context: DeliverContext): Promise<DeliveryOutcome> => {\n const devices = await ctx.store.find<PushDevice>(DEVICE, {\n userId: context.notification.userId,\n });\n\n if (devices.length === 0) {\n // Not a failure: this person has simply not enabled push anywhere.\n return { result: \"skipped\", reason: \"no registered devices\" };\n }\n\n const content = options.render({\n type: context.notification.type,\n payload: context.notification.payload,\n });\n\n // In parallel: one dead endpoint must not hold the others, or the\n // sweep, up to the runner's timeout.\n const results = await Promise.allSettled(\n devices.map((device) =>\n options.provider.send({\n subscription: {\n endpoint: device.endpoint,\n keys: { p256dh: device.p256dh, auth: device.auth },\n },\n ...content,\n data: { notificationId: context.notification.id, type: context.notification.type },\n signal: context.signal,\n }),\n ),\n );\n\n const prune: string[] = [];\n let delivered = 0;\n let retryable = false;\n let lastError = \"\";\n\n results.forEach((result, index) => {\n const device = devices[index];\n if (!device) return;\n\n if (result.status === \"rejected\") {\n lastError = redactErrorMessage(\n result.reason instanceof Error ? result.reason.message : String(result.reason),\n );\n retryable = true;\n return;\n }\n\n if (result.value.expired || result.value.invalid) prune.push(device.id);\n else if (result.value.retryable) retryable = true;\n else delivered += 1;\n });\n\n if (prune.length > 0) await ctx.store.remove(DEVICE, { id: { in: prune } });\n\n if (delivered > 0) return { result: \"sent\" };\n\n // Every endpoint was gone: retrying cannot help, and the rows are\n // already deleted.\n if (prune.length === devices.length) {\n return { result: \"failed\", error: \"all devices expired\", retryable: false };\n }\n\n return {\n result: \"failed\",\n error: lastError || \"no device accepted the push\",\n retryable,\n };\n },\n },\n\n routes: [\n {\n path: \"/push/devices\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ request, userId }) => {\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n\n const { endpoint, keys } = parsed.body as Partial<PushSubscription>;\n if (\n typeof endpoint !== \"string\" ||\n typeof keys?.p256dh !== \"string\" ||\n typeof keys?.auth !== \"string\"\n ) {\n return Response.json({ error: \"endpoint and keys are required\" }, { status: 400 });\n }\n\n const problem =\n validateEndpoint(\n endpoint,\n options.allowedEndpointHosts,\n options.allowInsecureEndpoints ?? false,\n ) ?? validateSubscriptionKeys(keys);\n if (problem) return Response.json({ error: problem }, { status: 400 });\n\n const owner = userId ?? \"\";\n const now = new Date();\n const userAgent = request.headers.get(\"user-agent\")?.slice(0, 512) ?? null;\n\n // An endpoint belongs to exactly one account for its whole life.\n // Upserting on endpoint alone let any caller re-home another user's\n // device by knowing its URL.\n const [existing] = await ctx.store.find<PushDevice>(DEVICE, { endpoint }, { limit: 1 });\n\n if (existing && existing.userId !== owner) {\n return Response.json(\n { error: \"endpoint is registered to another account\" },\n { status: 409 },\n );\n }\n\n if (existing) {\n await ctx.store.update(\n DEVICE,\n { id: existing.id },\n { p256dh: keys.p256dh, auth: keys.auth, userAgent, lastSeenAt: now },\n );\n return Response.json({ ok: true });\n }\n\n const devices = await ctx.store.find<PushDevice>(\n DEVICE,\n { userId: owner },\n { orderBy: { field: \"lastSeenAt\", direction: \"asc\" } },\n );\n const excess = devices.length - maxDevices + 1;\n if (excess > 0) {\n await ctx.store.remove(DEVICE, {\n id: { in: devices.slice(0, excess).map((device) => device.id) },\n });\n }\n\n try {\n await ctx.store.insert(DEVICE, [\n {\n id: crypto.randomUUID(),\n userId: owner,\n endpoint,\n p256dh: keys.p256dh,\n auth: keys.auth,\n userAgent,\n createdAt: now,\n lastSeenAt: now,\n },\n ]);\n } catch (error) {\n // Lost the race to the unique index: someone registered it first.\n if (isDuplicate(error)) {\n return Response.json(\n { error: \"endpoint is registered to another account\" },\n { status: 409 },\n );\n }\n throw error;\n }\n\n return Response.json({ ok: true });\n },\n },\n\n {\n path: \"/push/devices/remove\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ request, userId }) => {\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n\n const { endpoint } = parsed.body;\n if (typeof endpoint !== \"string\" || endpoint === \"\") {\n return Response.json({ error: \"endpoint is required\" }, { status: 400 });\n }\n\n // Scoped by userId as well as endpoint: an endpoint string is not a\n // secret, and must not let one user unregister another's device.\n const removed = await ctx.store.remove(DEVICE, { userId: userId ?? \"\", endpoint });\n\n return removed === 0 ? new Response(null, { status: 404 }) : Response.json({ ok: true });\n },\n },\n\n {\n path: \"/push/prune\",\n method: \"POST\",\n scope: { type: \"machine\" },\n handler: async () => {\n const cutoff = new Date(Date.now() - staleAfterDays * 86_400_000);\n const removed = await ctx.store.remove(DEVICE, { lastSeenAt: { lt: cutoff } });\n return Response.json({ removed });\n },\n },\n ],\n };\n}\n"]}
@@ -28,11 +28,17 @@ function concat(...parts) {
28
28
  }
29
29
  return out;
30
30
  }
31
- function requireBytes(value, label, length) {
31
+ var InvalidSubscriptionError = class extends Error {
32
+ constructor(message) {
33
+ super(message);
34
+ this.name = "InvalidSubscriptionError";
35
+ }
36
+ };
37
+ function requireBytes(value, label, length, Failure = Error) {
32
38
  const bytes = decodeBase64UrlBytes(value);
33
- if (!bytes) throw new Error(`${label} is not valid base64url`);
39
+ if (!bytes) throw new Failure(`${label} is not valid base64url`);
34
40
  if (length !== void 0 && bytes.length !== length) {
35
- throw new Error(`${label} must be ${length} bytes, got ${bytes.length}`);
41
+ throw new Failure(`${label} must be ${length} bytes, got ${bytes.length}`);
36
42
  }
37
43
  return bytes;
38
44
  }
@@ -51,8 +57,11 @@ async function encryptPayload(plaintext, userAgentPublicKey, authSecret, overrid
51
57
  if (plaintext.length > MAX_PAYLOAD_BYTES) {
52
58
  throw new Error(`push payload is ${plaintext.length} bytes; the limit is ${MAX_PAYLOAD_BYTES}`);
53
59
  }
54
- const uaPublic = requireBytes(userAgentPublicKey, "p256dh", 65);
55
- const auth = requireBytes(authSecret, "auth", 16);
60
+ const uaPublic = requireBytes(userAgentPublicKey, "p256dh", 65, InvalidSubscriptionError);
61
+ const auth = requireBytes(authSecret, "auth", 16, InvalidSubscriptionError);
62
+ if (uaPublic[0] !== 4) {
63
+ throw new InvalidSubscriptionError("p256dh must be an uncompressed EC point");
64
+ }
56
65
  const salt = overrides?.salt ? new Uint8Array(overrides.salt) : crypto.getRandomValues(new Uint8Array(16));
57
66
  const serverKeys = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
58
67
  const serverPublic = new Uint8Array(
@@ -165,12 +174,28 @@ function webPush(options) {
165
174
  `push payload is ${payload.length} bytes, over the ${MAX_PAYLOAD_BYTES} byte limit`
166
175
  );
167
176
  }
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, {
177
+ let endpoint;
178
+ try {
179
+ endpoint = new URL(message.subscription.endpoint);
180
+ } catch {
181
+ return { invalid: true };
182
+ }
183
+ const secure = endpoint.protocol === "https:";
184
+ if (!secure && !(options.allowInsecureEndpoints && endpoint.protocol === "http:")) {
185
+ return { invalid: true };
186
+ }
187
+ let encrypted;
188
+ try {
189
+ encrypted = await encryptPayload(
190
+ payload,
191
+ message.subscription.keys.p256dh,
192
+ message.subscription.keys.auth
193
+ );
194
+ } catch (error) {
195
+ if (error instanceof InvalidSubscriptionError) return { invalid: true };
196
+ throw error;
197
+ }
198
+ const authorization = await vapidAuthorization(endpoint.href, {
174
199
  subject: options.subject,
175
200
  keys: options.vapid
176
201
  });
@@ -204,6 +229,7 @@ function webPush(options) {
204
229
  };
205
230
  }
206
231
 
232
+ exports.InvalidSubscriptionError = InvalidSubscriptionError;
207
233
  exports.MAX_PAYLOAD_BYTES = MAX_PAYLOAD_BYTES;
208
234
  exports.generateVapidKeys = generateVapidKeys;
209
235
  exports.webPush = webPush;
@@ -1 +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"]}
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\n/** The subscription itself is unusable. Distinct from a transport failure so the plugin prunes rather than retries. */\nexport class InvalidSubscriptionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"InvalidSubscriptionError\";\n }\n}\n\nfunction requireBytes(\n value: string,\n label: string,\n length?: number,\n Failure: new (message: string) => Error = Error,\n): Uint8Array<ArrayBuffer> {\n const bytes = decodeBase64UrlBytes(value);\n if (!bytes) throw new Failure(`${label} is not valid base64url`);\n if (length !== undefined && bytes.length !== length) {\n throw new Failure(`${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, InvalidSubscriptionError);\n const auth = requireBytes(authSecret, \"auth\", 16, InvalidSubscriptionError);\n if (uaPublic[0] !== 0x04) {\n throw new InvalidSubscriptionError(\"p256dh must be an uncompressed EC point\");\n }\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 {\n encryptPayload,\n InvalidSubscriptionError,\n MAX_PAYLOAD_BYTES,\n type VapidKeys,\n vapidAuthorization,\n} 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 /** Lets an http endpoint through. For a local fake push service only; never in production. */\n allowInsecureEndpoints?: boolean;\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 // A subscription that cannot be encrypted for, or whose endpoint is not\n // an https URL, will never deliver. Report it so the row is pruned\n // instead of retried five times.\n let endpoint: URL;\n try {\n endpoint = new URL(message.subscription.endpoint);\n } catch {\n return { invalid: true };\n }\n const secure = endpoint.protocol === \"https:\";\n if (!secure && !(options.allowInsecureEndpoints && endpoint.protocol === \"http:\")) {\n return { invalid: true };\n }\n\n let encrypted: Awaited<ReturnType<typeof encryptPayload>>;\n try {\n encrypted = await encryptPayload(\n payload,\n message.subscription.keys.p256dh,\n message.subscription.keys.auth,\n );\n } catch (error) {\n if (error instanceof InvalidSubscriptionError) return { invalid: true };\n throw error;\n }\n\n const authorization = await vapidAuthorization(endpoint.href, {\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, InvalidSubscriptionError, MAX_PAYLOAD_BYTES } from \"./crypto\";\n"]}
@@ -1,7 +1,11 @@
1
1
  import { PushProvider } from '../plugins/push.cjs';
2
- import '../plugin-f5KaFWSN.cjs';
2
+ import '../plugin-CiMsGlQe.cjs';
3
3
  import '../adapter-BfLe2J0t.cjs';
4
4
 
5
+ /** The subscription itself is unusable. Distinct from a transport failure so the plugin prunes rather than retries. */
6
+ declare class InvalidSubscriptionError extends Error {
7
+ constructor(message: string);
8
+ }
5
9
  /** One record: record size less the padding delimiter and the GCM tag. */
6
10
  declare const MAX_PAYLOAD_BYTES: number;
7
11
  type VapidKeys = {
@@ -19,6 +23,8 @@ type WebPushOptions = {
19
23
  ttlSeconds?: number;
20
24
  urgency?: "very-low" | "low" | "normal" | "high";
21
25
  fetch?: typeof globalThis.fetch;
26
+ /** Lets an http endpoint through. For a local fake push service only; never in production. */
27
+ allowInsecureEndpoints?: boolean;
22
28
  };
23
29
  /**
24
30
  * Web push over Web Crypto, so it runs on Workers and Edge as well as Node.
@@ -30,4 +36,4 @@ type WebPushOptions = {
30
36
  */
31
37
  declare function webPush(options: WebPushOptions): PushProvider;
32
38
 
33
- export { MAX_PAYLOAD_BYTES, type VapidKeys, type WebPushOptions, generateVapidKeys, webPush };
39
+ export { InvalidSubscriptionError, MAX_PAYLOAD_BYTES, type VapidKeys, type WebPushOptions, generateVapidKeys, webPush };
@@ -1,7 +1,11 @@
1
1
  import { PushProvider } from '../plugins/push.js';
2
- import '../plugin-CmPOFVmn.js';
2
+ import '../plugin-C9Hoxus2.js';
3
3
  import '../adapter-BfLe2J0t.js';
4
4
 
5
+ /** The subscription itself is unusable. Distinct from a transport failure so the plugin prunes rather than retries. */
6
+ declare class InvalidSubscriptionError extends Error {
7
+ constructor(message: string);
8
+ }
5
9
  /** One record: record size less the padding delimiter and the GCM tag. */
6
10
  declare const MAX_PAYLOAD_BYTES: number;
7
11
  type VapidKeys = {
@@ -19,6 +23,8 @@ type WebPushOptions = {
19
23
  ttlSeconds?: number;
20
24
  urgency?: "very-low" | "low" | "normal" | "high";
21
25
  fetch?: typeof globalThis.fetch;
26
+ /** Lets an http endpoint through. For a local fake push service only; never in production. */
27
+ allowInsecureEndpoints?: boolean;
22
28
  };
23
29
  /**
24
30
  * Web push over Web Crypto, so it runs on Workers and Edge as well as Node.
@@ -30,4 +36,4 @@ type WebPushOptions = {
30
36
  */
31
37
  declare function webPush(options: WebPushOptions): PushProvider;
32
38
 
33
- export { MAX_PAYLOAD_BYTES, type VapidKeys, type WebPushOptions, generateVapidKeys, webPush };
39
+ export { InvalidSubscriptionError, MAX_PAYLOAD_BYTES, type VapidKeys, type WebPushOptions, generateVapidKeys, webPush };
@@ -12,11 +12,17 @@ function concat(...parts) {
12
12
  }
13
13
  return out;
14
14
  }
15
- function requireBytes(value, label, length) {
15
+ var InvalidSubscriptionError = class extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "InvalidSubscriptionError";
19
+ }
20
+ };
21
+ function requireBytes(value, label, length, Failure = Error) {
16
22
  const bytes = decodeBase64UrlBytes(value);
17
- if (!bytes) throw new Error(`${label} is not valid base64url`);
23
+ if (!bytes) throw new Failure(`${label} is not valid base64url`);
18
24
  if (length !== void 0 && bytes.length !== length) {
19
- throw new Error(`${label} must be ${length} bytes, got ${bytes.length}`);
25
+ throw new Failure(`${label} must be ${length} bytes, got ${bytes.length}`);
20
26
  }
21
27
  return bytes;
22
28
  }
@@ -35,8 +41,11 @@ async function encryptPayload(plaintext, userAgentPublicKey, authSecret, overrid
35
41
  if (plaintext.length > MAX_PAYLOAD_BYTES) {
36
42
  throw new Error(`push payload is ${plaintext.length} bytes; the limit is ${MAX_PAYLOAD_BYTES}`);
37
43
  }
38
- const uaPublic = requireBytes(userAgentPublicKey, "p256dh", 65);
39
- const auth = requireBytes(authSecret, "auth", 16);
44
+ const uaPublic = requireBytes(userAgentPublicKey, "p256dh", 65, InvalidSubscriptionError);
45
+ const auth = requireBytes(authSecret, "auth", 16, InvalidSubscriptionError);
46
+ if (uaPublic[0] !== 4) {
47
+ throw new InvalidSubscriptionError("p256dh must be an uncompressed EC point");
48
+ }
40
49
  const salt = overrides?.salt ? new Uint8Array(overrides.salt) : crypto.getRandomValues(new Uint8Array(16));
41
50
  const serverKeys = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
42
51
  const serverPublic = new Uint8Array(
@@ -149,12 +158,28 @@ function webPush(options) {
149
158
  `push payload is ${payload.length} bytes, over the ${MAX_PAYLOAD_BYTES} byte limit`
150
159
  );
151
160
  }
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, {
161
+ let endpoint;
162
+ try {
163
+ endpoint = new URL(message.subscription.endpoint);
164
+ } catch {
165
+ return { invalid: true };
166
+ }
167
+ const secure = endpoint.protocol === "https:";
168
+ if (!secure && !(options.allowInsecureEndpoints && endpoint.protocol === "http:")) {
169
+ return { invalid: true };
170
+ }
171
+ let encrypted;
172
+ try {
173
+ encrypted = await encryptPayload(
174
+ payload,
175
+ message.subscription.keys.p256dh,
176
+ message.subscription.keys.auth
177
+ );
178
+ } catch (error) {
179
+ if (error instanceof InvalidSubscriptionError) return { invalid: true };
180
+ throw error;
181
+ }
182
+ const authorization = await vapidAuthorization(endpoint.href, {
158
183
  subject: options.subject,
159
184
  keys: options.vapid
160
185
  });
@@ -188,6 +213,6 @@ function webPush(options) {
188
213
  };
189
214
  }
190
215
 
191
- export { MAX_PAYLOAD_BYTES, generateVapidKeys, webPush };
216
+ export { InvalidSubscriptionError, MAX_PAYLOAD_BYTES, generateVapidKeys, webPush };
192
217
  //# sourceMappingURL=web-push.js.map
193
218
  //# sourceMappingURL=web-push.js.map