@voltro/plugin-notifications 0.52.0 → 0.53.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,18 @@
1
- import { archiveDescriptor as e, clearQuietHoursDescriptor as t, inboxDescriptor as n, markAllReadDescriptor as r, markReadDescriptor as i, markUnreadDescriptor as a, notificationsRpcClientImports as o, preferencesDescriptor as s, setPreferenceDescriptor as c, setQuietHoursDescriptor as l, subscribeDescriptor as u, unarchiveDescriptor as ee, unreadCountDescriptor as te, unsubscribeDescriptor as ne } from "./rpc.js";
2
- import { Context as d, Effect as f, Layer as p, Schema as m } from "effect";
3
- import { and as h, boolean as g, eq as _, generateId as v, id as y, integer as b, isNotNull as re, isNull as x, json as S, lte as C, registerRetention as w, retentionTtlMsFromEnv as T, table as E, text as D, timestamp as O } from "@voltro/database";
4
- import { definePlugin as ie, pluginInstanceName as ae } from "@voltro/protocol";
5
- import { randomUUID as k } from "node:crypto";
1
+ import { archiveDescriptor as e, clearQuietHoursDescriptor as t, inboxDescriptor as n, markAllReadDescriptor as r, markReadDescriptor as i, markUnreadDescriptor as a, notificationsRpcClientImports as o, preferencesDescriptor as s, setPreferenceDescriptor as c, setQuietHoursDescriptor as l, subscribeDescriptor as u, unarchiveDescriptor as d, unreadCountDescriptor as f, unsubscribeDescriptor as p, webPushPublicKeyDescriptor as m, webPushRpcClientImports as ee, webPushStatusDescriptor as te, webPushSubscribeDescriptor as ne, webPushUnsubscribeDescriptor as re } from "./rpc.js";
2
+ import { Context as h, Effect as g, Layer as ie, 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 ae, 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 oe, pluginInstanceName as se } from "@voltro/protocol";
5
+ import { createCipheriv as M, createDecipheriv as ce, createECDH as N, createHash as le, createHmac as ue, createPrivateKey as de, randomBytes as fe, randomUUID as P, sign as pe, timingSafeEqual as me } from "node:crypto";
6
6
  //#region src/errors.ts
7
- var A = class extends m.TaggedError()("PushTokenRejected", {
8
- token: m.String,
9
- reason: m.String
10
- }) {}, j = () => ({
7
+ var F = class extends _.TaggedError()("PushTokenRejected", {
8
+ token: _.String,
9
+ reason: _.String
10
+ }) {}, I = () => ({
11
11
  id: "console",
12
12
  deliver: async (e) => {
13
13
  process.stdout.write(`[notify:${e.category}] → ${e.to}: ${e.title} — ${e.body}\n`);
14
14
  }
15
- }), oe = (e) => ({
15
+ }), he = (e) => ({
16
16
  id: e.id ?? "slack",
17
17
  deliver: async (t) => {
18
18
  let n = e.format ? e.format(t) : { text: `*${t.title}*\n${t.body}` }, r = await fetch(e.url, {
@@ -25,7 +25,7 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
25
25
  });
26
26
  if (!r.ok) throw Error(`webhook ${e.id ?? "slack"} POST ${r.status}`);
27
27
  }
28
- }), M = (e) => ({
28
+ }), ge = (e) => ({
29
29
  id: "email",
30
30
  deliver: (t) => e({
31
31
  to: t.to,
@@ -33,38 +33,58 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
33
33
  html: `<p>${t.body}</p>`,
34
34
  text: t.body
35
35
  })
36
- }), N = (e) => ({
36
+ }), _e = (e) => ({
37
37
  id: "sms",
38
38
  deliver: (t) => e(t.to, `${t.title}: ${t.body}`)
39
- }), P = (e, t) => ({
39
+ }), ve = (e, t) => ({
40
40
  id: e,
41
41
  deliver: t
42
- }), se = (e) => ({
43
- id: e.id ?? "push",
44
- deliver: async (t) => {
45
- let n = await e.tokensFor(t.to), r = t.data?.badge, i = typeof r == "number" ? r : void 0;
46
- for (let r of n) {
47
- let n = {
48
- token: r,
49
- title: t.title,
50
- body: t.body,
51
- ...i === void 0 ? {} : { badge: i },
52
- data: t.data ?? {}
42
+ }), ye = (e) => {
43
+ let t = async (t, n, r) => {
44
+ let i = {
45
+ token: n,
46
+ title: t.title,
47
+ body: t.body,
48
+ ...r === void 0 ? {} : { badge: r },
49
+ data: t.data ?? {}
50
+ };
51
+ try {
52
+ return await e.transport(i), {
53
+ endpoint: n,
54
+ status: "sent"
55
+ };
56
+ } catch (t) {
57
+ let r = t instanceof F, i = r ? t.reason : t instanceof Error ? t.message : String(t);
58
+ if (r && e.onTokenRejected !== void 0) try {
59
+ await e.onTokenRejected(n, i);
60
+ } catch {}
61
+ return {
62
+ endpoint: n,
63
+ status: "failed",
64
+ error: i,
65
+ ...r ? { pruned: e.onTokenRejected !== void 0 } : {}
53
66
  };
54
- try {
55
- await e.transport(n);
56
- } catch (e) {
57
- throw e instanceof A ? e : new A({
58
- token: r,
59
- reason: e instanceof Error ? e.message : String(e)
60
- });
61
- }
62
67
  }
63
- }
64
- }), F = (e, t, n, r, i) => {
68
+ }, n = async (n) => {
69
+ let r = await e.tokensFor(n.to), i = n.data?.badge, a = typeof i == "number" ? i : void 0, o = [];
70
+ for (let e of r) o.push(await t(n, e, a));
71
+ return o;
72
+ };
73
+ return {
74
+ id: e.id ?? "push",
75
+ deliver: async (e) => {
76
+ let t = await n(e), r = t.filter((e) => e.status === "failed");
77
+ if (t.length > 0 && r.length === t.length) throw new F({
78
+ token: t[0].endpoint,
79
+ reason: r[0].error ?? "all tokens rejected"
80
+ });
81
+ },
82
+ deliverDetailed: n
83
+ };
84
+ }, L = (e, t, n, r, i) => {
65
85
  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));
66
86
  return a.filter((e) => !o.has(e));
67
- }, I = () => {
87
+ }, R = () => {
68
88
  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}`;
69
89
  return {
70
90
  addInbox: async (t) => {
@@ -121,6 +141,13 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
121
141
  n.unshift(e);
122
142
  },
123
143
  listDeliveries: async (e) => e?.limit === void 0 ? [...n] : n.slice(0, e.limit),
144
+ markClicked: async (e) => {
145
+ let t = n.findIndex((t) => t.clickToken === e);
146
+ return t !== -1 && (n[t] = {
147
+ ...n[t],
148
+ clickedAt: (/* @__PURE__ */ new Date()).toISOString()
149
+ }, !0);
150
+ },
124
151
  subscribeTopic: async (e) => {
125
152
  r.set(s(e.topic, e.subjectId), e);
126
153
  },
@@ -145,7 +172,7 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
145
172
  for (let e = a.length - 1; e >= 0; e--) t.has(a[e].id) && a.splice(e, 1);
146
173
  }
147
174
  };
148
- }, L = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "", R = (e) => {
175
+ }, z = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "", be = (e) => {
149
176
  let t = async (t, n, r, i) => await e.query({
150
177
  table: t,
151
178
  predicate: n,
@@ -160,29 +187,27 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
160
187
  title: String(e.title),
161
188
  body: String(e.body),
162
189
  data: e.data ?? {},
163
- readAt: e.readAt == null ? null : L(e.readAt),
164
- archivedAt: e.archivedAt == null ? null : L(e.archivedAt),
165
- createdAt: L(e.createdAt),
190
+ readAt: e.readAt == null ? null : z(e.readAt),
191
+ archivedAt: e.archivedAt == null ? null : z(e.archivedAt),
192
+ createdAt: z(e.createdAt),
166
193
  tenantId: e.tenantId ?? null
167
194
  }), r = (e) => ({
168
195
  subjectId: String(e.subjectId),
169
196
  category: String(e.category),
170
197
  channel: String(e.channel),
171
198
  enabled: !!e.enabled
172
- }), i = (e) => {
173
- let t = {
174
- id: String(e.id),
175
- to: String(e.recipient),
176
- category: String(e.category),
177
- channel: String(e.channel),
178
- status: e.status,
179
- at: L(e.at)
180
- };
181
- return e.error == null ? t : {
182
- ...t,
183
- error: String(e.error)
184
- };
185
- }, a = (e) => ({
199
+ }), i = (e) => ({
200
+ id: String(e.id),
201
+ to: String(e.recipient),
202
+ category: String(e.category),
203
+ channel: String(e.channel),
204
+ status: e.status,
205
+ at: z(e.at),
206
+ ...e.error == null ? {} : { error: String(e.error) },
207
+ ...e.endpoint == null ? {} : { endpoint: String(e.endpoint) },
208
+ ...e.clickToken == null ? {} : { clickToken: String(e.clickToken) },
209
+ ...e.clickedAt == null ? {} : { clickedAt: z(e.clickedAt) }
210
+ }), a = (e) => ({
186
211
  topic: String(e.topic),
187
212
  subjectId: String(e.subjectId),
188
213
  tenantId: e.tenantId ?? null
@@ -197,7 +222,7 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
197
222
  kind: e.kind === "quiet" ? "quiet" : "digest",
198
223
  subjectId: String(e.subjectId),
199
224
  send: e.send ?? {},
200
- flushAt: L(e.flushAt)
225
+ flushAt: z(e.flushAt)
201
226
  });
202
227
  return {
203
228
  addInbox: async (t) => {
@@ -215,24 +240,24 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
215
240
  });
216
241
  },
217
242
  listInbox: async (e, r) => {
218
- let i = r?.unreadOnly ? h(_("subjectId", e), x("readAt")) : _("subjectId", e);
243
+ let i = r?.unreadOnly ? v(b("subjectId", e), w("readAt")) : b("subjectId", e);
219
244
  return (await t("_voltro_notification_inbox", i, [{
220
245
  column: "createdAt",
221
246
  direction: "desc"
222
247
  }], r?.limit)).map(n);
223
248
  },
224
- markRead: async (n, r) => (await t("_voltro_notification_inbox", h(_("id", n), _("subjectId", r)), [], 1))[0] ? (await e.update("_voltro_notification_inbox", n, { readAt: (/* @__PURE__ */ new Date()).toISOString() }), !0) : !1,
225
- markUnread: async (n, r) => (await t("_voltro_notification_inbox", h(_("id", n), _("subjectId", r)), [], 1))[0] ? (await e.update("_voltro_notification_inbox", n, { readAt: null }), !0) : !1,
249
+ 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,
250
+ 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,
226
251
  markAllRead: async (n) => {
227
- let r = await t("_voltro_notification_inbox", h(_("subjectId", n), x("readAt")), [], void 0), i = (/* @__PURE__ */ new Date()).toISOString();
252
+ let r = await t("_voltro_notification_inbox", v(b("subjectId", n), w("readAt")), [], void 0), i = (/* @__PURE__ */ new Date()).toISOString();
228
253
  for (let t of r) await e.update("_voltro_notification_inbox", String(t.id), { readAt: i });
229
254
  return r.length;
230
255
  },
231
- setArchived: async (n, r, i) => (await t("_voltro_notification_inbox", h(_("id", n), _("subjectId", r)), [], 1))[0] ? (await e.update("_voltro_notification_inbox", n, { archivedAt: i ? (/* @__PURE__ */ new Date()).toISOString() : null }), !0) : !1,
232
- unreadCount: async (e) => (await t("_voltro_notification_inbox", h(_("subjectId", e), x("readAt")), [], void 0)).length,
233
- getPreferences: async (e) => (await t("_voltro_notification_preferences", _("subjectId", e), [], void 0)).map(r),
256
+ 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,
257
+ unreadCount: async (e) => (await t("_voltro_notification_inbox", v(b("subjectId", e), w("readAt")), [], void 0)).length,
258
+ getPreferences: async (e) => (await t("_voltro_notification_preferences", b("subjectId", e), [], void 0)).map(r),
234
259
  setPreference: async (t) => {
235
- let n = v({
260
+ let n = x({
236
261
  kind: "typeid",
237
262
  prefix: "notifpref"
238
263
  }, "_voltro_notification_preferences");
@@ -259,15 +284,22 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
259
284
  channel: t.channel,
260
285
  status: t.status,
261
286
  error: t.error ?? null,
262
- at: t.at
287
+ at: t.at,
288
+ endpoint: t.endpoint ?? null,
289
+ clickToken: t.clickToken ?? null,
290
+ clickedAt: t.clickedAt ?? null
263
291
  });
264
292
  },
293
+ markClicked: async (n) => {
294
+ let r = await t("_voltro_notification_deliveries", b("clickToken", n), [], 1);
295
+ return r[0] ? (await e.update("_voltro_notification_deliveries", String(r[0].id), { clickedAt: (/* @__PURE__ */ new Date()).toISOString() }), !0) : !1;
296
+ },
265
297
  listDeliveries: async (e) => (await t("_voltro_notification_deliveries", void 0, [{
266
298
  column: "at",
267
299
  direction: "desc"
268
300
  }], e?.limit)).map(i),
269
301
  subscribeTopic: async (t) => {
270
- let n = v({
302
+ let n = x({
271
303
  kind: "typeid",
272
304
  prefix: "notiftsub"
273
305
  }, "_voltro_notification_topic_subscriptions");
@@ -282,16 +314,16 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
282
314
  });
283
315
  },
284
316
  unsubscribeTopic: async (n, r) => {
285
- let i = await t("_voltro_notification_topic_subscriptions", h(_("topic", n), _("subjectId", r)), [], 1);
317
+ let i = await t("_voltro_notification_topic_subscriptions", v(b("topic", n), b("subjectId", r)), [], 1);
286
318
  i[0] && await e.delete("_voltro_notification_topic_subscriptions", String(i[0].id));
287
319
  },
288
- listTopicSubscribers: async (e) => (await t("_voltro_notification_topic_subscriptions", _("topic", e), [], void 0)).map(a),
320
+ listTopicSubscribers: async (e) => (await t("_voltro_notification_topic_subscriptions", b("topic", e), [], void 0)).map(a),
289
321
  getQuietHours: async (e) => {
290
- let n = await t("_voltro_notification_quiet_hours", _("subjectId", e), [], 1);
322
+ let n = await t("_voltro_notification_quiet_hours", b("subjectId", e), [], 1);
291
323
  return n[0] ? o(n[0]) : null;
292
324
  },
293
325
  setQuietHours: async (t) => {
294
- let n = v({
326
+ let n = x({
295
327
  kind: "typeid",
296
328
  prefix: "notifqh"
297
329
  }, "_voltro_notification_quiet_hours");
@@ -313,7 +345,7 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
313
345
  });
314
346
  },
315
347
  clearQuietHours: async (n) => {
316
- let r = await t("_voltro_notification_quiet_hours", _("subjectId", n), [], 1);
348
+ let r = await t("_voltro_notification_quiet_hours", b("subjectId", n), [], 1);
317
349
  r[0] && await e.delete("_voltro_notification_quiet_hours", String(r[0].id));
318
350
  },
319
351
  enqueueHeld: async (t) => {
@@ -325,11 +357,11 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
325
357
  flushAt: t.flushAt
326
358
  });
327
359
  },
328
- pendingDigest: async (e) => (await t("_voltro_notification_held", h(_("kind", "digest"), _("subjectId", e)), [{
360
+ pendingDigest: async (e) => (await t("_voltro_notification_held", v(b("kind", "digest"), b("subjectId", e)), [{
329
361
  column: "flushAt",
330
362
  direction: "asc"
331
363
  }], void 0)).map(s),
332
- dueHeld: async (e) => (await t("_voltro_notification_held", C("flushAt", e), [{
364
+ dueHeld: async (e) => (await t("_voltro_notification_held", E("flushAt", e), [{
333
365
  column: "flushAt",
334
366
  direction: "asc"
335
367
  }], void 0)).map(s),
@@ -337,11 +369,11 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
337
369
  for (let n of t) await e.delete("_voltro_notification_held", n);
338
370
  }
339
371
  };
340
- }, z = (e) => ({
372
+ }, xe = (e) => ({
341
373
  id: "inApp",
342
374
  deliver: async (t) => {
343
375
  await e.addInbox({
344
- id: k(),
376
+ id: P(),
345
377
  subjectId: t.to,
346
378
  category: t.category,
347
379
  title: t.title,
@@ -363,16 +395,16 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
363
395
  } catch {
364
396
  return e.getUTCHours() * 60 + e.getUTCMinutes();
365
397
  }
366
- }, V = (e, t) => {
398
+ }, Se = (e, t) => {
367
399
  if (e.startMinute === e.endMinute) return !1;
368
400
  let n = B(t, e.tz ?? "UTC");
369
401
  return e.startMinute < e.endMinute ? n >= e.startMinute && n < e.endMinute : n >= e.startMinute || n < e.endMinute;
370
- }, H = (e, t) => {
402
+ }, Ce = (e, t) => {
371
403
  let n = B(t, e.tz ?? "UTC"), r = e.endMinute - n;
372
404
  return r <= 0 && (r += 1440), new Date(t.getTime() + r * 6e4);
373
- }, U = (e) => {
405
+ }, we = (e) => {
374
406
  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) => {
375
- let a = await e.store.getPreferences(i.to), o = F(n, i.category, i.channels, a, i.to), s = n.filter((e) => !o.includes(e)), c = [], l = [], u = {
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 = {
376
408
  to: i.to,
377
409
  category: i.category,
378
410
  title: i.title,
@@ -383,8 +415,44 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
383
415
  for (let n of o) {
384
416
  let a = t.get(n);
385
417
  if (!a) continue;
418
+ if (a.deliverDetailed !== void 0) {
419
+ try {
420
+ let t = await a.deliverDetailed(u);
421
+ for (let a of t) await e.store.recordDelivery({
422
+ id: P(),
423
+ to: i.to,
424
+ category: i.category,
425
+ channel: n,
426
+ at: r(),
427
+ status: a.status,
428
+ endpoint: a.endpoint,
429
+ ...a.error === void 0 ? {} : { error: a.error },
430
+ ...a.clickToken === void 0 ? {} : { clickToken: a.clickToken }
431
+ });
432
+ let o = t.filter((e) => e.status === "failed");
433
+ t.length === 0 || o.length < t.length ? c.push(n) : l.push({
434
+ channel: n,
435
+ error: o[0]?.error ?? "all endpoints failed"
436
+ });
437
+ } catch (t) {
438
+ let a = t instanceof Error ? t.message : String(t);
439
+ l.push({
440
+ channel: n,
441
+ error: a
442
+ }), await e.store.recordDelivery({
443
+ id: P(),
444
+ to: i.to,
445
+ category: i.category,
446
+ channel: n,
447
+ at: r(),
448
+ status: "failed",
449
+ error: a
450
+ });
451
+ }
452
+ continue;
453
+ }
386
454
  let o = {
387
- id: k(),
455
+ id: P(),
388
456
  to: i.to,
389
457
  category: i.category,
390
458
  channel: n,
@@ -414,17 +482,17 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
414
482
  };
415
483
  }, o = async (t) => {
416
484
  let n = await e.store.getQuietHours(t.to), o = new Date(r());
417
- if (n && V(n, o)) return (n.policy ?? "hold") === "drop" ? {
485
+ if (n && Se(n, o)) return (n.policy ?? "hold") === "drop" ? {
418
486
  delivered: [],
419
487
  failed: [],
420
488
  skipped: [],
421
489
  held: "quiet-drop"
422
490
  } : (await e.store.enqueueHeld({
423
- id: k(),
491
+ id: P(),
424
492
  kind: "quiet",
425
493
  subjectId: t.to,
426
494
  send: t,
427
- flushAt: H(n, o).toISOString()
495
+ flushAt: Ce(n, o).toISOString()
428
496
  }), {
429
497
  delivered: [],
430
498
  failed: [],
@@ -434,7 +502,7 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
434
502
  if (i > 0 && !t.channels) {
435
503
  let n = (await e.store.pendingDigest(t.to))[0]?.flushAt ?? new Date(o.getTime() + i).toISOString();
436
504
  return await e.store.enqueueHeld({
437
- id: k(),
505
+ id: P(),
438
506
  kind: "digest",
439
507
  subjectId: t.to,
440
508
  send: t,
@@ -488,11 +556,11 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
488
556
  let t = i.get(e.subjectId) ?? [];
489
557
  t.push(e), i.set(e.subjectId, t);
490
558
  }
491
- for (let [e, t] of i) await a(W(e, t));
559
+ for (let [e, t] of i) await a(Te(e, t));
492
560
  return await e.store.clearHeld(n.map((e) => e.id)), n.length;
493
561
  }
494
562
  };
495
- }, W = (e, t) => {
563
+ }, Te = (e, t) => {
496
564
  if (t.length === 1) return t[0].send;
497
565
  let n = t.map((e) => ({
498
566
  category: e.send.category,
@@ -507,159 +575,423 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
507
575
  data: { items: n },
508
576
  tenantId: t[0].send.tenantId ?? null
509
577
  };
510
- }, G = E("_voltro_notification_inbox", {
511
- id: y({ prefix: "notif" }),
512
- subjectId: D(),
513
- category: D(),
514
- title: D(),
515
- body: D(),
516
- data: S(),
517
- readAt: O().nullable(),
518
- archivedAt: O().nullable(),
519
- createdAt: O().default("now"),
520
- tenantId: D().nullable()
521
- }).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }), K = E("_voltro_notification_preferences", {
522
- id: y({ prefix: "notifpref" }),
523
- subjectId: D(),
524
- category: D(),
525
- channel: D(),
526
- enabled: g().default(!0)
578
+ }, V = (e) => e.toString("base64url"), H = (e) => Buffer.from(e, "base64url"), U = (e, t) => ue("sha256", e).update(t).digest(), W = (e, t, n) => U(e, Buffer.concat([t, Buffer.from([1])])).subarray(0, n), Ee = () => {
579
+ let e = N("prime256v1");
580
+ return e.generateKeys(), V(e.getPrivateKey());
581
+ }, G = (e) => {
582
+ let t = H(e);
583
+ if (t.length !== 32) throw Error(`VOLTRO_VAPID_PRIVATE_KEY must be a base64url 32-byte P-256 scalar (got ${t.length} bytes)`);
584
+ let n = N("prime256v1");
585
+ return n.setPrivateKey(t), V(n.getPublicKey());
586
+ }, De = (e) => {
587
+ let t = H(e), n = N("prime256v1");
588
+ n.setPrivateKey(t);
589
+ let r = n.getPublicKey();
590
+ return de({
591
+ key: {
592
+ kty: "EC",
593
+ crv: "P-256",
594
+ d: V(t),
595
+ x: V(r.subarray(1, 33)),
596
+ y: V(r.subarray(33, 65))
597
+ },
598
+ format: "jwk"
599
+ });
600
+ }, K = (e, t, n, r = Date.now()) => {
601
+ let i = new URL(e), a = {
602
+ aud: `${i.protocol}//${i.host}`,
603
+ exp: Math.floor(r / 1e3) + 43200,
604
+ ...n === void 0 ? {} : { sub: n }
605
+ }, o = `${V(Buffer.from(JSON.stringify({
606
+ typ: "JWT",
607
+ alg: "ES256"
608
+ })))}.${V(Buffer.from(JSON.stringify(a)))}`;
609
+ return `vapid t=${o}.${V(pe("sha256", Buffer.from(o), {
610
+ key: De(t),
611
+ dsaEncoding: "ieee-p1363"
612
+ }))}, k=${G(t)}`;
613
+ }, Oe = Buffer.from("WebPush: info\0"), ke = Buffer.from("Content-Encoding: aes128gcm\0"), Ae = Buffer.from("Content-Encoding: nonce\0"), je = 4096, Me = (e, t, n = {}) => {
614
+ let r = H(e.p256dh), i = H(e.auth);
615
+ if (r.length !== 65 || r[0] !== 4) throw Error("push subscription p256dh must be a 65-byte uncompressed P-256 point");
616
+ if (i.length !== 16) throw Error("push subscription auth must be 16 bytes");
617
+ let a = N("prime256v1");
618
+ n.asPrivate ? a.setPrivateKey(n.asPrivate) : a.generateKeys();
619
+ let o = a.getPublicKey(), s = W(U(i, a.computeSecret(r)), Buffer.concat([
620
+ Oe,
621
+ r,
622
+ o
623
+ ]), 32), c = n.salt ?? fe(16), l = U(c, s), u = W(l, ke, 16), d = W(l, Ae, 12), f = M("aes-128-gcm", u, d), p = Buffer.concat([
624
+ f.update(Buffer.concat([t, Buffer.from([2])])),
625
+ f.final(),
626
+ f.getAuthTag()
627
+ ]), m = Buffer.alloc(21);
628
+ return c.copy(m, 0), m.writeUInt32BE(je, 16), m.writeUInt8(o.length, 20), Buffer.concat([
629
+ m,
630
+ o,
631
+ p
632
+ ]);
633
+ }, Ne = (e, t) => {
634
+ let n = e.subarray(0, 16), r = e.readUInt8(20), i = e.subarray(21, 21 + r), a = e.subarray(21 + r), o = N("prime256v1");
635
+ o.setPrivateKey(t.uaPrivate);
636
+ let s = o.getPublicKey(), c = o.computeSecret(i), l = U(n, W(U(t.authSecret, c), Buffer.concat([
637
+ Oe,
638
+ s,
639
+ i
640
+ ]), 32)), u = W(l, ke, 16), d = W(l, Ae, 12), f = ce("aes-128-gcm", u, d);
641
+ f.setAuthTag(a.subarray(a.length - 16));
642
+ let p = Buffer.concat([f.update(a.subarray(0, a.length - 16)), f.final()]), m = p.length - 1;
643
+ for (; m >= 0 && p[m] === 0;) m--;
644
+ if (p[m] !== 2) throw Error("web-push record missing the last-record delimiter");
645
+ return p.subarray(0, m);
646
+ }, Pe = async (e, t, n) => {
647
+ let r = Me(e.keys, t), i = n.fetchImpl ?? fetch, a;
648
+ try {
649
+ a = await i(e.endpoint, {
650
+ method: "POST",
651
+ headers: {
652
+ "content-encoding": "aes128gcm",
653
+ "content-type": "application/octet-stream",
654
+ ttl: String(n.ttl ?? 86400),
655
+ urgency: "normal",
656
+ authorization: K(e.endpoint, n.privateKey, n.contact)
657
+ },
658
+ body: new Uint8Array(r)
659
+ });
660
+ } catch (e) {
661
+ return {
662
+ kind: "failed",
663
+ status: 0,
664
+ detail: e instanceof Error ? e.message : String(e)
665
+ };
666
+ }
667
+ if (a.status === 404 || a.status === 410) return {
668
+ kind: "gone",
669
+ status: a.status
670
+ };
671
+ if (a.status >= 200 && a.status < 300) return {
672
+ kind: "delivered",
673
+ status: a.status
674
+ };
675
+ let o = await a.text().catch(() => "");
676
+ return {
677
+ kind: "failed",
678
+ status: a.status,
679
+ detail: o.slice(0, 200)
680
+ };
681
+ }, Fe = (e, t) => {
682
+ let n = Buffer.from(e), r = Buffer.from(t);
683
+ return n.length === r.length && me(n, r);
684
+ }, Ie = k("_voltro_notification_push_subscriptions", {
685
+ id: S({ prefix: "notifpush" }),
686
+ subjectId: A(),
687
+ endpointHash: A(),
688
+ endpoint: A(),
689
+ p256dh: A(),
690
+ auth: A(),
691
+ ua: A().nullable(),
692
+ createdAt: j().default("now"),
693
+ tenantId: A().nullable()
694
+ }).unique("byPushEndpointHash", ["endpointHash"]).index("byPushSubject", ["subjectId"]), q = (e) => le("sha256").update(e).digest("hex"), J = () => {
695
+ let e = /* @__PURE__ */ new Map();
696
+ return {
697
+ upsert: async (t) => {
698
+ e.set(t.endpointHash, t);
699
+ },
700
+ listForSubject: async (t) => [...e.values()].filter((e) => e.subjectId === t),
701
+ removeForSubject: async (t, n) => {
702
+ let r = e.get(t);
703
+ return !r || r.subjectId !== n ? !1 : (e.delete(t), !0);
704
+ },
705
+ removeByHash: async (t) => {
706
+ e.delete(t);
707
+ },
708
+ countForSubject: async (t) => [...e.values()].filter((e) => e.subjectId === t).length
709
+ };
710
+ }, Y = "_voltro_notification_push_subscriptions", Le = (e) => {
711
+ let t = async (t, n) => e.query({
712
+ table: Y,
713
+ predicate: t,
714
+ order: [],
715
+ take: n,
716
+ skip: void 0,
717
+ projection: void 0
718
+ }), n = (e) => ({
719
+ subjectId: String(e.subjectId),
720
+ endpoint: String(e.endpoint),
721
+ endpointHash: String(e.endpointHash),
722
+ p256dh: String(e.p256dh),
723
+ auth: String(e.auth),
724
+ ua: e.ua == null ? null : String(e.ua),
725
+ tenantId: e.tenantId == null ? null : String(e.tenantId)
726
+ });
727
+ return {
728
+ upsert: async (t) => {
729
+ let n = x({
730
+ kind: "typeid",
731
+ prefix: "notifpush"
732
+ }, Y);
733
+ await e.upsert(Y, {
734
+ id: n,
735
+ subjectId: t.subjectId,
736
+ endpointHash: t.endpointHash,
737
+ endpoint: t.endpoint,
738
+ p256dh: t.p256dh,
739
+ auth: t.auth,
740
+ ua: t.ua,
741
+ tenantId: t.tenantId
742
+ }, {
743
+ conflictColumns: ["endpointHash"],
744
+ update: [
745
+ "subjectId",
746
+ "p256dh",
747
+ "auth",
748
+ "ua",
749
+ "tenantId"
750
+ ]
751
+ });
752
+ },
753
+ listForSubject: async (e) => (await t(b("subjectId", e))).map(n),
754
+ removeForSubject: async (n, r) => {
755
+ let i = await t(v(b("endpointHash", n), b("subjectId", r)), 1);
756
+ return i[0] ? (await e.delete(Y, String(i[0].id)), !0) : !1;
757
+ },
758
+ removeByHash: async (n) => {
759
+ let r = await t(b("endpointHash", n), 1);
760
+ r[0] && await e.delete(Y, String(r[0].id));
761
+ },
762
+ countForSubject: async (e) => (await t(b("subjectId", e))).length
763
+ };
764
+ }, Re = Symbol.for("@voltro/plugin-notifications:web-push"), X = () => {
765
+ let e = globalThis;
766
+ return e[Re] ??= {
767
+ store: J(),
768
+ bound: !1
769
+ };
770
+ }, ze = (e) => {
771
+ let t = X();
772
+ t.store = Le(e), t.bound = !0;
773
+ }, Be = () => {
774
+ let e = X();
775
+ e.store = J(), e.bound = !1;
776
+ }, Z = (e) => {
777
+ let t = e ?? process.env.VOLTRO_VAPID_PRIVATE_KEY;
778
+ 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.");
779
+ return t;
780
+ }, Q = 3800, Ve = (e, t) => {
781
+ let n = typeof e.data?.url == "string" ? e.data.url : void 0, r = {
782
+ title: e.title,
783
+ body: e.body,
784
+ category: e.category,
785
+ clickToken: t,
786
+ ...n === void 0 ? {} : { url: n },
787
+ ...e.data === void 0 ? {} : { data: e.data }
788
+ }, i = Buffer.from(JSON.stringify(r));
789
+ if (i.length <= Q) return i;
790
+ let { data: a, ...o } = r;
791
+ if (i = Buffer.from(JSON.stringify(o)), i.length <= Q) return i;
792
+ 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, "");
793
+ return Buffer.from(JSON.stringify({
794
+ ...o,
795
+ body: `${l}…`
796
+ }));
797
+ }, He = (e = {}) => (X().channelOptions = e, We(e)), Ue = () => {
798
+ let e = X().channelOptions;
799
+ G(Z(e?.privateKey));
800
+ }, We = (e) => ({
801
+ id: e.id ?? "webPush",
802
+ deliver: async (t) => {
803
+ let n = await Ge(t, e), r = n.filter((e) => e.status === "failed");
804
+ if (n.length > 0 && r.length === n.length) throw Error(`web push failed for all ${r.length} endpoint(s): ${r[0].error ?? "unknown"}`);
805
+ },
806
+ deliverDetailed: (t) => Ge(t, e)
807
+ }), Ge = async (e, t) => {
808
+ let n = Z(t.privateKey), r = await X().store.listForSubject(e.to), i = [];
809
+ for (let a of r) {
810
+ let r = fe(16).toString("base64url"), o = {
811
+ privateKey: n,
812
+ ...t.contact === void 0 ? {} : { contact: t.contact },
813
+ ...t.ttl === void 0 ? {} : { ttl: t.ttl },
814
+ ...t.fetchImpl === void 0 ? {} : { fetchImpl: t.fetchImpl }
815
+ }, s = await Pe({
816
+ endpoint: a.endpoint,
817
+ keys: {
818
+ p256dh: a.p256dh,
819
+ auth: a.auth
820
+ }
821
+ }, Ve(e, r), o);
822
+ s.kind === "delivered" ? i.push({
823
+ endpoint: a.endpoint,
824
+ status: "sent",
825
+ clickToken: r
826
+ }) : s.kind === "gone" ? (await X().store.removeByHash(a.endpointHash), i.push({
827
+ endpoint: a.endpoint,
828
+ status: "failed",
829
+ error: `endpoint gone (${s.status}) — pruned`,
830
+ pruned: !0
831
+ })) : i.push({
832
+ endpoint: a.endpoint,
833
+ status: "failed",
834
+ error: `${s.status}: ${s.detail}`
835
+ });
836
+ }
837
+ return i;
838
+ }, Ke = (e) => G(Z(e)), qe = k("_voltro_notification_inbox", {
839
+ id: S({ prefix: "notif" }),
840
+ subjectId: A(),
841
+ category: A(),
842
+ title: A(),
843
+ body: A(),
844
+ data: T(),
845
+ readAt: j().nullable(),
846
+ archivedAt: j().nullable(),
847
+ createdAt: j().default("now"),
848
+ tenantId: A().nullable()
849
+ }).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }), Je = k("_voltro_notification_preferences", {
850
+ id: S({ prefix: "notifpref" }),
851
+ subjectId: A(),
852
+ category: A(),
853
+ channel: A(),
854
+ enabled: y().default(!0)
527
855
  }).renamedFrom("notification_preferences").unique("byPreferencesSubjectCategoryChannel", [
528
856
  "subjectId",
529
857
  "category",
530
858
  "channel"
531
- ]), q = E("_voltro_notification_deliveries", {
532
- id: y({ prefix: "notifdlv" }),
533
- recipient: D(),
534
- category: D(),
535
- channel: D(),
536
- status: D(),
537
- error: D().nullable(),
538
- at: O().default("now")
539
- }).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]), J = E("_voltro_notification_topic_subscriptions", {
540
- id: y({ prefix: "notiftsub" }),
541
- topic: D(),
542
- subjectId: D(),
543
- tenantId: D().nullable()
544
- }).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]), Y = E("_voltro_notification_quiet_hours", {
545
- id: y({ prefix: "notifqh" }),
546
- subjectId: D(),
547
- startMinute: b(),
548
- endMinute: b(),
549
- tz: D().default("UTC"),
550
- policy: D().default("hold")
551
- }).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]), X = E("_voltro_notification_held", {
552
- id: y({ prefix: "notifheld" }),
553
- kind: D(),
554
- subjectId: D(),
555
- send: S(),
556
- flushAt: O()
557
- }).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]), Z = class extends d.Tag("@voltro/plugin-notifications/NotificationService")() {}, Q = (e) => async (t) => {
859
+ ]), Ye = k("_voltro_notification_deliveries", {
860
+ id: S({ prefix: "notifdlv" }),
861
+ recipient: A(),
862
+ category: A(),
863
+ channel: A(),
864
+ status: A(),
865
+ error: A().nullable(),
866
+ at: j().default("now"),
867
+ endpoint: A().nullable(),
868
+ clickToken: A().nullable(),
869
+ clickedAt: j().nullable()
870
+ }).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]).index("byDeliveriesClickToken", ["clickToken"]), Xe = k("_voltro_notification_topic_subscriptions", {
871
+ id: S({ prefix: "notiftsub" }),
872
+ topic: A(),
873
+ subjectId: A(),
874
+ tenantId: A().nullable()
875
+ }).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]), $ = k("_voltro_notification_quiet_hours", {
876
+ id: S({ prefix: "notifqh" }),
877
+ subjectId: A(),
878
+ startMinute: C(),
879
+ endMinute: C(),
880
+ tz: A().default("UTC"),
881
+ policy: A().default("hold")
882
+ }).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]), Ze = k("_voltro_notification_held", {
883
+ id: S({ prefix: "notifheld" }),
884
+ kind: A(),
885
+ subjectId: A(),
886
+ send: T(),
887
+ flushAt: j()
888
+ }).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]), Qe = class extends h.Tag("@voltro/plugin-notifications/NotificationService")() {}, $e = (e) => async (t) => {
558
889
  if (e !== void 0) try {
559
890
  let n = await e(t);
560
891
  if (typeof n == "string" && n !== "") return n;
561
892
  } catch {}
562
893
  return t.request.subject.id ?? "anonymous";
563
- }, ce = (e) => e.request.subject.tenantId ?? null, $ = "@voltro/plugin-notifications", le = (d = {}) => {
564
- let m = Q(d.resolveSubjectId), h = ae({
565
- base: $,
566
- alias: d.alias,
567
- instance: d.name
894
+ }, et = (e) => e.request.subject.tenantId ?? null, tt = "@voltro/plugin-notifications", nt = (h = {}) => {
895
+ let _ = $e(h.resolveSubjectId), v = se({
896
+ base: tt,
897
+ alias: h.alias,
898
+ instance: h.name
568
899
  });
569
- w({
900
+ D({
570
901
  source: "plugin",
571
902
  table: "_voltro_notification_deliveries",
572
903
  timeColumn: "at",
573
- ttlMs: T(process.env.VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS, 2160),
904
+ ttlMs: O(process.env.VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS, 2160),
574
905
  envVar: "VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS"
575
- }), w({
906
+ }), D({
576
907
  source: "plugin",
577
908
  table: "_voltro_notification_inbox",
578
909
  timeColumn: "createdAt",
579
- ttlMs: T(process.env.VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS, 4320),
910
+ ttlMs: O(process.env.VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS, 4320),
580
911
  envVar: "VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS",
581
- where: re("readAt")
582
- }), w({
912
+ where: ae("readAt")
913
+ }), D({
583
914
  source: "plugin",
584
915
  table: "_voltro_notification_held",
585
916
  timeColumn: "flushAt",
586
- ttlMs: T(process.env.VOLTRO_NOTIFICATION_HELD_TTL_HOURS, 168),
917
+ ttlMs: O(process.env.VOLTRO_NOTIFICATION_HELD_TTL_HOURS, 168),
587
918
  envVar: "VOLTRO_NOTIFICATION_HELD_TTL_HOURS"
588
919
  });
589
- let g = d.store === void 0, _ = { current: d.store ?? I() }, v = {
590
- addInbox: (e) => _.current.addInbox(e),
591
- listInbox: (e, t) => _.current.listInbox(e, t),
592
- markRead: (e, t) => _.current.markRead(e, t),
593
- markUnread: (e, t) => _.current.markUnread(e, t),
594
- markAllRead: (e) => _.current.markAllRead(e),
595
- setArchived: (e, t, n) => _.current.setArchived(e, t, n),
596
- unreadCount: (e) => _.current.unreadCount(e),
597
- getPreferences: (e) => _.current.getPreferences(e),
598
- setPreference: (e) => _.current.setPreference(e),
599
- recordDelivery: (e) => _.current.recordDelivery(e),
600
- listDeliveries: (e) => _.current.listDeliveries(e),
601
- subscribeTopic: (e) => _.current.subscribeTopic(e),
602
- unsubscribeTopic: (e, t) => _.current.unsubscribeTopic(e, t),
603
- listTopicSubscribers: (e) => _.current.listTopicSubscribers(e),
604
- getQuietHours: (e) => _.current.getQuietHours(e),
605
- setQuietHours: (e) => _.current.setQuietHours(e),
606
- clearQuietHours: (e) => _.current.clearQuietHours(e),
607
- enqueueHeld: (e) => _.current.enqueueHeld(e),
608
- pendingDigest: (e) => _.current.pendingDigest(e),
609
- dueHeld: (e) => _.current.dueHeld(e),
610
- clearHeld: (e) => _.current.clearHeld(e)
611
- }, y = d.channels ?? [j()], b = y.some((e) => e.id === "inApp") ? y : [...y, z(v)], x = d.digestWindowMs ?? 0, S = U({
612
- channels: b,
613
- store: v,
614
- digestWindowMs: x
615
- }), C, E = [
920
+ let y = h.store === void 0, b = { current: h.store ?? R() }, x = {
921
+ addInbox: (e) => b.current.addInbox(e),
922
+ listInbox: (e, t) => b.current.listInbox(e, t),
923
+ markRead: (e, t) => b.current.markRead(e, t),
924
+ markUnread: (e, t) => b.current.markUnread(e, t),
925
+ markAllRead: (e) => b.current.markAllRead(e),
926
+ setArchived: (e, t, n) => b.current.setArchived(e, t, n),
927
+ unreadCount: (e) => b.current.unreadCount(e),
928
+ getPreferences: (e) => b.current.getPreferences(e),
929
+ setPreference: (e) => b.current.setPreference(e),
930
+ recordDelivery: (e) => b.current.recordDelivery(e),
931
+ listDeliveries: (e) => b.current.listDeliveries(e),
932
+ markClicked: (e) => b.current.markClicked(e),
933
+ subscribeTopic: (e) => b.current.subscribeTopic(e),
934
+ unsubscribeTopic: (e, t) => b.current.unsubscribeTopic(e, t),
935
+ listTopicSubscribers: (e) => b.current.listTopicSubscribers(e),
936
+ getQuietHours: (e) => b.current.getQuietHours(e),
937
+ setQuietHours: (e) => b.current.setQuietHours(e),
938
+ clearQuietHours: (e) => b.current.clearQuietHours(e),
939
+ enqueueHeld: (e) => b.current.enqueueHeld(e),
940
+ pendingDigest: (e) => b.current.pendingDigest(e),
941
+ dueHeld: (e) => b.current.dueHeld(e),
942
+ clearHeld: (e) => b.current.clearHeld(e)
943
+ }, S = h.channels ?? [I()], C = S.some((e) => e.id === "inApp") ? S : [...S, xe(x)], w = h.digestWindowMs ?? 0, T = C.some((e) => e.id === "webPush"), E = we({
944
+ channels: C,
945
+ store: x,
946
+ digestWindowMs: w
947
+ }), k, A = [
616
948
  {
617
949
  ...n,
618
950
  description: "The caller's in-app inbox items.",
619
- execute: (e, t) => f.promise(async () => S.inbox(await m(t), e))
951
+ execute: (e, t) => g.promise(async () => E.inbox(await _(t), e))
620
952
  },
621
953
  {
622
- ...te,
954
+ ...f,
623
955
  description: "The caller's unread count.",
624
- execute: (e, t) => f.promise(async () => ({ count: await S.unreadCount(await m(t)) }))
956
+ execute: (e, t) => g.promise(async () => ({ count: await E.unreadCount(await _(t)) }))
625
957
  },
626
958
  {
627
959
  ...i,
628
960
  description: "Mark an inbox item read.",
629
- execute: (e, t) => f.promise(async () => ({ ok: await S.markRead(e.id, await m(t)) }))
961
+ execute: (e, t) => g.promise(async () => ({ ok: await E.markRead(e.id, await _(t)) }))
630
962
  },
631
963
  {
632
964
  ...a,
633
965
  description: "Mark an inbox item unread again.",
634
- execute: (e, t) => f.promise(async () => ({ ok: await S.markUnread(e.id, await m(t)) }))
966
+ execute: (e, t) => g.promise(async () => ({ ok: await E.markUnread(e.id, await _(t)) }))
635
967
  },
636
968
  {
637
969
  ...r,
638
970
  description: "Mark every unread item in the caller's inbox read.",
639
- execute: (e, t) => f.promise(async () => ({ count: await S.markAllRead(await m(t)) }))
971
+ execute: (e, t) => g.promise(async () => ({ count: await E.markAllRead(await _(t)) }))
640
972
  },
641
973
  {
642
974
  ...e,
643
975
  description: "Archive an inbox item — a different state from read; this is what empties the inbox.",
644
- execute: (e, t) => f.promise(async () => ({ ok: await S.setArchived(e.id, await m(t), !0) }))
976
+ execute: (e, t) => g.promise(async () => ({ ok: await E.setArchived(e.id, await _(t), !0) }))
645
977
  },
646
978
  {
647
- ...ee,
979
+ ...d,
648
980
  description: "Return an archived item to the inbox.",
649
- execute: (e, t) => f.promise(async () => ({ ok: await S.setArchived(e.id, await m(t), !1) }))
981
+ execute: (e, t) => g.promise(async () => ({ ok: await E.setArchived(e.id, await _(t), !1) }))
650
982
  },
651
983
  {
652
984
  ...s,
653
985
  description: "The caller's channel preferences.",
654
- execute: (e, t) => f.promise(async () => S.getPreferences(await m(t)))
986
+ execute: (e, t) => g.promise(async () => E.getPreferences(await _(t)))
655
987
  },
656
988
  {
657
989
  ...c,
658
990
  description: "Toggle a channel on/off for a category.",
659
- execute: (e, t) => f.promise(async () => {
991
+ execute: (e, t) => g.promise(async () => {
660
992
  let { category: n, channel: r, enabled: i } = e;
661
- return await S.setPreference({
662
- subjectId: await m(t),
993
+ return await E.setPreference({
994
+ subjectId: await _(t),
663
995
  category: n,
664
996
  channel: r,
665
997
  enabled: i
@@ -669,20 +1001,20 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
669
1001
  {
670
1002
  ...u,
671
1003
  description: "Subscribe the caller to a broadcast topic.",
672
- execute: (e, t) => f.promise(async () => (await S.subscribe(e.topic, await m(t), ce(t)), { ok: !0 }))
1004
+ execute: (e, t) => g.promise(async () => (await E.subscribe(e.topic, await _(t), et(t)), { ok: !0 }))
673
1005
  },
674
1006
  {
675
- ...ne,
1007
+ ...p,
676
1008
  description: "Unsubscribe the caller from a broadcast topic.",
677
- execute: (e, t) => f.promise(async () => (await S.unsubscribe(e.topic, await m(t)), { ok: !0 }))
1009
+ execute: (e, t) => g.promise(async () => (await E.unsubscribe(e.topic, await _(t)), { ok: !0 }))
678
1010
  },
679
1011
  {
680
1012
  ...l,
681
1013
  description: "Set the caller's quiet-hours (DND) window.",
682
- execute: (e, t) => f.promise(async () => {
1014
+ execute: (e, t) => g.promise(async () => {
683
1015
  let n = e;
684
- return await S.setQuietHours({
685
- subjectId: await m(t),
1016
+ return await E.setQuietHours({
1017
+ subjectId: await _(t),
686
1018
  startMinute: n.startMinute,
687
1019
  endMinute: n.endMinute,
688
1020
  ...n.tz ? { tz: n.tz } : {},
@@ -693,25 +1025,58 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
693
1025
  {
694
1026
  ...t,
695
1027
  description: "Clear the caller's quiet-hours window.",
696
- execute: (e, t) => f.promise(async () => (await S.clearQuietHours(await m(t)), { ok: !0 }))
1028
+ execute: (e, t) => g.promise(async () => (await E.clearQuietHours(await _(t)), { ok: !0 }))
1029
+ }
1030
+ ], j = T ? [
1031
+ {
1032
+ ...m,
1033
+ description: "The VAPID application-server key a subscribing browser needs.",
1034
+ execute: () => g.sync(() => ({ key: Ke(X().channelOptions?.privateKey) }))
1035
+ },
1036
+ {
1037
+ ...ne,
1038
+ description: "Register the calling subject's browser push subscription (one row per endpoint).",
1039
+ execute: (e, t) => g.promise(async () => {
1040
+ let n = e;
1041
+ return await X().store.upsert({
1042
+ subjectId: await _(t),
1043
+ endpoint: n.endpoint,
1044
+ endpointHash: q(n.endpoint),
1045
+ p256dh: n.p256dh,
1046
+ auth: n.auth,
1047
+ ua: n.ua ?? null,
1048
+ tenantId: et(t)
1049
+ }), { ok: !0 };
1050
+ })
1051
+ },
1052
+ {
1053
+ ...re,
1054
+ description: "Remove one of the calling subject's own subscriptions.",
1055
+ execute: (e, t) => g.promise(async () => ({ ok: await X().store.removeForSubject(q(e.endpoint), await _(t)) }))
1056
+ },
1057
+ {
1058
+ ...te,
1059
+ description: "How many push endpoints the calling subject has registered.",
1060
+ execute: (e, t) => g.promise(async () => ({ endpoints: await X().store.countForSubject(await _(t)) }))
697
1061
  }
698
- ], D = (e) => ({
1062
+ ] : [], M = (e) => ({
699
1063
  kind: "json",
700
1064
  data: e
701
- }), O = (e, t) => ({
1065
+ }), ce = (e, t) => ({
702
1066
  kind: "json",
703
1067
  status: e,
704
1068
  data: { error: t }
705
- }), k = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), A = [{
1069
+ }), N = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), le = [{
706
1070
  method: "GET",
707
1071
  path: "/deliveries",
708
1072
  description: "Recent delivery records (newest first) + per-channel + per-status counts.",
709
- handler: () => f.promise(async () => {
710
- let e = await v.listDeliveries({ limit: 200 }), t = {}, n = 0, r = 0;
1073
+ handler: () => g.promise(async () => {
1074
+ let e = await x.listDeliveries({ limit: 200 }), t = {}, n = 0, r = 0;
711
1075
  for (let i of e) t[i.channel] = (t[i.channel] ?? 0) + 1, i.status === "sent" ? n++ : r++;
712
- return D({
713
- deliveries: e.slice(0, 100),
714
- channels: b.map((e) => e.id),
1076
+ let i = e.slice(0, 100).map(({ clickToken: e, ...t }) => t);
1077
+ return M({
1078
+ deliveries: i,
1079
+ channels: C.map((e) => e.id),
715
1080
  stats: {
716
1081
  sent: n,
717
1082
  failed: r,
@@ -724,45 +1089,79 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
724
1089
  path: "/inbox",
725
1090
  description: "In-app inbox for a subject (?subjectId=…).",
726
1091
  handler: (e) => {
727
- let t = k(e.url, "subjectId");
728
- return t ? f.promise(async () => D({
729
- items: await v.listInbox(t, { limit: 100 }),
730
- unread: await v.unreadCount(t)
731
- })) : f.succeed(O(400, "subjectId query param required"));
1092
+ let t = N(e.url, "subjectId");
1093
+ return t ? g.promise(async () => M({
1094
+ items: await x.listInbox(t, { limit: 100 }),
1095
+ unread: await x.unreadCount(t)
1096
+ })) : g.succeed(ce(400, "subjectId query param required"));
732
1097
  }
733
1098
  }];
734
- return ie({
735
- name: h,
736
- baseName: $,
1099
+ return oe({
1100
+ name: v,
1101
+ baseName: tt,
737
1102
  description: "Unified notifications — email/slack/sms/push/in-app with preference routing, inbox + delivery log.",
738
1103
  permissions: ["inspect:read", "store:write"],
739
- ...d.tables === !1 ? {} : { extendSchema: { tables: [
740
- G,
741
- K,
742
- q,
743
- J,
744
- Y,
745
- X
1104
+ ...h.tables === !1 ? {} : { extendSchema: { tables: [
1105
+ qe,
1106
+ Je,
1107
+ Ye,
1108
+ Xe,
1109
+ $,
1110
+ Ze,
1111
+ Ie
746
1112
  ] } },
747
- services: p.succeed(Z, S),
748
- routes: E,
749
- rpcClientDescriptors: o,
750
- inspectEndpoints: A,
1113
+ ...T ? {
1114
+ declaredEnv: [{
1115
+ name: "VOLTRO_VAPID_PRIVATE_KEY",
1116
+ required: !0,
1117
+ secret: !0,
1118
+ generate: "p256",
1119
+ description: "VAPID P-256 private scalar (base64url) for web push. Minted per project by `voltro dev`; production sets its own — never a shared or default value."
1120
+ }],
1121
+ httpRoutes: [{
1122
+ method: "POST",
1123
+ path: "/_voltro/notifications/clicked",
1124
+ handle: async (e) => {
1125
+ let t;
1126
+ try {
1127
+ t = JSON.parse(Buffer.from(e.rawBody).toString("utf8")).token;
1128
+ } catch {}
1129
+ if (typeof t != "string" || t === "" || !Fe(t, t)) return {
1130
+ status: 400,
1131
+ body: JSON.stringify({ error: "token required" }),
1132
+ contentType: "application/json"
1133
+ };
1134
+ let n = await x.markClicked(t);
1135
+ return {
1136
+ status: n ? 200 : 404,
1137
+ body: JSON.stringify({ ok: n }),
1138
+ contentType: "application/json"
1139
+ };
1140
+ },
1141
+ maxBodyBytes: 4096
1142
+ }]
1143
+ } : {},
1144
+ services: ie.succeed(Qe, E),
1145
+ routes: [...A, ...j],
1146
+ rpcClientDescriptors: T ? [...o, ...ee] : o,
1147
+ inspectEndpoints: le,
751
1148
  bindDataStore: (e) => {
752
- g && (_.current = R(e));
753
- let t = d.flushIntervalMs ?? 3e4;
754
- C = setInterval(() => {
755
- S.flushDue().catch(() => {});
756
- }, t), typeof C.unref == "function" && C.unref();
757
- },
758
- onActivate: (e) => f.sync(() => e.logger.info("notifications active", {
759
- channels: b.map((e) => e.id),
760
- digestWindowMs: x
761
- })),
762
- onDeactivate: () => f.sync(() => {
763
- C && clearInterval(C), C = void 0;
1149
+ y && (b.current = be(e)), ze(e);
1150
+ let t = h.flushIntervalMs ?? 3e4;
1151
+ k = setInterval(() => {
1152
+ E.flushDue().catch(() => {});
1153
+ }, t), typeof k.unref == "function" && k.unref();
1154
+ },
1155
+ onActivate: (e) => g.sync(() => {
1156
+ T && Ue(), e.logger.info("notifications active", {
1157
+ channels: C.map((e) => e.id),
1158
+ digestWindowMs: w
1159
+ });
1160
+ }),
1161
+ onDeactivate: () => g.sync(() => {
1162
+ k && clearInterval(k), k = void 0;
764
1163
  })
765
1164
  });
766
1165
  };
767
1166
  //#endregion
768
- export { Z as NotificationService, A as PushTokenRejected, U as buildNotificationService, j as consoleChannel, P as customChannel, R as dataStoreNotificationStore, q as deliveriesTable, M as emailChannel, X as heldTable, z as inAppChannel, V as inQuietHours, G as inboxTable, Q as makeSubjectId, I as memoryNotificationStore, B as minuteOfDayInZone, le as notificationsPlugin, K as preferencesTable, se as pushChannel, H as quietHoursEnd, Y as quietHoursTable, F as resolveChannels, N as smsChannel, J as topicSubscriptionsTable, oe as webhookChannel };
1167
+ export { Qe as NotificationService, F as PushTokenRejected, we as buildNotificationService, I as consoleChannel, ve as customChannel, be as dataStoreNotificationStore, Le as dataStorePushSubscriptionStore, Ne as decryptWebPushPayload, Ye as deliveriesTable, ge as emailChannel, Me as encryptWebPushPayload, q as endpointHashOf, Ee as generateVapidPrivateKey, Ze as heldTable, xe as inAppChannel, Se as inQuietHours, qe as inboxTable, $e as makeSubjectId, R as memoryNotificationStore, J as memoryPushSubscriptionStore, B as minuteOfDayInZone, nt as notificationsPlugin, Je as preferencesTable, ye as pushChannel, Ie as pushSubscriptionsTable, Ce as quietHoursEnd, $ as quietHoursTable, Be as resetWebPushStoreForTest, L as resolveChannels, Pe as sendWebPush, _e as smsChannel, Xe as topicSubscriptionsTable, K as vapidAuthorization, G as vapidPublicKeyFor, He as webPushChannel, Ke as webPushPublicKey, he as webhookChannel };