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