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