@voltro/plugin-notifications 0.85.0 → 0.87.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/dist/store.js ADDED
@@ -0,0 +1,303 @@
1
+ import { and as e, eq as t, generateId as n, isNull as r, lte as i } from "@voltro/database";
2
+ //#region src/store.ts
3
+ var a = () => {
4
+ let e = [], t = /* @__PURE__ */ new Map(), n = [], r = /* @__PURE__ */ new Map(), i = /* @__PURE__ */ new Map(), a = [], o = (e) => `${e.subjectId}|${e.category}|${e.channel}`, s = (e, t) => `${e}|${t}`;
5
+ return {
6
+ addInbox: async (t) => {
7
+ e.unshift({
8
+ ...t,
9
+ readAt: null,
10
+ archivedAt: null,
11
+ data: t.data ?? {},
12
+ tenantId: t.tenantId ?? null
13
+ });
14
+ },
15
+ listInbox: async (t, n) => {
16
+ let r = e.filter((e) => e.subjectId === t);
17
+ return n?.unreadOnly && (r = r.filter((e) => e.readAt === null)), n?.limit === void 0 ? r : r.slice(0, n.limit);
18
+ },
19
+ markRead: async (t, n) => {
20
+ let r = e.findIndex((e) => e.id === t && e.subjectId === n);
21
+ return r !== -1 && (e[r] = {
22
+ ...e[r],
23
+ readAt: (/* @__PURE__ */ new Date(0)).toISOString()
24
+ }, !0);
25
+ },
26
+ markUnread: async (t, n) => {
27
+ let r = e.findIndex((e) => e.id === t && e.subjectId === n);
28
+ return r !== -1 && (e[r] = {
29
+ ...e[r],
30
+ readAt: null
31
+ }, !0);
32
+ },
33
+ markAllRead: async (t) => {
34
+ let n = 0;
35
+ for (let r = 0; r < e.length; r++) {
36
+ let i = e[r];
37
+ i.subjectId === t && i.readAt === null && (e[r] = {
38
+ ...i,
39
+ readAt: (/* @__PURE__ */ new Date(0)).toISOString()
40
+ }, n++);
41
+ }
42
+ return n;
43
+ },
44
+ setArchived: async (t, n, r) => {
45
+ let i = e.findIndex((e) => e.id === t && e.subjectId === n);
46
+ return i !== -1 && (e[i] = {
47
+ ...e[i],
48
+ archivedAt: r ? (/* @__PURE__ */ new Date(0)).toISOString() : null
49
+ }, !0);
50
+ },
51
+ unreadCount: async (t) => e.filter((e) => e.subjectId === t && e.readAt === null).length,
52
+ getPreferences: async (e) => [...t.values()].filter((t) => t.subjectId === e),
53
+ setPreference: async (e) => {
54
+ t.set(o(e), e);
55
+ },
56
+ recordDelivery: async (e) => {
57
+ n.unshift(e);
58
+ },
59
+ listDeliveries: async (e) => e?.limit === void 0 ? [...n] : n.slice(0, e.limit),
60
+ markClicked: async (e) => {
61
+ let t = n.findIndex((t) => t.clickToken === e);
62
+ return t !== -1 && (n[t] = {
63
+ ...n[t],
64
+ clickedAt: (/* @__PURE__ */ new Date()).toISOString()
65
+ }, !0);
66
+ },
67
+ subscribeTopic: async (e) => {
68
+ r.set(s(e.topic, e.subjectId), e);
69
+ },
70
+ unsubscribeTopic: async (e, t) => {
71
+ r.delete(s(e, t));
72
+ },
73
+ listTopicSubscribers: async (e) => [...r.values()].filter((t) => t.topic === e),
74
+ getQuietHours: async (e) => i.get(e) ?? null,
75
+ setQuietHours: async (e) => {
76
+ i.set(e.subjectId, e);
77
+ },
78
+ clearQuietHours: async (e) => {
79
+ i.delete(e);
80
+ },
81
+ enqueueHeld: async (e) => {
82
+ a.push(e);
83
+ },
84
+ pendingDigest: async (e) => a.filter((t) => t.kind === "digest" && t.subjectId === e),
85
+ dueHeld: async (e) => a.filter((t) => t.flushAt <= e).sort((e, t) => e.flushAt < t.flushAt ? -1 : 1),
86
+ clearHeld: async (e) => {
87
+ let t = new Set(e);
88
+ for (let e = a.length - 1; e >= 0; e--) t.has(a[e].id) && a.splice(e, 1);
89
+ }
90
+ };
91
+ }, o = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "", s = (a) => {
92
+ let s = async (e, t, n, r) => await a.query({
93
+ table: e,
94
+ predicate: t,
95
+ order: n,
96
+ take: r,
97
+ skip: void 0,
98
+ projection: void 0
99
+ }), c = (e) => ({
100
+ id: String(e.id),
101
+ subjectId: String(e.subjectId),
102
+ category: String(e.category),
103
+ title: String(e.title),
104
+ body: String(e.body),
105
+ data: e.data ?? {},
106
+ readAt: e.readAt == null ? null : o(e.readAt),
107
+ archivedAt: e.archivedAt == null ? null : o(e.archivedAt),
108
+ createdAt: o(e.createdAt),
109
+ tenantId: e.tenantId ?? null
110
+ }), l = (e) => ({
111
+ subjectId: String(e.subjectId),
112
+ category: String(e.category),
113
+ channel: String(e.channel),
114
+ enabled: !!e.enabled
115
+ }), u = (e) => ({
116
+ id: String(e.id),
117
+ to: String(e.recipient),
118
+ category: String(e.category),
119
+ channel: String(e.channel),
120
+ status: e.status,
121
+ at: o(e.at),
122
+ ...e.error == null ? {} : { error: String(e.error) },
123
+ ...e.endpoint == null ? {} : { endpoint: String(e.endpoint) },
124
+ ...e.clickToken == null ? {} : { clickToken: String(e.clickToken) },
125
+ ...e.clickedAt == null ? {} : { clickedAt: o(e.clickedAt) }
126
+ }), d = (e) => ({
127
+ topic: String(e.topic),
128
+ subjectId: String(e.subjectId),
129
+ tenantId: e.tenantId ?? null
130
+ }), f = (e) => ({
131
+ subjectId: String(e.subjectId),
132
+ startMinute: Number(e.startMinute),
133
+ endMinute: Number(e.endMinute),
134
+ tz: String(e.tz ?? "UTC"),
135
+ policy: e.policy === "drop" ? "drop" : "hold"
136
+ }), p = (e) => ({
137
+ id: String(e.id),
138
+ kind: e.kind === "quiet" ? "quiet" : "digest",
139
+ subjectId: String(e.subjectId),
140
+ send: e.send ?? {},
141
+ flushAt: o(e.flushAt)
142
+ });
143
+ return {
144
+ addInbox: async (e) => {
145
+ await a.insert("_voltro_notification_inbox", {
146
+ id: e.id,
147
+ subjectId: e.subjectId,
148
+ category: e.category,
149
+ title: e.title,
150
+ body: e.body,
151
+ data: e.data ?? {},
152
+ readAt: null,
153
+ archivedAt: null,
154
+ createdAt: e.createdAt,
155
+ tenantId: e.tenantId ?? null
156
+ });
157
+ },
158
+ listInbox: async (n, i) => {
159
+ let a = i?.unreadOnly ? e(t("subjectId", n), r("readAt")) : t("subjectId", n);
160
+ return (await s("_voltro_notification_inbox", a, [{
161
+ column: "createdAt",
162
+ direction: "desc"
163
+ }], i?.limit)).map(c);
164
+ },
165
+ markRead: async (n, r) => (await s("_voltro_notification_inbox", e(t("id", n), t("subjectId", r)), [], 1))[0] ? (await a.update("_voltro_notification_inbox", n, { readAt: (/* @__PURE__ */ new Date()).toISOString() }), !0) : !1,
166
+ markUnread: async (n, r) => (await s("_voltro_notification_inbox", e(t("id", n), t("subjectId", r)), [], 1))[0] ? (await a.update("_voltro_notification_inbox", n, { readAt: null }), !0) : !1,
167
+ markAllRead: async (n) => {
168
+ let i = /* @__PURE__ */ new Date();
169
+ return a.updateMany("_voltro_notification_inbox", { readAt: i }, { where: e(t("subjectId", n), r("readAt")) });
170
+ },
171
+ setArchived: async (n, r, i) => (await s("_voltro_notification_inbox", e(t("id", n), t("subjectId", r)), [], 1))[0] ? (await a.update("_voltro_notification_inbox", n, { archivedAt: i ? (/* @__PURE__ */ new Date()).toISOString() : null }), !0) : !1,
172
+ unreadCount: async (n) => {
173
+ let i = await a.query({
174
+ table: "_voltro_notification_inbox",
175
+ predicate: e(t("subjectId", n), r("readAt")),
176
+ order: [],
177
+ take: void 0,
178
+ skip: void 0,
179
+ projection: void 0,
180
+ aggregations: [{
181
+ op: "count",
182
+ alias: "count"
183
+ }]
184
+ });
185
+ return Number(i[0]?.count ?? 0);
186
+ },
187
+ getPreferences: async (e) => (await s("_voltro_notification_preferences", t("subjectId", e), [], void 0)).map(l),
188
+ setPreference: async (e) => {
189
+ let t = n({
190
+ kind: "typeid",
191
+ prefix: "notifpref"
192
+ }, "_voltro_notification_preferences");
193
+ await a.upsert("_voltro_notification_preferences", {
194
+ id: t,
195
+ subjectId: e.subjectId,
196
+ category: e.category,
197
+ channel: e.channel,
198
+ enabled: e.enabled
199
+ }, {
200
+ conflictColumns: [
201
+ "subjectId",
202
+ "category",
203
+ "channel"
204
+ ],
205
+ update: ["enabled"]
206
+ });
207
+ },
208
+ recordDelivery: async (e) => {
209
+ await a.insert("_voltro_notification_deliveries", {
210
+ id: e.id,
211
+ recipient: e.to,
212
+ category: e.category,
213
+ channel: e.channel,
214
+ status: e.status,
215
+ error: e.error ?? null,
216
+ at: e.at,
217
+ endpoint: e.endpoint ?? null,
218
+ clickToken: e.clickToken ?? null,
219
+ clickedAt: e.clickedAt ?? null
220
+ });
221
+ },
222
+ markClicked: async (e) => {
223
+ let n = await s("_voltro_notification_deliveries", t("clickToken", e), [], 1);
224
+ return n[0] ? (await a.update("_voltro_notification_deliveries", String(n[0].id), { clickedAt: (/* @__PURE__ */ new Date()).toISOString() }), !0) : !1;
225
+ },
226
+ listDeliveries: async (e) => (await s("_voltro_notification_deliveries", void 0, [{
227
+ column: "at",
228
+ direction: "desc"
229
+ }], e?.limit)).map(u),
230
+ subscribeTopic: async (e) => {
231
+ let t = n({
232
+ kind: "typeid",
233
+ prefix: "notiftsub"
234
+ }, "_voltro_notification_topic_subscriptions");
235
+ await a.upsert("_voltro_notification_topic_subscriptions", {
236
+ id: t,
237
+ topic: e.topic,
238
+ subjectId: e.subjectId,
239
+ tenantId: e.tenantId ?? null
240
+ }, {
241
+ conflictColumns: ["topic", "subjectId"],
242
+ update: ["tenantId"]
243
+ });
244
+ },
245
+ unsubscribeTopic: async (n, r) => {
246
+ let i = await s("_voltro_notification_topic_subscriptions", e(t("topic", n), t("subjectId", r)), [], 1);
247
+ i[0] && await a.delete("_voltro_notification_topic_subscriptions", String(i[0].id));
248
+ },
249
+ listTopicSubscribers: async (e) => (await s("_voltro_notification_topic_subscriptions", t("topic", e), [], void 0)).map(d),
250
+ getQuietHours: async (e) => {
251
+ let n = await s("_voltro_notification_quiet_hours", t("subjectId", e), [], 1);
252
+ return n[0] ? f(n[0]) : null;
253
+ },
254
+ setQuietHours: async (e) => {
255
+ let t = n({
256
+ kind: "typeid",
257
+ prefix: "notifqh"
258
+ }, "_voltro_notification_quiet_hours");
259
+ await a.upsert("_voltro_notification_quiet_hours", {
260
+ id: t,
261
+ subjectId: e.subjectId,
262
+ startMinute: e.startMinute,
263
+ endMinute: e.endMinute,
264
+ tz: e.tz ?? "UTC",
265
+ policy: e.policy ?? "hold"
266
+ }, {
267
+ conflictColumns: ["subjectId"],
268
+ update: [
269
+ "startMinute",
270
+ "endMinute",
271
+ "tz",
272
+ "policy"
273
+ ]
274
+ });
275
+ },
276
+ clearQuietHours: async (e) => {
277
+ let n = await s("_voltro_notification_quiet_hours", t("subjectId", e), [], 1);
278
+ n[0] && await a.delete("_voltro_notification_quiet_hours", String(n[0].id));
279
+ },
280
+ enqueueHeld: async (e) => {
281
+ await a.insert("_voltro_notification_held", {
282
+ id: e.id,
283
+ kind: e.kind,
284
+ subjectId: e.subjectId,
285
+ send: e.send,
286
+ flushAt: e.flushAt
287
+ });
288
+ },
289
+ pendingDigest: async (n) => (await s("_voltro_notification_held", e(t("kind", "digest"), t("subjectId", n)), [{
290
+ column: "flushAt",
291
+ direction: "asc"
292
+ }], void 0)).map(p),
293
+ dueHeld: async (e) => (await s("_voltro_notification_held", i("flushAt", e), [{
294
+ column: "flushAt",
295
+ direction: "asc"
296
+ }], void 0)).map(p),
297
+ clearHeld: async (e) => {
298
+ for (let t of e) await a.delete("_voltro_notification_held", t);
299
+ }
300
+ };
301
+ };
302
+ //#endregion
303
+ export { s as dataStoreNotificationStore, a as memoryNotificationStore };
@@ -0,0 +1,161 @@
1
+ import { sendWebPush as e, vapidPublicKeyFor as t } from "./webPushProtocol.js";
2
+ import { and as n, eq as r, generateId as i, id as a, table as o, text as s, timestamp as c } from "@voltro/database";
3
+ import { createHash as l, randomBytes as u } from "node:crypto";
4
+ //#region src/webPush.ts
5
+ var d = o("_voltro_notification_push_subscriptions", {
6
+ id: a({ prefix: "notifpush" }),
7
+ subjectId: s(),
8
+ endpointHash: s(),
9
+ endpoint: s(),
10
+ p256dh: s(),
11
+ auth: s(),
12
+ ua: s().nullable(),
13
+ createdAt: c().default("now"),
14
+ tenantId: s().nullable()
15
+ }).unique("byPushEndpointHash", ["endpointHash"]).index("byPushSubject", ["subjectId"]), f = (e) => l("sha256").update(e).digest("hex"), p = () => {
16
+ let e = /* @__PURE__ */ new Map();
17
+ return {
18
+ upsert: async (t) => {
19
+ e.set(t.endpointHash, t);
20
+ },
21
+ listForSubject: async (t) => [...e.values()].filter((e) => e.subjectId === t),
22
+ removeForSubject: async (t, n) => {
23
+ let r = e.get(t);
24
+ return !r || r.subjectId !== n ? !1 : (e.delete(t), !0);
25
+ },
26
+ removeByHash: async (t) => {
27
+ e.delete(t);
28
+ },
29
+ countForSubject: async (t) => [...e.values()].filter((e) => e.subjectId === t).length
30
+ };
31
+ }, m = "_voltro_notification_push_subscriptions", h = (e) => {
32
+ let t = async (t, n) => e.query({
33
+ table: m,
34
+ predicate: t,
35
+ order: [],
36
+ take: n,
37
+ skip: void 0,
38
+ projection: void 0
39
+ }), a = (e) => ({
40
+ subjectId: String(e.subjectId),
41
+ endpoint: String(e.endpoint),
42
+ endpointHash: String(e.endpointHash),
43
+ p256dh: String(e.p256dh),
44
+ auth: String(e.auth),
45
+ ua: e.ua == null ? null : String(e.ua),
46
+ tenantId: e.tenantId == null ? null : String(e.tenantId)
47
+ });
48
+ return {
49
+ upsert: async (t) => {
50
+ let n = i({
51
+ kind: "typeid",
52
+ prefix: "notifpush"
53
+ }, m);
54
+ await e.upsert(m, {
55
+ id: n,
56
+ subjectId: t.subjectId,
57
+ endpointHash: t.endpointHash,
58
+ endpoint: t.endpoint,
59
+ p256dh: t.p256dh,
60
+ auth: t.auth,
61
+ ua: t.ua,
62
+ tenantId: t.tenantId
63
+ }, {
64
+ conflictColumns: ["endpointHash"],
65
+ update: [
66
+ "subjectId",
67
+ "p256dh",
68
+ "auth",
69
+ "ua",
70
+ "tenantId"
71
+ ]
72
+ });
73
+ },
74
+ listForSubject: async (e) => (await t(r("subjectId", e))).map(a),
75
+ removeForSubject: async (i, a) => {
76
+ let o = await t(n(r("endpointHash", i), r("subjectId", a)), 1);
77
+ return o[0] ? (await e.delete(m, String(o[0].id)), !0) : !1;
78
+ },
79
+ removeByHash: async (n) => {
80
+ let i = await t(r("endpointHash", n), 1);
81
+ i[0] && await e.delete(m, String(i[0].id));
82
+ },
83
+ countForSubject: async (e) => (await t(r("subjectId", e))).length
84
+ };
85
+ }, g = Symbol.for("@voltro/plugin-notifications:web-push"), _ = () => {
86
+ let e = globalThis;
87
+ return e[g] ??= {
88
+ store: p(),
89
+ bound: !1
90
+ };
91
+ }, v = (e) => {
92
+ let t = _();
93
+ t.store = h(e), t.bound = !0;
94
+ }, y = () => {
95
+ let e = _();
96
+ e.store = p(), e.bound = !1;
97
+ }, b = (e) => {
98
+ let t = e ?? process.env.VOLTRO_VAPID_PRIVATE_KEY;
99
+ if (t === void 0 || t.trim() === "") throw Error("web push needs VOLTRO_VAPID_PRIVATE_KEY (a base64url P-256 scalar). `voltro dev` mints one into .env.local; for production generate one and set it in the deployment environment. There is deliberately no default.");
100
+ return t;
101
+ }, x = 3800, S = (e, t) => {
102
+ let n = typeof e.data?.url == "string" ? e.data.url : void 0, r = {
103
+ title: e.title,
104
+ body: e.body,
105
+ category: e.category,
106
+ clickToken: t,
107
+ ...n === void 0 ? {} : { url: n },
108
+ ...e.data === void 0 ? {} : { data: e.data }
109
+ }, i = Buffer.from(JSON.stringify(r));
110
+ if (i.length <= x) return i;
111
+ let { data: a, ...o } = r;
112
+ if (i = Buffer.from(JSON.stringify(o)), i.length <= x) return i;
113
+ let s = i.length - Buffer.byteLength(o.body), c = Math.max(0, x - s - Buffer.byteLength("…")), l = Buffer.from(o.body).subarray(0, c).toString("utf8").replace(/�+$/u, "");
114
+ return Buffer.from(JSON.stringify({
115
+ ...o,
116
+ body: `${l}…`
117
+ }));
118
+ }, C = (e = {}) => (_().channelOptions = e, T(e)), w = () => {
119
+ let e = _().channelOptions;
120
+ t(b(e?.privateKey));
121
+ }, T = (e) => ({
122
+ id: e.id ?? "webPush",
123
+ deliver: async (t) => {
124
+ let n = await E(t, e), r = n.filter((e) => e.status === "failed");
125
+ if (n.length > 0 && r.length === n.length) throw Error(`web push failed for all ${r.length} endpoint(s): ${r[0].error ?? "unknown"}`);
126
+ },
127
+ deliverDetailed: (t) => E(t, e)
128
+ }), E = async (t, n) => {
129
+ let r = b(n.privateKey), i = await _().store.listForSubject(t.to), a = [];
130
+ for (let o of i) {
131
+ let i = u(16).toString("base64url"), s = {
132
+ privateKey: r,
133
+ ...n.contact === void 0 ? {} : { contact: n.contact },
134
+ ...n.ttl === void 0 ? {} : { ttl: n.ttl },
135
+ ...n.fetchImpl === void 0 ? {} : { fetchImpl: n.fetchImpl }
136
+ }, c = await e({
137
+ endpoint: o.endpoint,
138
+ keys: {
139
+ p256dh: o.p256dh,
140
+ auth: o.auth
141
+ }
142
+ }, S(t, i), s);
143
+ c.kind === "delivered" ? a.push({
144
+ endpoint: o.endpoint,
145
+ status: "sent",
146
+ clickToken: i
147
+ }) : c.kind === "gone" ? (await _().store.removeByHash(o.endpointHash), a.push({
148
+ endpoint: o.endpoint,
149
+ status: "failed",
150
+ error: `endpoint gone (${c.status}) — pruned`,
151
+ pruned: !0
152
+ })) : a.push({
153
+ endpoint: o.endpoint,
154
+ status: "failed",
155
+ error: `${c.status}: ${c.detail}`
156
+ });
157
+ }
158
+ return a;
159
+ }, D = (e) => t(b(e));
160
+ //#endregion
161
+ export { w as assertWebPushConfigured, v as bindWebPushStore, S as buildPushPayload, h as dataStorePushSubscriptionStore, f as endpointHashOf, p as memoryPushSubscriptionStore, d as pushSubscriptionsTable, y as resetWebPushStoreForTest, b as resolveVapidPrivateKey, _ as webPushCell, C as webPushChannel, D as webPushPublicKey };
@@ -0,0 +1,117 @@
1
+ import { createCipheriv as e, createDecipheriv as t, createECDH as n, createHmac as r, createPrivateKey as i, randomBytes as a, sign as o, timingSafeEqual as s } from "node:crypto";
2
+ //#region src/webPushProtocol.ts
3
+ var c = (e) => e.toString("base64url"), l = (e) => Buffer.from(e, "base64url"), u = (e, t) => r("sha256", e).update(t).digest(), d = (e, t, n) => u(e, Buffer.concat([t, Buffer.from([1])])).subarray(0, n), f = () => {
4
+ let e = n("prime256v1");
5
+ return e.generateKeys(), c(p(e.getPrivateKey()));
6
+ }, p = (e) => {
7
+ if (e.length === 32) return e;
8
+ if (e.length > 32) throw Error(`P-256 scalar must be at most 32 bytes (got ${e.length})`);
9
+ return Buffer.concat([Buffer.alloc(32 - e.length), e]);
10
+ }, m = (e) => {
11
+ let t = l(e);
12
+ if (t.length !== 32) throw Error(`VOLTRO_VAPID_PRIVATE_KEY must be a base64url 32-byte P-256 scalar (got ${t.length} bytes)`);
13
+ return t;
14
+ }, h = (e) => {
15
+ let t = m(e), r = n("prime256v1");
16
+ return r.setPrivateKey(t), c(r.getPublicKey());
17
+ }, g = (e) => {
18
+ let t = m(e), r = n("prime256v1");
19
+ r.setPrivateKey(t);
20
+ let a = r.getPublicKey();
21
+ return i({
22
+ key: {
23
+ kty: "EC",
24
+ crv: "P-256",
25
+ d: c(t),
26
+ x: c(a.subarray(1, 33)),
27
+ y: c(a.subarray(33, 65))
28
+ },
29
+ format: "jwk"
30
+ });
31
+ }, _ = (e, t, n, r = Date.now()) => {
32
+ let i = new URL(e), a = {
33
+ aud: `${i.protocol}//${i.host}`,
34
+ exp: Math.floor(r / 1e3) + 43200,
35
+ ...n === void 0 ? {} : { sub: n }
36
+ }, s = `${c(Buffer.from(JSON.stringify({
37
+ typ: "JWT",
38
+ alg: "ES256"
39
+ })))}.${c(Buffer.from(JSON.stringify(a)))}`;
40
+ return `vapid t=${s}.${c(o("sha256", Buffer.from(s), {
41
+ key: g(t),
42
+ dsaEncoding: "ieee-p1363"
43
+ }))}, k=${h(t)}`;
44
+ }, v = Buffer.from("WebPush: info\0"), y = Buffer.from("Content-Encoding: aes128gcm\0"), b = Buffer.from("Content-Encoding: nonce\0"), x = 4096, S = (t, r, i = {}) => {
45
+ let o = l(t.p256dh), s = l(t.auth);
46
+ if (o.length !== 65 || o[0] !== 4) throw Error("push subscription p256dh must be a 65-byte uncompressed P-256 point");
47
+ if (s.length !== 16) throw Error("push subscription auth must be 16 bytes");
48
+ let c = n("prime256v1");
49
+ i.asPrivate ? c.setPrivateKey(i.asPrivate) : c.generateKeys();
50
+ let f = c.getPublicKey(), p = d(u(s, c.computeSecret(o)), Buffer.concat([
51
+ v,
52
+ o,
53
+ f
54
+ ]), 32), m = i.salt ?? a(16), h = u(m, p), g = d(h, y, 16), _ = d(h, b, 12), S = e("aes-128-gcm", g, _), C = Buffer.concat([
55
+ S.update(Buffer.concat([r, Buffer.from([2])])),
56
+ S.final(),
57
+ S.getAuthTag()
58
+ ]), w = Buffer.alloc(21);
59
+ return m.copy(w, 0), w.writeUInt32BE(x, 16), w.writeUInt8(f.length, 20), Buffer.concat([
60
+ w,
61
+ f,
62
+ C
63
+ ]);
64
+ }, C = (e, r) => {
65
+ let i = e.subarray(0, 16), a = e.readUInt8(20), o = e.subarray(21, 21 + a), s = e.subarray(21 + a), c = n("prime256v1");
66
+ c.setPrivateKey(r.uaPrivate);
67
+ let l = c.getPublicKey(), f = c.computeSecret(o), p = u(i, d(u(r.authSecret, f), Buffer.concat([
68
+ v,
69
+ l,
70
+ o
71
+ ]), 32)), m = d(p, y, 16), h = d(p, b, 12), g = t("aes-128-gcm", m, h);
72
+ g.setAuthTag(s.subarray(s.length - 16));
73
+ let _ = Buffer.concat([g.update(s.subarray(0, s.length - 16)), g.final()]), x = _.length - 1;
74
+ for (; x >= 0 && _[x] === 0;) x--;
75
+ if (_[x] !== 2) throw Error("web-push record missing the last-record delimiter");
76
+ return _.subarray(0, x);
77
+ }, w = async (e, t, n) => {
78
+ let r = S(e.keys, t), i = n.fetchImpl ?? fetch, a;
79
+ try {
80
+ a = await i(e.endpoint, {
81
+ method: "POST",
82
+ headers: {
83
+ "content-encoding": "aes128gcm",
84
+ "content-type": "application/octet-stream",
85
+ ttl: String(n.ttl ?? 86400),
86
+ urgency: "normal",
87
+ authorization: _(e.endpoint, n.privateKey, n.contact)
88
+ },
89
+ body: new Uint8Array(r)
90
+ });
91
+ } catch (e) {
92
+ return {
93
+ kind: "failed",
94
+ status: 0,
95
+ detail: e instanceof Error ? e.message : String(e)
96
+ };
97
+ }
98
+ if (a.status === 404 || a.status === 410) return {
99
+ kind: "gone",
100
+ status: a.status
101
+ };
102
+ if (a.status >= 200 && a.status < 300) return {
103
+ kind: "delivered",
104
+ status: a.status
105
+ };
106
+ let o = await a.text().catch(() => "");
107
+ return {
108
+ kind: "failed",
109
+ status: a.status,
110
+ detail: o.slice(0, 200)
111
+ };
112
+ }, T = (e, t) => {
113
+ let n = Buffer.from(e), r = Buffer.from(t);
114
+ return n.length === r.length && s(n, r);
115
+ };
116
+ //#endregion
117
+ export { T as clickTokenMatches, C as decryptWebPushPayload, S as encryptWebPushPayload, f as generateVapidPrivateKey, w as sendWebPush, _ as vapidAuthorization, h as vapidPublicKeyFor };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-notifications",
3
- "version": "0.85.0",
3
+ "version": "0.87.0",
4
4
  "description": "One unified notification surface — email, Slack/webhook, SMS, push, in-app — with per-subject preference routing, an in-app inbox, and a delivery log. NotificationService + typed routes + useInbox/useUnreadCount hooks.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -49,9 +49,9 @@
49
49
  "node": ">=24.0.0"
50
50
  },
51
51
  "dependencies": {
52
- "@voltro/client": "0.85.0",
53
- "@voltro/database": "0.85.0",
54
- "@voltro/protocol": "0.85.0"
52
+ "@voltro/client": "0.87.0",
53
+ "@voltro/database": "0.87.0",
54
+ "@voltro/protocol": "0.87.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "effect": "4.0.1",