@voltro/plugin-notifications 0.85.0 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 ee, clearQuietHoursDescriptor as te, inboxDescriptor as ne, inboxFeed as re, markAllReadDescriptor as ie, markReadDescriptor as ae, markUnreadDescriptor as oe, notificationsRpcClientImports as _, preferencesDescriptor as se, preferencesFeed as ce, setPreferenceDescriptor as le, setQuietHoursDescriptor as v, subscribeDescriptor as ue, unarchiveDescriptor as de, unreadCountDescriptor as fe, unsubscribeDescriptor as pe, webPushPublicKeyDescriptor as me, webPushRpcClientImports as he, webPushStatusDescriptor as ge, webPushSubscribeDescriptor as _e, webPushUnsubscribeDescriptor as ve } from "./rpc.js";
6
+ import { clickTokenMatches as ye, decryptWebPushPayload as y, encryptWebPushPayload as b, generateVapidPrivateKey as x, sendWebPush as S, vapidAuthorization as C, vapidPublicKeyFor as w } from "./webPushProtocol.js";
7
+ import { assertWebPushConfigured as be, bindWebPushStore as xe, dataStorePushSubscriptionStore as T, endpointHashOf as E, memoryPushSubscriptionStore as D, pushSubscriptionsTable as O, resetWebPushStoreForTest as k, webPushCell as A, webPushChannel as j, webPushPublicKey as M } from "./webPush.js";
8
+ import { defineNotification as Se } from "./define.js";
9
+ import { sendNotification as Ce } from "./sendNotification.js";
10
+ import { NOTIFICATION_SEND_EFFECT as we, enqueueNotification as Te, notificationOutboxHandler as Ee } from "./durable.js";
11
+ import { APNS_HOSTS as De, APNS_TOKEN_REJECTIONS as Oe, apnsBody as ke, apnsProviderToken as Ae, apnsTransport as je } from "./apnsPush.js";
12
+ import { FCM_SCOPE as Me, FCM_TOKEN_REJECTIONS as N, GOOGLE_TOKEN_URI as Ne, fcmAssertion as Pe, fcmMessage as Fe, fcmSendEndpoint as Ie, fcmTransport as Le, parseServiceAccount as Re } from "./fcmPush.js";
13
+ import { EXPO_PUSH_ENDPOINT as ze, expoPushTransport as Be, isExpoPushToken as Ve } from "./expoPush.js";
14
+ import { Context as He, Effect as P, Layer as Ue } from "effect";
15
+ import { boolean as We, id as F, integer as I, isNotNull as Ge, json as L, registerRetention as R, retentionTtlMsFromEnv as z, table as B, text as V, timestamp as H } from "@voltro/database";
16
+ import { definePlugin as Ke, pluginInstanceName as qe, publishReactivity as U } from "@voltro/protocol";
17
+ //#region src/index.ts
18
+ var W = B("_voltro_notification_inbox", {
19
+ id: F({ prefix: "notif" }),
20
+ subjectId: V(),
21
+ category: V(),
22
+ title: V(),
23
+ body: V(),
24
+ data: L(),
25
+ readAt: H().nullable(),
26
+ archivedAt: H().nullable(),
27
+ createdAt: H().default("now"),
28
+ tenantId: V().nullable()
29
+ }).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }), G = B("_voltro_notification_preferences", {
30
+ id: F({ prefix: "notifpref" }),
31
+ subjectId: V(),
32
+ category: V(),
33
+ channel: V(),
34
+ enabled: We().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 = B("_voltro_notification_deliveries", {
40
+ id: F({ prefix: "notifdlv" }),
41
+ recipient: V(),
42
+ category: V(),
43
+ channel: V(),
44
+ status: V(),
45
+ error: V().nullable(),
46
+ at: H().default("now"),
47
+ endpoint: V().nullable(),
48
+ clickToken: V().nullable(),
49
+ clickedAt: H().nullable()
50
+ }).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]).index("byDeliveriesClickToken", ["clickToken"]), q = B("_voltro_notification_topic_subscriptions", {
51
+ id: F({ prefix: "notiftsub" }),
52
+ topic: V(),
53
+ subjectId: V(),
54
+ tenantId: V().nullable()
55
+ }).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]), J = B("_voltro_notification_quiet_hours", {
56
+ id: F({ prefix: "notifqh" }),
57
+ subjectId: V(),
58
+ startMinute: I(),
59
+ endMinute: I(),
60
+ tz: V().default("UTC"),
61
+ policy: V().default("hold")
62
+ }).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]), Y = B("_voltro_notification_held", {
63
+ id: F({ prefix: "notifheld" }),
64
+ kind: V(),
65
+ subjectId: V(),
66
+ send: L(),
67
+ flushAt: H()
68
+ }).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]), X = class extends He.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, Je = { collects: [
1195
75
  {
1196
76
  category: "device-id",
1197
77
  linked: !0,
@@ -1210,193 +90,194 @@ 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
+ ] }, Ye = (e = {}) => {
94
+ let n = Z(e.resolveSubjectId), r = qe({
95
+ base: $,
1216
96
  alias: e.alias,
1217
97
  instance: e.name
1218
98
  });
1219
- D({
99
+ R({
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: z(process.env.VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS, 2160),
1224
104
  envVar: "VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS"
1225
- }), D({
105
+ }), R({
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: z(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: Ge("readAt")
112
+ }), R({
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: z(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) => (U(o, re), e)), c = (e) => e.then((e) => (U(o, ce), e)), p = {
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) => c(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
+ }, m = e.channels ?? [t()], h = m.some((e) => e.id === "inApp") ? m : [...m, f(p)], g = e.digestWindowMs ?? 0, y = h.some((e) => e.id === "webPush"), b = d({
143
+ channels: h,
144
+ store: p,
145
+ digestWindowMs: g,
146
+ ...e.deliveryGuards === void 0 ? {} : { deliveryGuards: e.deliveryGuards }
147
+ }), x, S = [
1267
148
  {
1268
- ...r,
149
+ ...ne,
1269
150
  description: "The caller's in-app inbox items.",
1270
- execute: (e, t) => g.promise(async () => E.inbox(await h(t), e))
151
+ execute: (e, t) => P.promise(async () => b.inbox(await n(t), e))
1271
152
  },
1272
153
  {
1273
- ...m,
154
+ ...fe,
1274
155
  description: "The caller's unread count.",
1275
- execute: (e, t) => g.promise(async () => ({ count: await E.unreadCount(await h(t)) }))
156
+ execute: (e, t) => P.promise(async () => ({ count: await b.unreadCount(await n(t)) }))
1276
157
  },
1277
158
  {
1278
- ...o,
159
+ ...ae,
1279
160
  description: "Mark an inbox item read.",
1280
- execute: (e, t) => g.promise(async () => ({ ok: await E.markRead(e.id, await h(t)) }))
161
+ execute: (e, t) => P.promise(async () => ({ ok: await b.markRead(e.id, await n(t)) }))
1281
162
  },
1282
163
  {
1283
- ...s,
164
+ ...oe,
1284
165
  description: "Mark an inbox item unread again.",
1285
- execute: (e, t) => g.promise(async () => ({ ok: await E.markUnread(e.id, await h(t)) }))
166
+ execute: (e, t) => P.promise(async () => ({ ok: await b.markUnread(e.id, await n(t)) }))
1286
167
  },
1287
168
  {
1288
- ...a,
169
+ ...ie,
1289
170
  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)) }))
171
+ execute: (e, t) => P.promise(async () => ({ count: await b.markAllRead(await n(t)) }))
1291
172
  },
1292
173
  {
1293
- ...t,
174
+ ...ee,
1294
175
  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) }))
176
+ execute: (e, t) => P.promise(async () => ({ ok: await b.setArchived(e.id, await n(t), !0) }))
1296
177
  },
1297
178
  {
1298
- ...p,
179
+ ...de,
1299
180
  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) }))
181
+ execute: (e, t) => P.promise(async () => ({ ok: await b.setArchived(e.id, await n(t), !1) }))
1301
182
  },
1302
183
  {
1303
- ...l,
184
+ ...se,
1304
185
  description: "The caller's channel preferences.",
1305
- execute: (e, t) => g.promise(async () => E.getPreferences(await h(t)))
186
+ execute: (e, t) => P.promise(async () => b.getPreferences(await n(t)))
1306
187
  },
1307
188
  {
1308
- ...u,
189
+ ...le,
1309
190
  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
191
+ execute: (e, t) => P.promise(async () => {
192
+ let { category: r, channel: i, enabled: a } = e;
193
+ return await b.setPreference({
194
+ subjectId: await n(t),
195
+ category: r,
196
+ channel: i,
197
+ enabled: a
1317
198
  }), { ok: !0 };
1318
199
  })
1319
200
  },
1320
201
  {
1321
- ...f,
202
+ ...ue,
1322
203
  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 }))
204
+ execute: (e, t) => P.promise(async () => (await b.subscribe(e.topic, await n(t), Q(t)), { ok: !0 }))
1324
205
  },
1325
206
  {
1326
- ...ee,
207
+ ...pe,
1327
208
  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 }))
209
+ execute: (e, t) => P.promise(async () => (await b.unsubscribe(e.topic, await n(t)), { ok: !0 }))
1329
210
  },
1330
211
  {
1331
- ...d,
212
+ ...v,
1332
213
  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 } : {}
214
+ execute: (e, t) => P.promise(async () => {
215
+ let r = e;
216
+ return await b.setQuietHours({
217
+ subjectId: await n(t),
218
+ startMinute: r.startMinute,
219
+ endMinute: r.endMinute,
220
+ ...r.tz ? { tz: r.tz } : {},
221
+ ...r.policy ? { policy: r.policy } : {}
1341
222
  }), { ok: !0 };
1342
223
  })
1343
224
  },
1344
225
  {
1345
- ...n,
226
+ ...te,
1346
227
  description: "Clear the caller's quiet-hours window.",
1347
- execute: (e, t) => g.promise(async () => (await E.clearQuietHours(await h(t)), { ok: !0 }))
228
+ execute: (e, t) => P.promise(async () => (await b.clearQuietHours(await n(t)), { ok: !0 }))
1348
229
  }
1349
- ], j = T ? [
230
+ ], C = y ? [
1350
231
  {
1351
- ...te,
232
+ ...me,
1352
233
  description: "The VAPID application-server key a subscribing browser needs.",
1353
- execute: () => g.sync(() => ({ key: nt(q().channelOptions?.privateKey) }))
234
+ execute: () => P.sync(() => ({ key: M(A().channelOptions?.privateKey) }))
1354
235
  },
1355
236
  {
1356
- ...ie,
237
+ ..._e,
1357
238
  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)
239
+ execute: (e, t) => P.promise(async () => {
240
+ let r = e;
241
+ return await A().store.upsert({
242
+ subjectId: await n(t),
243
+ endpoint: r.endpoint,
244
+ endpointHash: E(r.endpoint),
245
+ p256dh: r.p256dh,
246
+ auth: r.auth,
247
+ ua: r.ua ?? null,
248
+ tenantId: Q(t)
1368
249
  }), { ok: !0 };
1369
250
  })
1370
251
  },
1371
252
  {
1372
- ...ae,
253
+ ...ve,
1373
254
  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)) }))
255
+ execute: (e, t) => P.promise(async () => ({ ok: await A().store.removeForSubject(E(e.endpoint), await n(t)) }))
1375
256
  },
1376
257
  {
1377
- ...re,
258
+ ...ge,
1378
259
  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)) }))
260
+ execute: (e, t) => P.promise(async () => ({ endpoints: await A().store.countForSubject(await n(t)) }))
1380
261
  }
1381
- ] : [], M = (e) => ({
262
+ ] : [], w = (e) => ({
1382
263
  kind: "json",
1383
264
  data: e
1384
- }), fe = (e, t) => ({
265
+ }), T = (e, t) => ({
1385
266
  kind: "json",
1386
267
  status: e,
1387
268
  data: { error: t }
1388
- }), N = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), pe = [{
269
+ }), D = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), k = [{
1389
270
  method: "GET",
1390
271
  path: "/deliveries",
1391
272
  scope: "process",
1392
273
  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;
274
+ handler: () => P.promise(async () => {
275
+ let e = await p.listDeliveries({ limit: 200 }), t = {}, n = 0, r = 0;
1395
276
  for (let i of e) t[i.channel] = (t[i.channel] ?? 0) + 1, i.status === "sent" ? n++ : i.status === "failed" && r++;
1396
277
  let i = e.slice(0, 100).map(({ clickToken: e, ...t }) => t);
1397
- return M({
278
+ return w({
1398
279
  deliveries: i,
1399
- channels: C.map((e) => e.id),
280
+ channels: h.map((e) => e.id),
1400
281
  stats: {
1401
282
  sent: n,
1402
283
  failed: r,
@@ -1410,29 +291,29 @@ var I = class extends _.TaggedError()("PushTokenRejected", {
1410
291
  scope: "process",
1411
292
  description: "In-app inbox for a subject (?subjectId=…).",
1412
293
  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"));
294
+ let t = D(e.url, "subjectId");
295
+ return t ? P.promise(async () => w({
296
+ items: await p.listInbox(t, { limit: 100 }),
297
+ unread: await p.unreadCount(t)
298
+ })) : P.succeed(T(400, "subjectId query param required"));
1418
299
  }
1419
300
  }];
1420
- return le({
1421
- privacy: It,
1422
- name: oe,
1423
- baseName: Ft,
301
+ return Ke({
302
+ privacy: Je,
303
+ name: r,
304
+ baseName: $,
1424
305
  description: "Unified notifications — email/slack/sms/push/in-app with preference routing, inbox + delivery log.",
1425
306
  permissions: ["inspect:read", "store:write"],
1426
307
  ...e.tables === !1 ? {} : { extendSchema: { tables: [
1427
- Dt,
1428
- Ot,
1429
- kt,
1430
- At,
1431
- jt,
1432
- Mt,
1433
- Ke
308
+ W,
309
+ G,
310
+ K,
311
+ q,
312
+ J,
313
+ Y,
314
+ O
1434
315
  ] } },
1435
- ...T ? {
316
+ ...y ? {
1436
317
  declaredEnv: [{
1437
318
  name: "VOLTRO_VAPID_PRIVATE_KEY",
1438
319
  required: !0,
@@ -1448,12 +329,12 @@ var I = class extends _.TaggedError()("PushTokenRejected", {
1448
329
  try {
1449
330
  t = JSON.parse(Buffer.from(e.rawBody).toString("utf8")).token;
1450
331
  } catch {}
1451
- if (typeof t != "string" || t === "" || !Ge(t, t)) return {
332
+ if (typeof t != "string" || t === "" || !ye(t, t)) return {
1452
333
  status: 400,
1453
334
  body: JSON.stringify({ error: "token required" }),
1454
335
  contentType: "application/json"
1455
336
  };
1456
- let n = await x.markClicked(t);
337
+ let n = await p.markClicked(t);
1457
338
  return {
1458
339
  status: n ? 200 : 404,
1459
340
  body: JSON.stringify({ ok: n }),
@@ -1463,27 +344,27 @@ var I = class extends _.TaggedError()("PushTokenRejected", {
1463
344
  maxBodyBytes: 4096
1464
345
  }]
1465
346
  } : {},
1466
- services: se.succeed($, E),
1467
- routes: [...A, ...j],
1468
- rpcClientDescriptors: T ? [...c, ...ne] : c,
1469
- inspectEndpoints: pe,
347
+ services: Ue.succeed(X, b),
348
+ routes: [...S, ...C],
349
+ rpcClientDescriptors: y ? [..._, ...he] : _,
350
+ inspectEndpoints: k,
1470
351
  bindDataStore: (t) => {
1471
- y = t, _ && (v.current = De(t)), Ye(t);
352
+ o = t, i && (a.current = l(t)), xe(t);
1472
353
  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
354
+ x = setInterval(() => {
355
+ b.flushDue().catch(() => {});
356
+ }, n), typeof x.unref == "function" && x.unref();
357
+ },
358
+ onActivate: (e) => P.sync(() => {
359
+ y && be(), e.logger.info("notifications active", {
360
+ channels: h.map((e) => e.id),
361
+ digestWindowMs: g
1481
362
  });
1482
363
  }),
1483
- onDeactivate: () => g.sync(() => {
1484
- k && clearInterval(k), k = void 0;
364
+ onDeactivate: () => P.sync(() => {
365
+ x && clearInterval(x), x = void 0;
1485
366
  })
1486
367
  });
1487
368
  };
1488
369
  //#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 };
370
+ export { De as APNS_HOSTS, Oe as APNS_TOKEN_REJECTIONS, ze as EXPO_PUSH_ENDPOINT, Me as FCM_SCOPE, N as FCM_TOKEN_REJECTIONS, Ne as GOOGLE_TOKEN_URI, we as NOTIFICATION_SEND_EFFECT, X as NotificationService, e as PushTokenRejected, ke as apnsBody, Ae as apnsProviderToken, je as apnsTransport, d as buildNotificationService, t as consoleChannel, n as customChannel, l as dataStoreNotificationStore, T as dataStorePushSubscriptionStore, y as decryptWebPushPayload, Se as defineNotification, K as deliveriesTable, r as emailChannel, b as encryptWebPushPayload, E as endpointHashOf, Te as enqueueNotification, Be as expoPushTransport, Pe as fcmAssertion, Fe as fcmMessage, Ie as fcmSendEndpoint, Le as fcmTransport, x as generateVapidPrivateKey, Y as heldTable, f as inAppChannel, p as inQuietHours, W as inboxTable, Ve as isExpoPushToken, Z as makeSubjectId, u as memoryNotificationStore, D as memoryPushSubscriptionStore, m as minuteOfDayInZone, Ee as notificationOutboxHandler, Ye as notificationsPlugin, Re as parseServiceAccount, G as preferencesTable, i as pushChannel, O as pushSubscriptionsTable, h as quietHoursEnd, J as quietHoursTable, k as resetWebPushStoreForTest, a as resolveChannels, o as richPushOf, Ce as sendNotification, S as sendWebPush, s as smsChannel, q as topicSubscriptionsTable, C as vapidAuthorization, w as vapidPublicKeyFor, j as webPushChannel, M as webPushPublicKey, c as webhookChannel };