@voltro/plugin-notifications 0.84.0 → 0.86.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/index.js CHANGED
@@ -1,1184 +1,77 @@
1
- import { BASE_NAME as e, archiveDescriptor as t, clearQuietHoursDescriptor as n, inboxDescriptor as r, inboxFeed as i, markAllReadDescriptor as a, markReadDescriptor as o, markUnreadDescriptor as s, notificationsRpcClientImports as c, preferencesDescriptor as l, setPreferenceDescriptor as u, setQuietHoursDescriptor as d, subscribeDescriptor as f, unarchiveDescriptor as p, unreadCountDescriptor as m, unsubscribeDescriptor as ee, webPushPublicKeyDescriptor as te, webPushRpcClientImports as ne, webPushStatusDescriptor as re, webPushSubscribeDescriptor as ie, webPushUnsubscribeDescriptor as ae } from "./rpc.js";
2
- import { defineNotification as h } from "./define.js";
3
- import { Context as oe, Effect as g, Layer as se, Schema as _ } from "effect";
4
- import { and as v, boolean as y, eq as b, generateId as x, id as S, integer as C, isNotNull as ce, isNull as w, json as T, lte as E, registerRetention as D, retentionTtlMsFromEnv as O, table as k, text as A, timestamp as j } from "@voltro/database";
5
- import { definePlugin as le, pluginInstanceName as ue, publishReactivity as de } from "@voltro/protocol";
6
- import { createCipheriv as M, createDecipheriv as fe, createECDH as N, createHash as pe, createHmac as me, createPrivateKey as he, randomBytes as ge, randomUUID as P, sign as F, timingSafeEqual as _e } from "node:crypto";
7
- //#region src/errors.ts
8
- var I = class extends _.TaggedError()("PushTokenRejected", {
9
- token: _.String,
10
- reason: _.String
11
- }) {}, ve = () => ({
12
- id: "console",
13
- deliver: async (e) => {
14
- process.stdout.write(`[notify:${e.category}] → ${e.to}: ${e.title} — ${e.body}\n`);
15
- }
16
- }), ye = (e) => ({
17
- id: e.id ?? "slack",
18
- deliver: async (t) => {
19
- let n = e.format ? e.format(t) : { text: `*${t.title}*\n${t.body}` }, r = await fetch(e.url, {
20
- method: "POST",
21
- headers: {
22
- "content-type": "application/json",
23
- ...e.headers ?? {}
24
- },
25
- body: JSON.stringify(n)
26
- });
27
- if (!r.ok) throw Error(`webhook ${e.id ?? "slack"} POST ${r.status}`);
28
- }
29
- }), be = (e) => ({
30
- id: "email",
31
- deliver: (t) => e({
32
- to: t.to,
33
- subject: t.title,
34
- html: `<p>${t.body}</p>`,
35
- text: t.body
36
- })
37
- }), xe = (e) => ({
38
- id: "sms",
39
- deliver: (t) => e(t.to, `${t.title}: ${t.body}`)
40
- }), Se = (e, t) => ({
41
- id: e,
42
- deliver: t
43
- }), Ce = (e) => {
44
- let t = typeof e?.image == "string" ? e.image : void 0, n = e?.actions, r = Array.isArray(n) ? n.filter((e) => typeof e == "object" && !!e && typeof e.id == "string" && typeof e.title == "string").map((e) => ({
45
- id: e.id,
46
- title: e.title
47
- })) : void 0;
48
- return {
49
- ...t === void 0 ? {} : { image: t },
50
- ...r !== void 0 && r.length > 0 ? { actions: r } : {}
51
- };
52
- }, we = (e) => {
53
- let t = async (t, n, r) => {
54
- let i = {
55
- token: n,
56
- title: t.title,
57
- body: t.body,
58
- ...r === void 0 ? {} : { badge: r },
59
- data: t.data ?? {},
60
- ...Ce(t.data)
61
- };
62
- try {
63
- return await e.transport(i), {
64
- endpoint: n,
65
- status: "sent"
66
- };
67
- } catch (t) {
68
- let r = t instanceof I, i = r ? t.reason : t instanceof Error ? t.message : String(t);
69
- if (r && e.onTokenRejected !== void 0) try {
70
- await e.onTokenRejected(n, i);
71
- } catch {}
72
- return {
73
- endpoint: n,
74
- status: "failed",
75
- error: i,
76
- ...r ? { pruned: e.onTokenRejected !== void 0 } : {}
77
- };
78
- }
79
- }, n = async (n) => {
80
- let r = await e.tokensFor(n.to), i = n.data?.badge, a = typeof i == "number" ? i : void 0, o = [];
81
- for (let e of r) o.push(await t(n, e, a));
82
- return o;
83
- };
84
- return {
85
- id: e.id ?? "push",
86
- deliver: async (e) => {
87
- let t = await n(e), r = t.filter((e) => e.status === "failed");
88
- if (t.length > 0 && r.length === t.length) throw new I({
89
- token: t[0].endpoint,
90
- reason: r[0].error ?? "all tokens rejected"
91
- });
92
- },
93
- deliverDetailed: n
94
- };
95
- }, Te = (e, t, n, r, i) => {
96
- let a = n ? e.filter((e) => n.includes(e)) : [...e], o = new Set(r.filter((e) => e.subjectId === i && e.category === t && e.enabled === !1).map((e) => e.channel));
97
- return a.filter((e) => !o.has(e));
98
- }, Ee = () => {
99
- 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}`;
100
- return {
101
- addInbox: async (t) => {
102
- e.unshift({
103
- ...t,
104
- readAt: null,
105
- archivedAt: null,
106
- data: t.data ?? {},
107
- tenantId: t.tenantId ?? null
108
- });
109
- },
110
- listInbox: async (t, n) => {
111
- let r = e.filter((e) => e.subjectId === t);
112
- return n?.unreadOnly && (r = r.filter((e) => e.readAt === null)), n?.limit === void 0 ? r : r.slice(0, n.limit);
113
- },
114
- markRead: async (t, n) => {
115
- let r = e.findIndex((e) => e.id === t && e.subjectId === n);
116
- return r !== -1 && (e[r] = {
117
- ...e[r],
118
- readAt: (/* @__PURE__ */ new Date(0)).toISOString()
119
- }, !0);
120
- },
121
- markUnread: async (t, n) => {
122
- let r = e.findIndex((e) => e.id === t && e.subjectId === n);
123
- return r !== -1 && (e[r] = {
124
- ...e[r],
125
- readAt: null
126
- }, !0);
127
- },
128
- markAllRead: async (t) => {
129
- let n = 0;
130
- for (let r = 0; r < e.length; r++) {
131
- let i = e[r];
132
- i.subjectId === t && i.readAt === null && (e[r] = {
133
- ...i,
134
- readAt: (/* @__PURE__ */ new Date(0)).toISOString()
135
- }, n++);
136
- }
137
- return n;
138
- },
139
- setArchived: async (t, n, r) => {
140
- let i = e.findIndex((e) => e.id === t && e.subjectId === n);
141
- return i !== -1 && (e[i] = {
142
- ...e[i],
143
- archivedAt: r ? (/* @__PURE__ */ new Date(0)).toISOString() : null
144
- }, !0);
145
- },
146
- unreadCount: async (t) => e.filter((e) => e.subjectId === t && e.readAt === null).length,
147
- getPreferences: async (e) => [...t.values()].filter((t) => t.subjectId === e),
148
- setPreference: async (e) => {
149
- t.set(o(e), e);
150
- },
151
- recordDelivery: async (e) => {
152
- n.unshift(e);
153
- },
154
- listDeliveries: async (e) => e?.limit === void 0 ? [...n] : n.slice(0, e.limit),
155
- markClicked: async (e) => {
156
- let t = n.findIndex((t) => t.clickToken === e);
157
- return t !== -1 && (n[t] = {
158
- ...n[t],
159
- clickedAt: (/* @__PURE__ */ new Date()).toISOString()
160
- }, !0);
161
- },
162
- subscribeTopic: async (e) => {
163
- r.set(s(e.topic, e.subjectId), e);
164
- },
165
- unsubscribeTopic: async (e, t) => {
166
- r.delete(s(e, t));
167
- },
168
- listTopicSubscribers: async (e) => [...r.values()].filter((t) => t.topic === e),
169
- getQuietHours: async (e) => i.get(e) ?? null,
170
- setQuietHours: async (e) => {
171
- i.set(e.subjectId, e);
172
- },
173
- clearQuietHours: async (e) => {
174
- i.delete(e);
175
- },
176
- enqueueHeld: async (e) => {
177
- a.push(e);
178
- },
179
- pendingDigest: async (e) => a.filter((t) => t.kind === "digest" && t.subjectId === e),
180
- dueHeld: async (e) => a.filter((t) => t.flushAt <= e).sort((e, t) => e.flushAt < t.flushAt ? -1 : 1),
181
- clearHeld: async (e) => {
182
- let t = new Set(e);
183
- for (let e = a.length - 1; e >= 0; e--) t.has(a[e].id) && a.splice(e, 1);
184
- }
185
- };
186
- }, L = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "", De = (e) => {
187
- let t = async (t, n, r, i) => await e.query({
188
- table: t,
189
- predicate: n,
190
- order: r,
191
- take: i,
192
- skip: void 0,
193
- projection: void 0
194
- }), n = (e) => ({
195
- id: String(e.id),
196
- subjectId: String(e.subjectId),
197
- category: String(e.category),
198
- title: String(e.title),
199
- body: String(e.body),
200
- data: e.data ?? {},
201
- readAt: e.readAt == null ? null : L(e.readAt),
202
- archivedAt: e.archivedAt == null ? null : L(e.archivedAt),
203
- createdAt: L(e.createdAt),
204
- tenantId: e.tenantId ?? null
205
- }), r = (e) => ({
206
- subjectId: String(e.subjectId),
207
- category: String(e.category),
208
- channel: String(e.channel),
209
- enabled: !!e.enabled
210
- }), i = (e) => ({
211
- id: String(e.id),
212
- to: String(e.recipient),
213
- category: String(e.category),
214
- channel: String(e.channel),
215
- status: e.status,
216
- at: L(e.at),
217
- ...e.error == null ? {} : { error: String(e.error) },
218
- ...e.endpoint == null ? {} : { endpoint: String(e.endpoint) },
219
- ...e.clickToken == null ? {} : { clickToken: String(e.clickToken) },
220
- ...e.clickedAt == null ? {} : { clickedAt: L(e.clickedAt) }
221
- }), a = (e) => ({
222
- topic: String(e.topic),
223
- subjectId: String(e.subjectId),
224
- tenantId: e.tenantId ?? null
225
- }), o = (e) => ({
226
- subjectId: String(e.subjectId),
227
- startMinute: Number(e.startMinute),
228
- endMinute: Number(e.endMinute),
229
- tz: String(e.tz ?? "UTC"),
230
- policy: e.policy === "drop" ? "drop" : "hold"
231
- }), s = (e) => ({
232
- id: String(e.id),
233
- kind: e.kind === "quiet" ? "quiet" : "digest",
234
- subjectId: String(e.subjectId),
235
- send: e.send ?? {},
236
- flushAt: L(e.flushAt)
237
- });
238
- return {
239
- addInbox: async (t) => {
240
- await e.insert("_voltro_notification_inbox", {
241
- id: t.id,
242
- subjectId: t.subjectId,
243
- category: t.category,
244
- title: t.title,
245
- body: t.body,
246
- data: t.data ?? {},
247
- readAt: null,
248
- archivedAt: null,
249
- createdAt: t.createdAt,
250
- tenantId: t.tenantId ?? null
251
- });
252
- },
253
- listInbox: async (e, r) => {
254
- let i = r?.unreadOnly ? v(b("subjectId", e), w("readAt")) : b("subjectId", e);
255
- return (await t("_voltro_notification_inbox", i, [{
256
- column: "createdAt",
257
- direction: "desc"
258
- }], r?.limit)).map(n);
259
- },
260
- markRead: async (n, r) => (await t("_voltro_notification_inbox", v(b("id", n), b("subjectId", r)), [], 1))[0] ? (await e.update("_voltro_notification_inbox", n, { readAt: (/* @__PURE__ */ new Date()).toISOString() }), !0) : !1,
261
- markUnread: async (n, r) => (await t("_voltro_notification_inbox", v(b("id", n), b("subjectId", r)), [], 1))[0] ? (await e.update("_voltro_notification_inbox", n, { readAt: null }), !0) : !1,
262
- markAllRead: async (n) => {
263
- let r = await t("_voltro_notification_inbox", v(b("subjectId", n), w("readAt")), [], void 0), i = (/* @__PURE__ */ new Date()).toISOString();
264
- for (let t of r) await e.update("_voltro_notification_inbox", String(t.id), { readAt: i });
265
- return r.length;
266
- },
267
- setArchived: async (n, r, i) => (await t("_voltro_notification_inbox", v(b("id", n), b("subjectId", r)), [], 1))[0] ? (await e.update("_voltro_notification_inbox", n, { archivedAt: i ? (/* @__PURE__ */ new Date()).toISOString() : null }), !0) : !1,
268
- unreadCount: async (e) => (await t("_voltro_notification_inbox", v(b("subjectId", e), w("readAt")), [], void 0)).length,
269
- getPreferences: async (e) => (await t("_voltro_notification_preferences", b("subjectId", e), [], void 0)).map(r),
270
- setPreference: async (t) => {
271
- let n = x({
272
- kind: "typeid",
273
- prefix: "notifpref"
274
- }, "_voltro_notification_preferences");
275
- await e.upsert("_voltro_notification_preferences", {
276
- id: n,
277
- subjectId: t.subjectId,
278
- category: t.category,
279
- channel: t.channel,
280
- enabled: t.enabled
281
- }, {
282
- conflictColumns: [
283
- "subjectId",
284
- "category",
285
- "channel"
286
- ],
287
- update: ["enabled"]
288
- });
289
- },
290
- recordDelivery: async (t) => {
291
- await e.insert("_voltro_notification_deliveries", {
292
- id: t.id,
293
- recipient: t.to,
294
- category: t.category,
295
- channel: t.channel,
296
- status: t.status,
297
- error: t.error ?? null,
298
- at: t.at,
299
- endpoint: t.endpoint ?? null,
300
- clickToken: t.clickToken ?? null,
301
- clickedAt: t.clickedAt ?? null
302
- });
303
- },
304
- markClicked: async (n) => {
305
- let r = await t("_voltro_notification_deliveries", b("clickToken", n), [], 1);
306
- return r[0] ? (await e.update("_voltro_notification_deliveries", String(r[0].id), { clickedAt: (/* @__PURE__ */ new Date()).toISOString() }), !0) : !1;
307
- },
308
- listDeliveries: async (e) => (await t("_voltro_notification_deliveries", void 0, [{
309
- column: "at",
310
- direction: "desc"
311
- }], e?.limit)).map(i),
312
- subscribeTopic: async (t) => {
313
- let n = x({
314
- kind: "typeid",
315
- prefix: "notiftsub"
316
- }, "_voltro_notification_topic_subscriptions");
317
- await e.upsert("_voltro_notification_topic_subscriptions", {
318
- id: n,
319
- topic: t.topic,
320
- subjectId: t.subjectId,
321
- tenantId: t.tenantId ?? null
322
- }, {
323
- conflictColumns: ["topic", "subjectId"],
324
- update: ["tenantId"]
325
- });
326
- },
327
- unsubscribeTopic: async (n, r) => {
328
- let i = await t("_voltro_notification_topic_subscriptions", v(b("topic", n), b("subjectId", r)), [], 1);
329
- i[0] && await e.delete("_voltro_notification_topic_subscriptions", String(i[0].id));
330
- },
331
- listTopicSubscribers: async (e) => (await t("_voltro_notification_topic_subscriptions", b("topic", e), [], void 0)).map(a),
332
- getQuietHours: async (e) => {
333
- let n = await t("_voltro_notification_quiet_hours", b("subjectId", e), [], 1);
334
- return n[0] ? o(n[0]) : null;
335
- },
336
- setQuietHours: async (t) => {
337
- let n = x({
338
- kind: "typeid",
339
- prefix: "notifqh"
340
- }, "_voltro_notification_quiet_hours");
341
- await e.upsert("_voltro_notification_quiet_hours", {
342
- id: n,
343
- subjectId: t.subjectId,
344
- startMinute: t.startMinute,
345
- endMinute: t.endMinute,
346
- tz: t.tz ?? "UTC",
347
- policy: t.policy ?? "hold"
348
- }, {
349
- conflictColumns: ["subjectId"],
350
- update: [
351
- "startMinute",
352
- "endMinute",
353
- "tz",
354
- "policy"
355
- ]
356
- });
357
- },
358
- clearQuietHours: async (n) => {
359
- let r = await t("_voltro_notification_quiet_hours", b("subjectId", n), [], 1);
360
- r[0] && await e.delete("_voltro_notification_quiet_hours", String(r[0].id));
361
- },
362
- enqueueHeld: async (t) => {
363
- await e.insert("_voltro_notification_held", {
364
- id: t.id,
365
- kind: t.kind,
366
- subjectId: t.subjectId,
367
- send: t.send,
368
- flushAt: t.flushAt
369
- });
370
- },
371
- pendingDigest: async (e) => (await t("_voltro_notification_held", v(b("kind", "digest"), b("subjectId", e)), [{
372
- column: "flushAt",
373
- direction: "asc"
374
- }], void 0)).map(s),
375
- dueHeld: async (e) => (await t("_voltro_notification_held", E("flushAt", e), [{
376
- column: "flushAt",
377
- direction: "asc"
378
- }], void 0)).map(s),
379
- clearHeld: async (t) => {
380
- for (let n of t) await e.delete("_voltro_notification_held", n);
381
- }
382
- };
383
- }, Oe = (e) => ({
384
- id: "inApp",
385
- deliver: async (t) => {
386
- await e.addInbox({
387
- id: P(),
388
- subjectId: t.to,
389
- category: t.category,
390
- title: t.title,
391
- body: t.body,
392
- data: t.data ?? {},
393
- tenantId: t.tenantId ?? null,
394
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
395
- });
396
- }
397
- }), R = (e, t) => {
398
- try {
399
- let n = new Intl.DateTimeFormat("en-US", {
400
- timeZone: t,
401
- hour: "2-digit",
402
- minute: "2-digit",
403
- hour12: !1
404
- }).formatToParts(e), r = Number(n.find((e) => e.type === "hour")?.value ?? "0") % 24, i = Number(n.find((e) => e.type === "minute")?.value ?? "0");
405
- return r * 60 + i;
406
- } catch {
407
- return e.getUTCHours() * 60 + e.getUTCMinutes();
408
- }
409
- }, ke = (e, t) => {
410
- if (e.startMinute === e.endMinute) return !1;
411
- let n = R(t, e.tz ?? "UTC");
412
- return e.startMinute < e.endMinute ? n >= e.startMinute && n < e.endMinute : n >= e.startMinute || n < e.endMinute;
413
- }, Ae = (e, t) => {
414
- let n = R(t, e.tz ?? "UTC"), r = e.endMinute - n;
415
- return r <= 0 && (r += 1440), new Date(t.getTime() + r * 6e4);
416
- }, je = (e) => {
417
- let t = new Map(e.channels.map((e) => [e.id, e])), n = e.channels.map((e) => e.id), r = e.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()), i = e.digestWindowMs ?? 0, a = async (i) => {
418
- let a = await e.store.getPreferences(i.to), o = Te(n, i.category, i.channels, a, i.to), s = n.filter((e) => !o.includes(e)), c = [], l = [], u = {
419
- to: i.to,
420
- category: i.category,
421
- title: i.title,
422
- body: i.body,
423
- ...i.data ? { data: i.data } : {},
424
- tenantId: i.tenantId ?? null
425
- };
426
- for (let n of o) {
427
- let a = t.get(n);
428
- if (!a) continue;
429
- if (a.deliverDetailed !== void 0) {
430
- try {
431
- let t = await a.deliverDetailed(u);
432
- for (let a of t) await e.store.recordDelivery({
433
- id: P(),
434
- to: i.to,
435
- category: i.category,
436
- channel: n,
437
- at: r(),
438
- status: a.status,
439
- endpoint: a.endpoint,
440
- ...a.error === void 0 ? {} : { error: a.error },
441
- ...a.clickToken === void 0 ? {} : { clickToken: a.clickToken }
442
- });
443
- let o = t.filter((e) => e.status === "failed");
444
- t.length === 0 || o.length < t.length ? c.push(n) : l.push({
445
- channel: n,
446
- error: o[0]?.error ?? "all endpoints failed"
447
- });
448
- } catch (t) {
449
- let a = t instanceof Error ? t.message : String(t);
450
- l.push({
451
- channel: n,
452
- error: a
453
- }), await e.store.recordDelivery({
454
- id: P(),
455
- to: i.to,
456
- category: i.category,
457
- channel: n,
458
- at: r(),
459
- status: "failed",
460
- error: a
461
- });
462
- }
463
- continue;
464
- }
465
- let o = {
466
- id: P(),
467
- to: i.to,
468
- category: i.category,
469
- channel: n,
470
- at: r()
471
- };
472
- try {
473
- await a.deliver(u), c.push(n), await e.store.recordDelivery({
474
- ...o,
475
- status: "sent"
476
- });
477
- } catch (t) {
478
- let r = t instanceof Error ? t.message : String(t);
479
- l.push({
480
- channel: n,
481
- error: r
482
- }), await e.store.recordDelivery({
483
- ...o,
484
- status: "failed",
485
- error: r
486
- });
487
- }
488
- }
489
- return {
490
- delivered: c,
491
- failed: l,
492
- skipped: s
493
- };
494
- }, o = async (t) => {
495
- let n = await e.store.getQuietHours(t.to), o = new Date(r());
496
- if (n && ke(n, o)) return (n.policy ?? "hold") === "drop" ? {
497
- delivered: [],
498
- failed: [],
499
- skipped: [],
500
- held: "quiet-drop"
501
- } : (await e.store.enqueueHeld({
502
- id: P(),
503
- kind: "quiet",
504
- subjectId: t.to,
505
- send: t,
506
- flushAt: Ae(n, o).toISOString()
507
- }), {
508
- delivered: [],
509
- failed: [],
510
- skipped: [],
511
- held: "quiet-held"
512
- });
513
- if (i > 0 && !t.channels) {
514
- let n = (await e.store.pendingDigest(t.to))[0]?.flushAt ?? new Date(o.getTime() + i).toISOString();
515
- return await e.store.enqueueHeld({
516
- id: P(),
517
- kind: "digest",
518
- subjectId: t.to,
519
- send: t,
520
- flushAt: n
521
- }), {
522
- delivered: [],
523
- failed: [],
524
- skipped: [],
525
- held: "digest"
526
- };
527
- }
528
- return a(t);
529
- };
530
- return {
531
- send: o,
532
- inbox: (t, n) => e.store.listInbox(t, n),
533
- unreadCount: (t) => e.store.unreadCount(t),
534
- markRead: (t, n) => e.store.markRead(t, n),
535
- markUnread: (t, n) => e.store.markUnread(t, n),
536
- markAllRead: (t) => e.store.markAllRead(t),
537
- setArchived: (t, n, r) => e.store.setArchived(t, n, r),
538
- getPreferences: (t) => e.store.getPreferences(t),
539
- setPreference: (t) => e.store.setPreference(t),
540
- subscribe: (t, n, r) => e.store.subscribeTopic({
541
- topic: t,
542
- subjectId: n,
543
- tenantId: r ?? null
544
- }),
545
- unsubscribe: (t, n) => e.store.unsubscribeTopic(t, n),
546
- broadcast: async (t, n) => {
547
- let r = await e.store.listTopicSubscribers(t), i = [];
548
- for (let e of r) i.push(await o({
549
- ...n,
550
- to: e.subjectId,
551
- tenantId: n.tenantId ?? e.tenantId ?? null
552
- }));
553
- return {
554
- recipients: r.length,
555
- results: i
556
- };
557
- },
558
- setQuietHours: (t) => e.store.setQuietHours(t),
559
- clearQuietHours: (t) => e.store.clearQuietHours(t),
560
- getQuietHours: (t) => e.store.getQuietHours(t),
561
- flushDue: async (t = new Date(r())) => {
562
- let n = await e.store.dueHeld(t.toISOString());
563
- if (n.length === 0) return 0;
564
- for (let e of n.filter((e) => e.kind === "quiet")) await a(e.send);
565
- let i = /* @__PURE__ */ new Map();
566
- for (let e of n.filter((e) => e.kind === "digest")) {
567
- let t = i.get(e.subjectId) ?? [];
568
- t.push(e), i.set(e.subjectId, t);
569
- }
570
- for (let [e, t] of i) await a(Me(e, t));
571
- return await e.store.clearHeld(n.map((e) => e.id)), n.length;
572
- }
573
- };
574
- }, Me = (e, t) => {
575
- if (t.length === 1) return t[0].send;
576
- let n = t.map((e) => ({
577
- category: e.send.category,
578
- title: e.send.title,
579
- body: e.send.body
580
- }));
581
- return {
582
- to: e,
583
- category: "digest",
584
- title: `${t.length} new notifications`,
585
- body: n.map((e) => `• ${e.title}`).join("\n"),
586
- data: { items: n },
587
- tenantId: t[0].send.tenantId ?? null
588
- };
589
- }, z = (e) => e.toString("base64url"), B = (e) => Buffer.from(e, "base64url"), V = (e, t) => me("sha256", e).update(t).digest(), H = (e, t, n) => V(e, Buffer.concat([t, Buffer.from([1])])).subarray(0, n), Ne = () => {
590
- let e = N("prime256v1");
591
- return e.generateKeys(), z(Pe(e.getPrivateKey()));
592
- }, Pe = (e) => {
593
- if (e.length === 32) return e;
594
- if (e.length > 32) throw Error(`P-256 scalar must be at most 32 bytes (got ${e.length})`);
595
- return Buffer.concat([Buffer.alloc(32 - e.length), e]);
596
- }, Fe = (e) => {
597
- let t = B(e);
598
- if (t.length !== 32) throw Error(`VOLTRO_VAPID_PRIVATE_KEY must be a base64url 32-byte P-256 scalar (got ${t.length} bytes)`);
599
- return t;
600
- }, U = (e) => {
601
- let t = Fe(e), n = N("prime256v1");
602
- return n.setPrivateKey(t), z(n.getPublicKey());
603
- }, Ie = (e) => {
604
- let t = Fe(e), n = N("prime256v1");
605
- n.setPrivateKey(t);
606
- let r = n.getPublicKey();
607
- return he({
608
- key: {
609
- kty: "EC",
610
- crv: "P-256",
611
- d: z(t),
612
- x: z(r.subarray(1, 33)),
613
- y: z(r.subarray(33, 65))
614
- },
615
- format: "jwk"
616
- });
617
- }, Le = (e, t, n, r = Date.now()) => {
618
- let i = new URL(e), a = {
619
- aud: `${i.protocol}//${i.host}`,
620
- exp: Math.floor(r / 1e3) + 43200,
621
- ...n === void 0 ? {} : { sub: n }
622
- }, o = `${z(Buffer.from(JSON.stringify({
623
- typ: "JWT",
624
- alg: "ES256"
625
- })))}.${z(Buffer.from(JSON.stringify(a)))}`;
626
- return `vapid t=${o}.${z(F("sha256", Buffer.from(o), {
627
- key: Ie(t),
628
- dsaEncoding: "ieee-p1363"
629
- }))}, k=${U(t)}`;
630
- }, Re = Buffer.from("WebPush: info\0"), ze = Buffer.from("Content-Encoding: aes128gcm\0"), Be = Buffer.from("Content-Encoding: nonce\0"), Ve = 4096, He = (e, t, n = {}) => {
631
- let r = B(e.p256dh), i = B(e.auth);
632
- if (r.length !== 65 || r[0] !== 4) throw Error("push subscription p256dh must be a 65-byte uncompressed P-256 point");
633
- if (i.length !== 16) throw Error("push subscription auth must be 16 bytes");
634
- let a = N("prime256v1");
635
- n.asPrivate ? a.setPrivateKey(n.asPrivate) : a.generateKeys();
636
- let o = a.getPublicKey(), s = H(V(i, a.computeSecret(r)), Buffer.concat([
637
- Re,
638
- r,
639
- o
640
- ]), 32), c = n.salt ?? ge(16), l = V(c, s), u = H(l, ze, 16), d = H(l, Be, 12), f = M("aes-128-gcm", u, d), p = Buffer.concat([
641
- f.update(Buffer.concat([t, Buffer.from([2])])),
642
- f.final(),
643
- f.getAuthTag()
644
- ]), m = Buffer.alloc(21);
645
- return c.copy(m, 0), m.writeUInt32BE(Ve, 16), m.writeUInt8(o.length, 20), Buffer.concat([
646
- m,
647
- o,
648
- p
649
- ]);
650
- }, Ue = (e, t) => {
651
- let n = e.subarray(0, 16), r = e.readUInt8(20), i = e.subarray(21, 21 + r), a = e.subarray(21 + r), o = N("prime256v1");
652
- o.setPrivateKey(t.uaPrivate);
653
- let s = o.getPublicKey(), c = o.computeSecret(i), l = V(n, H(V(t.authSecret, c), Buffer.concat([
654
- Re,
655
- s,
656
- i
657
- ]), 32)), u = H(l, ze, 16), d = H(l, Be, 12), f = fe("aes-128-gcm", u, d);
658
- f.setAuthTag(a.subarray(a.length - 16));
659
- let p = Buffer.concat([f.update(a.subarray(0, a.length - 16)), f.final()]), m = p.length - 1;
660
- for (; m >= 0 && p[m] === 0;) m--;
661
- if (p[m] !== 2) throw Error("web-push record missing the last-record delimiter");
662
- return p.subarray(0, m);
663
- }, We = async (e, t, n) => {
664
- let r = He(e.keys, t), i = n.fetchImpl ?? fetch, a;
665
- try {
666
- a = await i(e.endpoint, {
667
- method: "POST",
668
- headers: {
669
- "content-encoding": "aes128gcm",
670
- "content-type": "application/octet-stream",
671
- ttl: String(n.ttl ?? 86400),
672
- urgency: "normal",
673
- authorization: Le(e.endpoint, n.privateKey, n.contact)
674
- },
675
- body: new Uint8Array(r)
676
- });
677
- } catch (e) {
678
- return {
679
- kind: "failed",
680
- status: 0,
681
- detail: e instanceof Error ? e.message : String(e)
682
- };
683
- }
684
- if (a.status === 404 || a.status === 410) return {
685
- kind: "gone",
686
- status: a.status
687
- };
688
- if (a.status >= 200 && a.status < 300) return {
689
- kind: "delivered",
690
- status: a.status
691
- };
692
- let o = await a.text().catch(() => "");
693
- return {
694
- kind: "failed",
695
- status: a.status,
696
- detail: o.slice(0, 200)
697
- };
698
- }, Ge = (e, t) => {
699
- let n = Buffer.from(e), r = Buffer.from(t);
700
- return n.length === r.length && _e(n, r);
701
- }, Ke = k("_voltro_notification_push_subscriptions", {
702
- id: S({ prefix: "notifpush" }),
703
- subjectId: A(),
704
- endpointHash: A(),
705
- endpoint: A(),
706
- p256dh: A(),
707
- auth: A(),
708
- ua: A().nullable(),
709
- createdAt: j().default("now"),
710
- tenantId: A().nullable()
711
- }).unique("byPushEndpointHash", ["endpointHash"]).index("byPushSubject", ["subjectId"]), W = (e) => pe("sha256").update(e).digest("hex"), G = () => {
712
- let e = /* @__PURE__ */ new Map();
713
- return {
714
- upsert: async (t) => {
715
- e.set(t.endpointHash, t);
716
- },
717
- listForSubject: async (t) => [...e.values()].filter((e) => e.subjectId === t),
718
- removeForSubject: async (t, n) => {
719
- let r = e.get(t);
720
- return !r || r.subjectId !== n ? !1 : (e.delete(t), !0);
721
- },
722
- removeByHash: async (t) => {
723
- e.delete(t);
724
- },
725
- countForSubject: async (t) => [...e.values()].filter((e) => e.subjectId === t).length
726
- };
727
- }, K = "_voltro_notification_push_subscriptions", qe = (e) => {
728
- let t = async (t, n) => e.query({
729
- table: K,
730
- predicate: t,
731
- order: [],
732
- take: n,
733
- skip: void 0,
734
- projection: void 0
735
- }), n = (e) => ({
736
- subjectId: String(e.subjectId),
737
- endpoint: String(e.endpoint),
738
- endpointHash: String(e.endpointHash),
739
- p256dh: String(e.p256dh),
740
- auth: String(e.auth),
741
- ua: e.ua == null ? null : String(e.ua),
742
- tenantId: e.tenantId == null ? null : String(e.tenantId)
743
- });
744
- return {
745
- upsert: async (t) => {
746
- let n = x({
747
- kind: "typeid",
748
- prefix: "notifpush"
749
- }, K);
750
- await e.upsert(K, {
751
- id: n,
752
- subjectId: t.subjectId,
753
- endpointHash: t.endpointHash,
754
- endpoint: t.endpoint,
755
- p256dh: t.p256dh,
756
- auth: t.auth,
757
- ua: t.ua,
758
- tenantId: t.tenantId
759
- }, {
760
- conflictColumns: ["endpointHash"],
761
- update: [
762
- "subjectId",
763
- "p256dh",
764
- "auth",
765
- "ua",
766
- "tenantId"
767
- ]
768
- });
769
- },
770
- listForSubject: async (e) => (await t(b("subjectId", e))).map(n),
771
- removeForSubject: async (n, r) => {
772
- let i = await t(v(b("endpointHash", n), b("subjectId", r)), 1);
773
- return i[0] ? (await e.delete(K, String(i[0].id)), !0) : !1;
774
- },
775
- removeByHash: async (n) => {
776
- let r = await t(b("endpointHash", n), 1);
777
- r[0] && await e.delete(K, String(r[0].id));
778
- },
779
- countForSubject: async (e) => (await t(b("subjectId", e))).length
780
- };
781
- }, Je = Symbol.for("@voltro/plugin-notifications:web-push"), q = () => {
782
- let e = globalThis;
783
- return e[Je] ??= {
784
- store: G(),
785
- bound: !1
786
- };
787
- }, Ye = (e) => {
788
- let t = q();
789
- t.store = qe(e), t.bound = !0;
790
- }, Xe = () => {
791
- let e = q();
792
- e.store = G(), e.bound = !1;
793
- }, J = (e) => {
794
- let t = e ?? process.env.VOLTRO_VAPID_PRIVATE_KEY;
795
- 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.");
796
- return t;
797
- }, Y = 3800, Ze = (e, t) => {
798
- let n = typeof e.data?.url == "string" ? e.data.url : void 0, r = {
799
- title: e.title,
800
- body: e.body,
801
- category: e.category,
802
- clickToken: t,
803
- ...n === void 0 ? {} : { url: n },
804
- ...e.data === void 0 ? {} : { data: e.data }
805
- }, i = Buffer.from(JSON.stringify(r));
806
- if (i.length <= Y) return i;
807
- let { data: a, ...o } = r;
808
- if (i = Buffer.from(JSON.stringify(o)), i.length <= Y) return i;
809
- let s = i.length - Buffer.byteLength(o.body), c = Math.max(0, Y - s - Buffer.byteLength("…")), l = Buffer.from(o.body).subarray(0, c).toString("utf8").replace(/�+$/u, "");
810
- return Buffer.from(JSON.stringify({
811
- ...o,
812
- body: `${l}…`
813
- }));
814
- }, Qe = (e = {}) => (q().channelOptions = e, et(e)), $e = () => {
815
- let e = q().channelOptions;
816
- U(J(e?.privateKey));
817
- }, et = (e) => ({
818
- id: e.id ?? "webPush",
819
- deliver: async (t) => {
820
- let n = await tt(t, e), r = n.filter((e) => e.status === "failed");
821
- if (n.length > 0 && r.length === n.length) throw Error(`web push failed for all ${r.length} endpoint(s): ${r[0].error ?? "unknown"}`);
822
- },
823
- deliverDetailed: (t) => tt(t, e)
824
- }), tt = async (e, t) => {
825
- let n = J(t.privateKey), r = await q().store.listForSubject(e.to), i = [];
826
- for (let a of r) {
827
- let r = ge(16).toString("base64url"), o = {
828
- privateKey: n,
829
- ...t.contact === void 0 ? {} : { contact: t.contact },
830
- ...t.ttl === void 0 ? {} : { ttl: t.ttl },
831
- ...t.fetchImpl === void 0 ? {} : { fetchImpl: t.fetchImpl }
832
- }, s = await We({
833
- endpoint: a.endpoint,
834
- keys: {
835
- p256dh: a.p256dh,
836
- auth: a.auth
837
- }
838
- }, Ze(e, r), o);
839
- s.kind === "delivered" ? i.push({
840
- endpoint: a.endpoint,
841
- status: "sent",
842
- clickToken: r
843
- }) : s.kind === "gone" ? (await q().store.removeByHash(a.endpointHash), i.push({
844
- endpoint: a.endpoint,
845
- status: "failed",
846
- error: `endpoint gone (${s.status}) — pruned`,
847
- pruned: !0
848
- })) : i.push({
849
- endpoint: a.endpoint,
850
- status: "failed",
851
- error: `${s.status}: ${s.detail}`
852
- });
853
- }
854
- return i;
855
- }, nt = (e) => U(J(e)), rt = (e) => {
856
- if (typeof e == "object" && e) {
857
- let t = e;
858
- if (typeof t.userId == "string") return [t.userId];
859
- if (Array.isArray(t.userIds)) return t.userIds.filter((e) => typeof e == "string");
860
- }
861
- throw Error("sendNotification: pass `recipients` — the audience has neither `userId` nor `userIds`");
862
- }, it = async (e, t, n) => {
863
- let r = _.decodeUnknownSync(t.audience)(n.audience), i = _.decodeUnknownSync(t.payload)(n.payload), a = n.recipients ? await n.recipients(r) : rt(r), o = [];
864
- for (let r of a) {
865
- let a = (n.localeFor ? await n.localeFor(r) : null) ?? n.defaultLocale ?? "en", s = await e.send(at(t, i, r, a, n.tenantId));
866
- o.push({
867
- recipientId: r,
868
- locale: a,
869
- result: s
870
- });
871
- }
872
- return o;
873
- }, at = (e, t, n, r, i) => {
874
- let a = e.render(t, {
875
- locale: r,
876
- recipientId: n
877
- });
878
- return {
879
- to: n,
880
- category: e.category,
881
- title: a.title,
882
- body: a.body,
883
- data: {
884
- ...a.data,
885
- notification: e.name,
886
- ...a.image === void 0 ? {} : { image: a.image },
887
- ...a.actions === void 0 ? {} : { actions: a.actions }
888
- },
889
- ...e.channels === void 0 ? {} : { channels: e.channels },
890
- ...i === void 0 ? {} : { tenantId: i }
891
- };
892
- }, X = "voltro.notification.send", ot = async (e, t, n) => {
893
- let r = _.decodeUnknownSync(t.audience)(n.audience), i = _.is(t.payload)(n.payload) ? _.encodeUnknownSync(t.payload)(n.payload) : (_.decodeUnknownSync(t.payload)(n.payload), n.payload), a = n.recipients ? await n.recipients(r) : rt(r), o = [];
894
- for (let r of a) {
895
- let a = (n.localeFor ? await n.localeFor(r) : null) ?? n.defaultLocale ?? "en", s = {
896
- notification: t.name,
897
- recipientId: r,
898
- locale: a,
899
- payload: i,
900
- tenantId: n.tenantId ?? null
901
- }, c = await e.enqueue(X, { ...s }, {
902
- ...n.idempotencyKey === void 0 ? {} : { idempotencyKey: `${t.name}:${r}:${n.idempotencyKey}` },
903
- ...n.maxAttempts === void 0 ? {} : { maxAttempts: n.maxAttempts },
904
- ...n.delayMs === void 0 ? {} : { delayMs: n.delayMs }
905
- });
906
- o.push({
907
- recipientId: r,
908
- locale: a,
909
- outboxId: c
910
- });
911
- }
912
- return o;
913
- }, st = (e) => typeof e.notification == "string" && typeof e.recipientId == "string" && typeof e.locale == "string", ct = (e) => {
914
- let t = new Map(e.notifications.map((e) => [e.name, e]));
915
- return {
916
- effect: X,
917
- maxAttempts: e.maxAttempts ?? 5,
918
- handler: async ({ payload: n }) => {
919
- if (!st(n)) throw Error(`${X}: row payload needs notification, recipientId and locale — got keys ${Object.keys(n).join(", ") || "(none)"}`);
920
- let r = t.get(n.notification);
921
- if (r === void 0) throw Error(`${X}: no descriptor named ${JSON.stringify(n.notification)} in this handler — list it in notificationOutboxHandler({ notifications })`);
922
- let i = e.service();
923
- if (i === void 0) throw Error(`${X}: the notification service is not available in this process`);
924
- let a = _.decodeUnknownSync(r.payload)(n.payload);
925
- return i.send(at(r, a, n.recipientId, n.locale, n.tenantId));
926
- }
927
- };
928
- }, lt = {
929
- production: "https://api.push.apple.com",
930
- sandbox: "https://api.sandbox.push.apple.com"
931
- }, ut = 3e6, dt = /* @__PURE__ */ new Set([
932
- "BadDeviceToken",
933
- "Unregistered",
934
- "DeviceTokenNotForTopic",
935
- "ExpiredToken"
936
- ]), Z = (e) => Buffer.from(e).toString("base64url"), ft = (e, t) => {
937
- let n = Z(JSON.stringify({
938
- alg: "ES256",
939
- kid: e.keyId
940
- })), r = Z(JSON.stringify({
941
- iss: e.teamId,
942
- iat: Math.floor(t / 1e3)
943
- }));
944
- return `${n}.${r}.${Z(F("sha256", Buffer.from(`${n}.${r}`), {
945
- key: e.privateKey,
946
- dsaEncoding: "ieee-p1363"
947
- }))}`;
948
- }, pt = (e) => ({
949
- aps: {
950
- alert: {
951
- title: e.title,
952
- body: e.body
953
- },
954
- ...e.badge === void 0 ? {} : { badge: e.badge },
955
- sound: "default",
956
- ...e.image === void 0 ? {} : { "mutable-content": 1 },
957
- ...e.actions !== void 0 && typeof e.data.notification == "string" ? { category: e.data.notification } : {}
958
- },
959
- ...e.data
960
- }), mt = async ({ url: e, headers: t, body: n }) => {
961
- let r = await import("node:http2"), i = new URL(e), a = r.connect(i.origin);
962
- try {
963
- return await new Promise((e, r) => {
964
- a.once("error", r);
965
- let o = a.request({
966
- ":method": "POST",
967
- ":path": i.pathname,
968
- ...t
969
- }), s = 0, c = [];
970
- o.on("response", (e) => {
971
- s = Number(e[":status"] ?? 0);
972
- }), o.on("data", (e) => c.push(e)), o.on("end", () => e({
973
- status: s,
974
- body: Buffer.concat(c).toString("utf8")
975
- })), o.on("error", r), o.end(n);
976
- });
977
- } finally {
978
- a.close();
979
- }
980
- }, ht = (e) => {
981
- let t = [
982
- "keyId",
983
- "teamId",
984
- "privateKey",
985
- "bundleId"
986
- ].filter((t) => !e[t]);
987
- if (t.length > 0) throw Error(`apnsTransport: ${t.join(", ")} required — supply APNS_KEY_ID, APNS_TEAM_ID, APNS_PRIVATE_KEY (the .p8) and APNS_BUNDLE_ID from the environment`);
988
- let n = e.request ?? mt, r = e.now ?? Date.now, i = lt[e.environment ?? "production"], a = null, o = () => {
989
- let t = r();
990
- return (a === null || t - a.at > ut) && (a = {
991
- token: ft(e, t),
992
- at: t
993
- }), a.token;
994
- };
995
- return async (t) => {
996
- let r = await n({
997
- url: `${i}/3/device/${encodeURIComponent(t.token)}`,
998
- headers: {
999
- authorization: `bearer ${o()}`,
1000
- "apns-topic": e.bundleId,
1001
- "apns-push-type": "alert",
1002
- "apns-priority": "10",
1003
- "content-type": "application/json"
1004
- },
1005
- body: JSON.stringify(pt(t))
1006
- });
1007
- if (r.status === 200) return;
1008
- let a = "";
1009
- try {
1010
- a = String(JSON.parse(r.body).reason ?? "");
1011
- } catch {
1012
- a = "";
1013
- }
1014
- throw r.status === 410 || dt.has(a) ? new I({
1015
- token: t.token,
1016
- reason: a || "Unregistered"
1017
- }) : Error(`apns: HTTP ${r.status}${a ? ` — ${a}` : ""}`);
1018
- };
1019
- }, gt = "https://www.googleapis.com/auth/firebase.messaging", _t = "https://oauth2.googleapis.com/token", vt = (e) => `https://fcm.googleapis.com/v1/projects/${encodeURIComponent(e)}/messages:send`, yt = /* @__PURE__ */ new Set(["UNREGISTERED", "NOT_FOUND"]), Q = (e) => Buffer.from(e).toString("base64url"), bt = (e) => {
1020
- let t;
1021
- if (typeof e == "string") try {
1022
- t = JSON.parse(e);
1023
- } catch {
1024
- throw Error("fcmTransport: FCM_SERVICE_ACCOUNT is not the JSON Google issued");
1025
- }
1026
- else t = e;
1027
- let n = [
1028
- "project_id",
1029
- "client_email",
1030
- "private_key"
1031
- ].filter((e) => typeof t[e] != "string" || t[e].length === 0);
1032
- if (n.length > 0) throw Error(`fcmTransport: service account is missing ${n.join(", ")} — supply FCM_SERVICE_ACCOUNT from the environment`);
1033
- return t;
1034
- }, xt = (e, t) => {
1035
- let n = Math.floor(t / 1e3), r = Q(JSON.stringify({
1036
- alg: "RS256",
1037
- typ: "JWT"
1038
- })), i = Q(JSON.stringify({
1039
- iss: e.client_email,
1040
- scope: gt,
1041
- aud: e.token_uri ?? "https://oauth2.googleapis.com/token",
1042
- iat: n,
1043
- exp: n + 3600
1044
- }));
1045
- return `${r}.${i}.${Q(F("sha256", Buffer.from(`${r}.${i}`), e.private_key))}`;
1046
- }, St = (e) => ({ message: {
1047
- token: e.token,
1048
- notification: {
1049
- title: e.title,
1050
- body: e.body,
1051
- ...e.image === void 0 ? {} : { image: e.image }
1052
- },
1053
- data: Object.fromEntries(Object.entries(e.data).map(([e, t]) => [e, typeof t == "string" ? t : JSON.stringify(t)])),
1054
- ...e.badge !== void 0 || e.image !== void 0 ? { apns: {
1055
- payload: { aps: {
1056
- ...e.badge === void 0 ? {} : { badge: e.badge },
1057
- ...e.image === void 0 ? {} : { "mutable-content": 1 }
1058
- } },
1059
- ...e.image === void 0 ? {} : { fcm_options: { image: e.image } }
1060
- } } : {}
1061
- } }), Ct = (e) => {
1062
- let t = bt(e.serviceAccount), n = e.fetch ?? fetch, r = e.now ?? Date.now, i = e.endpoint ?? vt(t.project_id), a = null, o = async () => {
1063
- let e = r();
1064
- if (a !== null && a.expiresAt - 6e4 > e) return a.token;
1065
- let i = await n(t.token_uri ?? "https://oauth2.googleapis.com/token", {
1066
- method: "POST",
1067
- headers: { "content-type": "application/x-www-form-urlencoded" },
1068
- body: new URLSearchParams({
1069
- grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
1070
- assertion: xt(t, e)
1071
- }).toString()
1072
- }), o = await i.json().catch(() => ({}));
1073
- if (!i.ok || typeof o.access_token != "string") throw Error(`fcm: token exchange HTTP ${i.status}${o.error ? ` — ${o.error}` : ""}`);
1074
- return a = {
1075
- token: o.access_token,
1076
- expiresAt: e + (o.expires_in ?? 3600) * 1e3
1077
- }, a.token;
1078
- };
1079
- return async (e) => {
1080
- let t = await n(i, {
1081
- method: "POST",
1082
- headers: {
1083
- authorization: `Bearer ${await o()}`,
1084
- "content-type": "application/json"
1085
- },
1086
- body: JSON.stringify(St(e))
1087
- });
1088
- if (t.ok) return;
1089
- let r = await t.json().catch(() => ({})), a = r.error?.details?.find((e) => typeof e.errorCode == "string")?.errorCode ?? r.error?.status ?? "";
1090
- throw t.status === 404 || yt.has(a) ? new I({
1091
- token: e.token,
1092
- reason: a || "UNREGISTERED"
1093
- }) : Error(`fcm: HTTP ${t.status}${a ? ` — ${a}` : ""}${r.error?.message ? ` — ${r.error.message}` : ""}`);
1094
- };
1095
- }, wt = "https://exp.host/--/api/v2/push/send", Tt = (e) => /^Expo(nent)?PushToken\[[^\]]+\]$/.test(e), Et = (e = {}) => async (t) => {
1096
- if (!Tt(t.token)) throw new I({
1097
- token: t.token,
1098
- reason: "not an Expo push token"
1099
- });
1100
- let n = await (e.fetch ?? fetch)(e.endpoint ?? "https://exp.host/--/api/v2/push/send", {
1101
- method: "POST",
1102
- headers: {
1103
- "content-type": "application/json",
1104
- accept: "application/json",
1105
- ...e.accessToken ? { authorization: `Bearer ${e.accessToken}` } : {}
1106
- },
1107
- body: JSON.stringify({
1108
- to: t.token,
1109
- title: t.title,
1110
- body: t.body,
1111
- data: t.data,
1112
- ...t.badge === void 0 ? {} : { badge: t.badge },
1113
- ...t.image === void 0 ? {} : { richContent: { image: t.image } },
1114
- ...t.actions !== void 0 && typeof t.data.notification == "string" ? { categoryId: t.data.notification } : {}
1115
- })
1116
- }), r = await n.json().catch(() => ({}));
1117
- if (!n.ok) throw Error(`expo push: HTTP ${n.status}${r.errors?.[0]?.message ? ` — ${r.errors[0].message}` : ""}`);
1118
- let i = Array.isArray(r.data) ? r.data[0] : r.data;
1119
- if (!i || i.status === "ok") return;
1120
- let a = i.details?.error;
1121
- throw a === "DeviceNotRegistered" || a === "InvalidCredentials" ? new I({
1122
- token: t.token,
1123
- reason: a
1124
- }) : Error(`expo push: ${a ?? "error"}${i.message ? ` — ${i.message}` : ""}`);
1125
- }, Dt = k("_voltro_notification_inbox", {
1126
- id: S({ prefix: "notif" }),
1127
- subjectId: A(),
1128
- category: A(),
1129
- title: A(),
1130
- body: A(),
1131
- data: T(),
1132
- readAt: j().nullable(),
1133
- archivedAt: j().nullable(),
1134
- createdAt: j().default("now"),
1135
- tenantId: A().nullable()
1136
- }).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }), Ot = k("_voltro_notification_preferences", {
1137
- id: S({ prefix: "notifpref" }),
1138
- subjectId: A(),
1139
- category: A(),
1140
- channel: A(),
1141
- enabled: y().default(!0)
1
+ import { PushTokenRejected as e } from "./errors.js";
2
+ import { consoleChannel as t, customChannel as n, emailChannel as r, pushChannel as i, resolveChannels as a, richPushOf as o, smsChannel as s, webhookChannel as c } from "./channels.js";
3
+ import { dataStoreNotificationStore as l, memoryNotificationStore as u } from "./store.js";
4
+ import { buildNotificationService as d, inAppChannel as f, inQuietHours as p, minuteOfDayInZone as m, quietHoursEnd as h } from "./service.js";
5
+ import { BASE_NAME as g, archiveDescriptor as _, clearQuietHoursDescriptor as ee, inboxDescriptor as te, inboxFeed as ne, markAllReadDescriptor as re, markReadDescriptor as ie, markUnreadDescriptor as ae, notificationsRpcClientImports as v, preferencesDescriptor as oe, setPreferenceDescriptor as se, setQuietHoursDescriptor as ce, subscribeDescriptor as y, unarchiveDescriptor as le, unreadCountDescriptor as ue, unsubscribeDescriptor as de, webPushPublicKeyDescriptor as fe, webPushRpcClientImports as pe, webPushStatusDescriptor as me, webPushSubscribeDescriptor as he, webPushUnsubscribeDescriptor as ge } from "./rpc.js";
6
+ import { clickTokenMatches as _e, decryptWebPushPayload as b, encryptWebPushPayload as x, generateVapidPrivateKey as S, sendWebPush as C, vapidAuthorization as w, vapidPublicKeyFor as T } from "./webPushProtocol.js";
7
+ import { assertWebPushConfigured as ve, bindWebPushStore as ye, dataStorePushSubscriptionStore as E, endpointHashOf as D, memoryPushSubscriptionStore as O, pushSubscriptionsTable as k, resetWebPushStoreForTest as be, webPushCell as A, webPushChannel as j, webPushPublicKey as M } from "./webPush.js";
8
+ import { defineNotification as N } from "./define.js";
9
+ import { sendNotification as xe } from "./sendNotification.js";
10
+ import { NOTIFICATION_SEND_EFFECT as Se, enqueueNotification as Ce, notificationOutboxHandler as we } from "./durable.js";
11
+ import { APNS_HOSTS as Te, APNS_TOKEN_REJECTIONS as Ee, apnsBody as De, apnsProviderToken as Oe, apnsTransport as ke } from "./apnsPush.js";
12
+ import { FCM_SCOPE as Ae, FCM_TOKEN_REJECTIONS as je, GOOGLE_TOKEN_URI as P, fcmAssertion as Me, fcmMessage as Ne, fcmSendEndpoint as Pe, fcmTransport as Fe, parseServiceAccount as Ie } from "./fcmPush.js";
13
+ import { EXPO_PUSH_ENDPOINT as Le, expoPushTransport as Re, isExpoPushToken as ze } from "./expoPush.js";
14
+ import { Context as Be, Effect as F, Layer as Ve } from "effect";
15
+ import { boolean as He, id as I, integer as L, isNotNull as Ue, json as R, registerRetention as z, retentionTtlMsFromEnv as B, table as V, text as H, timestamp as U } from "@voltro/database";
16
+ import { definePlugin as We, pluginInstanceName as Ge, publishReactivity as Ke } from "@voltro/protocol";
17
+ //#region src/index.ts
18
+ var W = V("_voltro_notification_inbox", {
19
+ id: I({ prefix: "notif" }),
20
+ subjectId: H(),
21
+ category: H(),
22
+ title: H(),
23
+ body: H(),
24
+ data: R(),
25
+ readAt: U().nullable(),
26
+ archivedAt: U().nullable(),
27
+ createdAt: U().default("now"),
28
+ tenantId: H().nullable()
29
+ }).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }), G = V("_voltro_notification_preferences", {
30
+ id: I({ prefix: "notifpref" }),
31
+ subjectId: H(),
32
+ category: H(),
33
+ channel: H(),
34
+ enabled: He().default(!0)
1142
35
  }).renamedFrom("notification_preferences").unique("byPreferencesSubjectCategoryChannel", [
1143
36
  "subjectId",
1144
37
  "category",
1145
38
  "channel"
1146
- ]), kt = k("_voltro_notification_deliveries", {
1147
- id: S({ prefix: "notifdlv" }),
1148
- recipient: A(),
1149
- category: A(),
1150
- channel: A(),
1151
- status: A(),
1152
- error: A().nullable(),
1153
- at: j().default("now"),
1154
- endpoint: A().nullable(),
1155
- clickToken: A().nullable(),
1156
- clickedAt: j().nullable()
1157
- }).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]).index("byDeliveriesClickToken", ["clickToken"]), At = k("_voltro_notification_topic_subscriptions", {
1158
- id: S({ prefix: "notiftsub" }),
1159
- topic: A(),
1160
- subjectId: A(),
1161
- tenantId: A().nullable()
1162
- }).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]), jt = k("_voltro_notification_quiet_hours", {
1163
- id: S({ prefix: "notifqh" }),
1164
- subjectId: A(),
1165
- startMinute: C(),
1166
- endMinute: C(),
1167
- tz: A().default("UTC"),
1168
- policy: A().default("hold")
1169
- }).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]), Mt = k("_voltro_notification_held", {
1170
- id: S({ prefix: "notifheld" }),
1171
- kind: A(),
1172
- subjectId: A(),
1173
- send: T(),
1174
- flushAt: j()
1175
- }).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]), $ = class extends oe.Tag("@voltro/plugin-notifications/NotificationService")() {}, Nt = (e) => async (t) => {
39
+ ]), K = V("_voltro_notification_deliveries", {
40
+ id: I({ prefix: "notifdlv" }),
41
+ recipient: H(),
42
+ category: H(),
43
+ channel: H(),
44
+ status: H(),
45
+ error: H().nullable(),
46
+ at: U().default("now"),
47
+ endpoint: H().nullable(),
48
+ clickToken: H().nullable(),
49
+ clickedAt: U().nullable()
50
+ }).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]).index("byDeliveriesClickToken", ["clickToken"]), q = V("_voltro_notification_topic_subscriptions", {
51
+ id: I({ prefix: "notiftsub" }),
52
+ topic: H(),
53
+ subjectId: H(),
54
+ tenantId: H().nullable()
55
+ }).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]), J = V("_voltro_notification_quiet_hours", {
56
+ id: I({ prefix: "notifqh" }),
57
+ subjectId: H(),
58
+ startMinute: L(),
59
+ endMinute: L(),
60
+ tz: H().default("UTC"),
61
+ policy: H().default("hold")
62
+ }).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]), Y = V("_voltro_notification_held", {
63
+ id: I({ prefix: "notifheld" }),
64
+ kind: H(),
65
+ subjectId: H(),
66
+ send: R(),
67
+ flushAt: U()
68
+ }).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]), X = class extends Be.Service()("@voltro/plugin-notifications/NotificationService") {}, Z = (e) => async (t) => {
1176
69
  if (e !== void 0) try {
1177
70
  let n = await e(t);
1178
71
  if (typeof n == "string" && n !== "") return n;
1179
72
  } catch {}
1180
73
  return t.request.subject.id ?? "anonymous";
1181
- }, Pt = (e) => e.request.subject.tenantId ?? null, Ft = e, It = { collects: [
74
+ }, Q = (e) => e.request.subject.tenantId ?? null, $ = g, qe = { collects: [
1182
75
  {
1183
76
  category: "device-id",
1184
77
  linked: !0,
@@ -1197,193 +90,193 @@ var I = class extends _.TaggedError()("PushTokenRejected", {
1197
90
  tracking: !1,
1198
91
  purposes: ["app-functionality"]
1199
92
  }
1200
- ] }, Lt = (e = {}) => {
1201
- let h = Nt(e.resolveSubjectId), oe = ue({
1202
- base: Ft,
93
+ ] }, Je = (e = {}) => {
94
+ let n = Z(e.resolveSubjectId), r = Ge({
95
+ base: $,
1203
96
  alias: e.alias,
1204
97
  instance: e.name
1205
98
  });
1206
- D({
99
+ z({
1207
100
  source: "plugin",
1208
101
  table: "_voltro_notification_deliveries",
1209
102
  timeColumn: "at",
1210
- ttlMs: O(process.env.VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS, 2160),
103
+ ttlMs: B(process.env.VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS, 2160),
1211
104
  envVar: "VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS"
1212
- }), D({
105
+ }), z({
1213
106
  source: "plugin",
1214
107
  table: "_voltro_notification_inbox",
1215
108
  timeColumn: "createdAt",
1216
- ttlMs: O(process.env.VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS, 4320),
109
+ ttlMs: B(process.env.VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS, 4320),
1217
110
  envVar: "VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS",
1218
- where: ce("readAt")
1219
- }), D({
111
+ where: Ue("readAt")
112
+ }), z({
1220
113
  source: "plugin",
1221
114
  table: "_voltro_notification_held",
1222
115
  timeColumn: "flushAt",
1223
- ttlMs: O(process.env.VOLTRO_NOTIFICATION_HELD_TTL_HOURS, 168),
116
+ ttlMs: B(process.env.VOLTRO_NOTIFICATION_HELD_TTL_HOURS, 168),
1224
117
  envVar: "VOLTRO_NOTIFICATION_HELD_TTL_HOURS"
1225
118
  });
1226
- let _ = e.store === void 0, v = { current: e.store ?? Ee() }, y, b = (e) => e.then((e) => (de(y, i), e)), x = {
1227
- addInbox: (e) => b(v.current.addInbox(e)),
1228
- listInbox: (e, t) => v.current.listInbox(e, t),
1229
- markRead: (e, t) => b(v.current.markRead(e, t)),
1230
- markUnread: (e, t) => b(v.current.markUnread(e, t)),
1231
- markAllRead: (e) => b(v.current.markAllRead(e)),
1232
- setArchived: (e, t, n) => b(v.current.setArchived(e, t, n)),
1233
- unreadCount: (e) => v.current.unreadCount(e),
1234
- getPreferences: (e) => v.current.getPreferences(e),
1235
- setPreference: (e) => v.current.setPreference(e),
1236
- recordDelivery: (e) => v.current.recordDelivery(e),
1237
- listDeliveries: (e) => v.current.listDeliveries(e),
1238
- markClicked: (e) => v.current.markClicked(e),
1239
- subscribeTopic: (e) => v.current.subscribeTopic(e),
1240
- unsubscribeTopic: (e, t) => v.current.unsubscribeTopic(e, t),
1241
- listTopicSubscribers: (e) => v.current.listTopicSubscribers(e),
1242
- getQuietHours: (e) => v.current.getQuietHours(e),
1243
- setQuietHours: (e) => v.current.setQuietHours(e),
1244
- clearQuietHours: (e) => v.current.clearQuietHours(e),
1245
- enqueueHeld: (e) => v.current.enqueueHeld(e),
1246
- pendingDigest: (e) => v.current.pendingDigest(e),
1247
- dueHeld: (e) => v.current.dueHeld(e),
1248
- clearHeld: (e) => v.current.clearHeld(e)
1249
- }, S = e.channels ?? [ve()], C = S.some((e) => e.id === "inApp") ? S : [...S, Oe(x)], w = e.digestWindowMs ?? 0, T = C.some((e) => e.id === "webPush"), E = je({
1250
- channels: C,
1251
- store: x,
1252
- digestWindowMs: w
1253
- }), k, A = [
119
+ let i = e.store === void 0, a = { current: e.store ?? u() }, o, s = (e) => e.then((e) => (Ke(o, ne), e)), c = {
120
+ addInbox: (e) => s(a.current.addInbox(e)),
121
+ listInbox: (e, t) => a.current.listInbox(e, t),
122
+ markRead: (e, t) => s(a.current.markRead(e, t)),
123
+ markUnread: (e, t) => s(a.current.markUnread(e, t)),
124
+ markAllRead: (e) => s(a.current.markAllRead(e)),
125
+ setArchived: (e, t, n) => s(a.current.setArchived(e, t, n)),
126
+ unreadCount: (e) => a.current.unreadCount(e),
127
+ getPreferences: (e) => a.current.getPreferences(e),
128
+ setPreference: (e) => a.current.setPreference(e),
129
+ recordDelivery: (e) => a.current.recordDelivery(e),
130
+ listDeliveries: (e) => a.current.listDeliveries(e),
131
+ markClicked: (e) => a.current.markClicked(e),
132
+ subscribeTopic: (e) => a.current.subscribeTopic(e),
133
+ unsubscribeTopic: (e, t) => a.current.unsubscribeTopic(e, t),
134
+ listTopicSubscribers: (e) => a.current.listTopicSubscribers(e),
135
+ getQuietHours: (e) => a.current.getQuietHours(e),
136
+ setQuietHours: (e) => a.current.setQuietHours(e),
137
+ clearQuietHours: (e) => a.current.clearQuietHours(e),
138
+ enqueueHeld: (e) => a.current.enqueueHeld(e),
139
+ pendingDigest: (e) => a.current.pendingDigest(e),
140
+ dueHeld: (e) => a.current.dueHeld(e),
141
+ clearHeld: (e) => a.current.clearHeld(e)
142
+ }, p = e.channels ?? [t()], m = p.some((e) => e.id === "inApp") ? p : [...p, f(c)], h = e.digestWindowMs ?? 0, g = m.some((e) => e.id === "webPush"), b = d({
143
+ channels: m,
144
+ store: c,
145
+ digestWindowMs: h
146
+ }), x, S = [
1254
147
  {
1255
- ...r,
148
+ ...te,
1256
149
  description: "The caller's in-app inbox items.",
1257
- execute: (e, t) => g.promise(async () => E.inbox(await h(t), e))
150
+ execute: (e, t) => F.promise(async () => b.inbox(await n(t), e))
1258
151
  },
1259
152
  {
1260
- ...m,
153
+ ...ue,
1261
154
  description: "The caller's unread count.",
1262
- execute: (e, t) => g.promise(async () => ({ count: await E.unreadCount(await h(t)) }))
155
+ execute: (e, t) => F.promise(async () => ({ count: await b.unreadCount(await n(t)) }))
1263
156
  },
1264
157
  {
1265
- ...o,
158
+ ...ie,
1266
159
  description: "Mark an inbox item read.",
1267
- execute: (e, t) => g.promise(async () => ({ ok: await E.markRead(e.id, await h(t)) }))
160
+ execute: (e, t) => F.promise(async () => ({ ok: await b.markRead(e.id, await n(t)) }))
1268
161
  },
1269
162
  {
1270
- ...s,
163
+ ...ae,
1271
164
  description: "Mark an inbox item unread again.",
1272
- execute: (e, t) => g.promise(async () => ({ ok: await E.markUnread(e.id, await h(t)) }))
165
+ execute: (e, t) => F.promise(async () => ({ ok: await b.markUnread(e.id, await n(t)) }))
1273
166
  },
1274
167
  {
1275
- ...a,
168
+ ...re,
1276
169
  description: "Mark every unread item in the caller's inbox read.",
1277
- execute: (e, t) => g.promise(async () => ({ count: await E.markAllRead(await h(t)) }))
170
+ execute: (e, t) => F.promise(async () => ({ count: await b.markAllRead(await n(t)) }))
1278
171
  },
1279
172
  {
1280
- ...t,
173
+ ..._,
1281
174
  description: "Archive an inbox item — a different state from read; this is what empties the inbox.",
1282
- execute: (e, t) => g.promise(async () => ({ ok: await E.setArchived(e.id, await h(t), !0) }))
175
+ execute: (e, t) => F.promise(async () => ({ ok: await b.setArchived(e.id, await n(t), !0) }))
1283
176
  },
1284
177
  {
1285
- ...p,
178
+ ...le,
1286
179
  description: "Return an archived item to the inbox.",
1287
- execute: (e, t) => g.promise(async () => ({ ok: await E.setArchived(e.id, await h(t), !1) }))
180
+ execute: (e, t) => F.promise(async () => ({ ok: await b.setArchived(e.id, await n(t), !1) }))
1288
181
  },
1289
182
  {
1290
- ...l,
183
+ ...oe,
1291
184
  description: "The caller's channel preferences.",
1292
- execute: (e, t) => g.promise(async () => E.getPreferences(await h(t)))
185
+ execute: (e, t) => F.promise(async () => b.getPreferences(await n(t)))
1293
186
  },
1294
187
  {
1295
- ...u,
188
+ ...se,
1296
189
  description: "Toggle a channel on/off for a category.",
1297
- execute: (e, t) => g.promise(async () => {
1298
- let { category: n, channel: r, enabled: i } = e;
1299
- return await E.setPreference({
1300
- subjectId: await h(t),
1301
- category: n,
1302
- channel: r,
1303
- enabled: i
190
+ execute: (e, t) => F.promise(async () => {
191
+ let { category: r, channel: i, enabled: a } = e;
192
+ return await b.setPreference({
193
+ subjectId: await n(t),
194
+ category: r,
195
+ channel: i,
196
+ enabled: a
1304
197
  }), { ok: !0 };
1305
198
  })
1306
199
  },
1307
200
  {
1308
- ...f,
201
+ ...y,
1309
202
  description: "Subscribe the caller to a broadcast topic.",
1310
- execute: (e, t) => g.promise(async () => (await E.subscribe(e.topic, await h(t), Pt(t)), { ok: !0 }))
203
+ execute: (e, t) => F.promise(async () => (await b.subscribe(e.topic, await n(t), Q(t)), { ok: !0 }))
1311
204
  },
1312
205
  {
1313
- ...ee,
206
+ ...de,
1314
207
  description: "Unsubscribe the caller from a broadcast topic.",
1315
- execute: (e, t) => g.promise(async () => (await E.unsubscribe(e.topic, await h(t)), { ok: !0 }))
208
+ execute: (e, t) => F.promise(async () => (await b.unsubscribe(e.topic, await n(t)), { ok: !0 }))
1316
209
  },
1317
210
  {
1318
- ...d,
211
+ ...ce,
1319
212
  description: "Set the caller's quiet-hours (DND) window.",
1320
- execute: (e, t) => g.promise(async () => {
1321
- let n = e;
1322
- return await E.setQuietHours({
1323
- subjectId: await h(t),
1324
- startMinute: n.startMinute,
1325
- endMinute: n.endMinute,
1326
- ...n.tz ? { tz: n.tz } : {},
1327
- ...n.policy ? { policy: n.policy } : {}
213
+ execute: (e, t) => F.promise(async () => {
214
+ let r = e;
215
+ return await b.setQuietHours({
216
+ subjectId: await n(t),
217
+ startMinute: r.startMinute,
218
+ endMinute: r.endMinute,
219
+ ...r.tz ? { tz: r.tz } : {},
220
+ ...r.policy ? { policy: r.policy } : {}
1328
221
  }), { ok: !0 };
1329
222
  })
1330
223
  },
1331
224
  {
1332
- ...n,
225
+ ...ee,
1333
226
  description: "Clear the caller's quiet-hours window.",
1334
- execute: (e, t) => g.promise(async () => (await E.clearQuietHours(await h(t)), { ok: !0 }))
227
+ execute: (e, t) => F.promise(async () => (await b.clearQuietHours(await n(t)), { ok: !0 }))
1335
228
  }
1336
- ], j = T ? [
229
+ ], C = g ? [
1337
230
  {
1338
- ...te,
231
+ ...fe,
1339
232
  description: "The VAPID application-server key a subscribing browser needs.",
1340
- execute: () => g.sync(() => ({ key: nt(q().channelOptions?.privateKey) }))
233
+ execute: () => F.sync(() => ({ key: M(A().channelOptions?.privateKey) }))
1341
234
  },
1342
235
  {
1343
- ...ie,
236
+ ...he,
1344
237
  description: "Register the calling subject's browser push subscription (one row per endpoint).",
1345
- execute: (e, t) => g.promise(async () => {
1346
- let n = e;
1347
- return await q().store.upsert({
1348
- subjectId: await h(t),
1349
- endpoint: n.endpoint,
1350
- endpointHash: W(n.endpoint),
1351
- p256dh: n.p256dh,
1352
- auth: n.auth,
1353
- ua: n.ua ?? null,
1354
- tenantId: Pt(t)
238
+ execute: (e, t) => F.promise(async () => {
239
+ let r = e;
240
+ return await A().store.upsert({
241
+ subjectId: await n(t),
242
+ endpoint: r.endpoint,
243
+ endpointHash: D(r.endpoint),
244
+ p256dh: r.p256dh,
245
+ auth: r.auth,
246
+ ua: r.ua ?? null,
247
+ tenantId: Q(t)
1355
248
  }), { ok: !0 };
1356
249
  })
1357
250
  },
1358
251
  {
1359
- ...ae,
252
+ ...ge,
1360
253
  description: "Remove one of the calling subject's own subscriptions.",
1361
- execute: (e, t) => g.promise(async () => ({ ok: await q().store.removeForSubject(W(e.endpoint), await h(t)) }))
254
+ execute: (e, t) => F.promise(async () => ({ ok: await A().store.removeForSubject(D(e.endpoint), await n(t)) }))
1362
255
  },
1363
256
  {
1364
- ...re,
257
+ ...me,
1365
258
  description: "How many push endpoints the calling subject has registered.",
1366
- execute: (e, t) => g.promise(async () => ({ endpoints: await q().store.countForSubject(await h(t)) }))
259
+ execute: (e, t) => F.promise(async () => ({ endpoints: await A().store.countForSubject(await n(t)) }))
1367
260
  }
1368
- ] : [], M = (e) => ({
261
+ ] : [], w = (e) => ({
1369
262
  kind: "json",
1370
263
  data: e
1371
- }), fe = (e, t) => ({
264
+ }), T = (e, t) => ({
1372
265
  kind: "json",
1373
266
  status: e,
1374
267
  data: { error: t }
1375
- }), N = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), pe = [{
268
+ }), E = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), O = [{
1376
269
  method: "GET",
1377
270
  path: "/deliveries",
1378
271
  scope: "process",
1379
272
  description: "Recent delivery records (newest first) + per-channel + per-status counts.",
1380
- handler: () => g.promise(async () => {
1381
- let e = await x.listDeliveries({ limit: 200 }), t = {}, n = 0, r = 0;
273
+ handler: () => F.promise(async () => {
274
+ let e = await c.listDeliveries({ limit: 200 }), t = {}, n = 0, r = 0;
1382
275
  for (let i of e) t[i.channel] = (t[i.channel] ?? 0) + 1, i.status === "sent" ? n++ : i.status === "failed" && r++;
1383
276
  let i = e.slice(0, 100).map(({ clickToken: e, ...t }) => t);
1384
- return M({
277
+ return w({
1385
278
  deliveries: i,
1386
- channels: C.map((e) => e.id),
279
+ channels: m.map((e) => e.id),
1387
280
  stats: {
1388
281
  sent: n,
1389
282
  failed: r,
@@ -1397,29 +290,29 @@ var I = class extends _.TaggedError()("PushTokenRejected", {
1397
290
  scope: "process",
1398
291
  description: "In-app inbox for a subject (?subjectId=…).",
1399
292
  handler: (e) => {
1400
- let t = N(e.url, "subjectId");
1401
- return t ? g.promise(async () => M({
1402
- items: await x.listInbox(t, { limit: 100 }),
1403
- unread: await x.unreadCount(t)
1404
- })) : g.succeed(fe(400, "subjectId query param required"));
293
+ let t = E(e.url, "subjectId");
294
+ return t ? F.promise(async () => w({
295
+ items: await c.listInbox(t, { limit: 100 }),
296
+ unread: await c.unreadCount(t)
297
+ })) : F.succeed(T(400, "subjectId query param required"));
1405
298
  }
1406
299
  }];
1407
- return le({
1408
- privacy: It,
1409
- name: oe,
1410
- baseName: Ft,
300
+ return We({
301
+ privacy: qe,
302
+ name: r,
303
+ baseName: $,
1411
304
  description: "Unified notifications — email/slack/sms/push/in-app with preference routing, inbox + delivery log.",
1412
305
  permissions: ["inspect:read", "store:write"],
1413
306
  ...e.tables === !1 ? {} : { extendSchema: { tables: [
1414
- Dt,
1415
- Ot,
1416
- kt,
1417
- At,
1418
- jt,
1419
- Mt,
1420
- Ke
307
+ W,
308
+ G,
309
+ K,
310
+ q,
311
+ J,
312
+ Y,
313
+ k
1421
314
  ] } },
1422
- ...T ? {
315
+ ...g ? {
1423
316
  declaredEnv: [{
1424
317
  name: "VOLTRO_VAPID_PRIVATE_KEY",
1425
318
  required: !0,
@@ -1435,12 +328,12 @@ var I = class extends _.TaggedError()("PushTokenRejected", {
1435
328
  try {
1436
329
  t = JSON.parse(Buffer.from(e.rawBody).toString("utf8")).token;
1437
330
  } catch {}
1438
- if (typeof t != "string" || t === "" || !Ge(t, t)) return {
331
+ if (typeof t != "string" || t === "" || !_e(t, t)) return {
1439
332
  status: 400,
1440
333
  body: JSON.stringify({ error: "token required" }),
1441
334
  contentType: "application/json"
1442
335
  };
1443
- let n = await x.markClicked(t);
336
+ let n = await c.markClicked(t);
1444
337
  return {
1445
338
  status: n ? 200 : 404,
1446
339
  body: JSON.stringify({ ok: n }),
@@ -1450,27 +343,27 @@ var I = class extends _.TaggedError()("PushTokenRejected", {
1450
343
  maxBodyBytes: 4096
1451
344
  }]
1452
345
  } : {},
1453
- services: se.succeed($, E),
1454
- routes: [...A, ...j],
1455
- rpcClientDescriptors: T ? [...c, ...ne] : c,
1456
- inspectEndpoints: pe,
346
+ services: Ve.succeed(X, b),
347
+ routes: [...S, ...C],
348
+ rpcClientDescriptors: g ? [...v, ...pe] : v,
349
+ inspectEndpoints: O,
1457
350
  bindDataStore: (t) => {
1458
- y = t, _ && (v.current = De(t)), Ye(t);
351
+ o = t, i && (a.current = l(t)), ye(t);
1459
352
  let n = e.flushIntervalMs ?? 3e4;
1460
- k = setInterval(() => {
1461
- E.flushDue().catch(() => {});
1462
- }, n), typeof k.unref == "function" && k.unref();
1463
- },
1464
- onActivate: (e) => g.sync(() => {
1465
- T && $e(), e.logger.info("notifications active", {
1466
- channels: C.map((e) => e.id),
1467
- digestWindowMs: w
353
+ x = setInterval(() => {
354
+ b.flushDue().catch(() => {});
355
+ }, n), typeof x.unref == "function" && x.unref();
356
+ },
357
+ onActivate: (e) => F.sync(() => {
358
+ g && ve(), e.logger.info("notifications active", {
359
+ channels: m.map((e) => e.id),
360
+ digestWindowMs: h
1468
361
  });
1469
362
  }),
1470
- onDeactivate: () => g.sync(() => {
1471
- k && clearInterval(k), k = void 0;
363
+ onDeactivate: () => F.sync(() => {
364
+ x && clearInterval(x), x = void 0;
1472
365
  })
1473
366
  });
1474
367
  };
1475
368
  //#endregion
1476
- export { lt as APNS_HOSTS, dt as APNS_TOKEN_REJECTIONS, wt as EXPO_PUSH_ENDPOINT, gt as FCM_SCOPE, yt as FCM_TOKEN_REJECTIONS, _t as GOOGLE_TOKEN_URI, X as NOTIFICATION_SEND_EFFECT, $ as NotificationService, I as PushTokenRejected, pt as apnsBody, ft as apnsProviderToken, ht as apnsTransport, je as buildNotificationService, ve as consoleChannel, Se as customChannel, De as dataStoreNotificationStore, qe as dataStorePushSubscriptionStore, Ue as decryptWebPushPayload, h as defineNotification, kt as deliveriesTable, be as emailChannel, He as encryptWebPushPayload, W as endpointHashOf, ot as enqueueNotification, Et as expoPushTransport, xt as fcmAssertion, St as fcmMessage, vt as fcmSendEndpoint, Ct as fcmTransport, Ne as generateVapidPrivateKey, Mt as heldTable, Oe as inAppChannel, ke as inQuietHours, Dt as inboxTable, Tt as isExpoPushToken, Nt as makeSubjectId, Ee as memoryNotificationStore, G as memoryPushSubscriptionStore, R as minuteOfDayInZone, ct as notificationOutboxHandler, Lt as notificationsPlugin, bt as parseServiceAccount, Ot as preferencesTable, we as pushChannel, Ke as pushSubscriptionsTable, Ae as quietHoursEnd, jt as quietHoursTable, Xe as resetWebPushStoreForTest, Te as resolveChannels, Ce as richPushOf, it as sendNotification, We as sendWebPush, xe as smsChannel, At as topicSubscriptionsTable, Le as vapidAuthorization, U as vapidPublicKeyFor, Qe as webPushChannel, nt as webPushPublicKey, ye as webhookChannel };
369
+ export { Te as APNS_HOSTS, Ee as APNS_TOKEN_REJECTIONS, Le as EXPO_PUSH_ENDPOINT, Ae as FCM_SCOPE, je as FCM_TOKEN_REJECTIONS, P as GOOGLE_TOKEN_URI, Se as NOTIFICATION_SEND_EFFECT, X as NotificationService, e as PushTokenRejected, De as apnsBody, Oe as apnsProviderToken, ke as apnsTransport, d as buildNotificationService, t as consoleChannel, n as customChannel, l as dataStoreNotificationStore, E as dataStorePushSubscriptionStore, b as decryptWebPushPayload, N as defineNotification, K as deliveriesTable, r as emailChannel, x as encryptWebPushPayload, D as endpointHashOf, Ce as enqueueNotification, Re as expoPushTransport, Me as fcmAssertion, Ne as fcmMessage, Pe as fcmSendEndpoint, Fe as fcmTransport, S as generateVapidPrivateKey, Y as heldTable, f as inAppChannel, p as inQuietHours, W as inboxTable, ze as isExpoPushToken, Z as makeSubjectId, u as memoryNotificationStore, O as memoryPushSubscriptionStore, m as minuteOfDayInZone, we as notificationOutboxHandler, Je as notificationsPlugin, Ie as parseServiceAccount, G as preferencesTable, i as pushChannel, k as pushSubscriptionsTable, h as quietHoursEnd, J as quietHoursTable, be as resetWebPushStoreForTest, a as resolveChannels, o as richPushOf, xe as sendNotification, C as sendWebPush, s as smsChannel, q as topicSubscriptionsTable, w as vapidAuthorization, T as vapidPublicKeyFor, j as webPushChannel, M as webPushPublicKey, c as webhookChannel };