@voltro/plugin-notifications 0.1.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 ADDED
@@ -0,0 +1,689 @@
1
+ import { clearQuietHoursDescriptor as e, inboxDescriptor as t, markReadDescriptor as n, notificationsRpcClientImports as r, preferencesDescriptor as i, setPreferenceDescriptor as a, setQuietHoursDescriptor as o, subscribeDescriptor as s, unreadCountDescriptor as c, unsubscribeDescriptor as l } from "./rpc.js";
2
+ import { Context as u, Effect as d, Layer as f, Schema as p } from "effect";
3
+ import { and as m, boolean as h, eq as g, generateId as _, id as v, integer as y, isNotNull as ee, isNull as b, json as x, lte as S, registerRetention as C, retentionTtlMsFromEnv as w, table as T, text as E, timestamp as D } from "@voltro/database";
4
+ import { definePlugin as O } from "@voltro/protocol";
5
+ import { randomUUID as k } from "node:crypto";
6
+ //#region src/errors.ts
7
+ var A = class extends p.TaggedError()("PushTokenRejected", {
8
+ token: p.String,
9
+ reason: p.String
10
+ }) {}, j = () => ({
11
+ id: "console",
12
+ deliver: async (e) => {
13
+ process.stdout.write(`[notify:${e.category}] → ${e.to}: ${e.title} — ${e.body}\n`);
14
+ }
15
+ }), M = (e) => ({
16
+ id: e.id ?? "slack",
17
+ deliver: async (t) => {
18
+ let n = e.format ? e.format(t) : { text: `*${t.title}*\n${t.body}` }, r = await fetch(e.url, {
19
+ method: "POST",
20
+ headers: {
21
+ "content-type": "application/json",
22
+ ...e.headers ?? {}
23
+ },
24
+ body: JSON.stringify(n)
25
+ });
26
+ if (!r.ok) throw Error(`webhook ${e.id ?? "slack"} POST ${r.status}`);
27
+ }
28
+ }), N = (e) => ({
29
+ id: "email",
30
+ deliver: (t) => e({
31
+ to: t.to,
32
+ subject: t.title,
33
+ html: `<p>${t.body}</p>`,
34
+ text: t.body
35
+ })
36
+ }), P = (e) => ({
37
+ id: "sms",
38
+ deliver: (t) => e(t.to, `${t.title}: ${t.body}`)
39
+ }), te = (e, t) => ({
40
+ id: e,
41
+ deliver: t
42
+ }), F = (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 ?? {}
53
+ };
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
+ }
63
+ }
64
+ }), I = (e, t, n, r, i) => {
65
+ 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
+ return a.filter((e) => !o.has(e));
67
+ }, L = () => {
68
+ 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
+ return {
70
+ addInbox: async (t) => {
71
+ e.unshift({
72
+ ...t,
73
+ readAt: null,
74
+ data: t.data ?? {},
75
+ tenantId: t.tenantId ?? null
76
+ });
77
+ },
78
+ listInbox: async (t, n) => {
79
+ let r = e.filter((e) => e.subjectId === t);
80
+ return n?.unreadOnly && (r = r.filter((e) => e.readAt === null)), n?.limit === void 0 ? r : r.slice(0, n.limit);
81
+ },
82
+ markRead: async (t, n) => {
83
+ let r = e.findIndex((e) => e.id === t && e.subjectId === n);
84
+ return r === -1 ? !1 : (e[r] = {
85
+ ...e[r],
86
+ readAt: (/* @__PURE__ */ new Date(0)).toISOString()
87
+ }, !0);
88
+ },
89
+ unreadCount: async (t) => e.filter((e) => e.subjectId === t && e.readAt === null).length,
90
+ getPreferences: async (e) => [...t.values()].filter((t) => t.subjectId === e),
91
+ setPreference: async (e) => {
92
+ t.set(o(e), e);
93
+ },
94
+ recordDelivery: async (e) => {
95
+ n.unshift(e);
96
+ },
97
+ listDeliveries: async (e) => e?.limit === void 0 ? [...n] : n.slice(0, e.limit),
98
+ subscribeTopic: async (e) => {
99
+ r.set(s(e.topic, e.subjectId), e);
100
+ },
101
+ unsubscribeTopic: async (e, t) => {
102
+ r.delete(s(e, t));
103
+ },
104
+ listTopicSubscribers: async (e) => [...r.values()].filter((t) => t.topic === e),
105
+ getQuietHours: async (e) => i.get(e) ?? null,
106
+ setQuietHours: async (e) => {
107
+ i.set(e.subjectId, e);
108
+ },
109
+ clearQuietHours: async (e) => {
110
+ i.delete(e);
111
+ },
112
+ enqueueHeld: async (e) => {
113
+ a.push(e);
114
+ },
115
+ pendingDigest: async (e) => a.filter((t) => t.kind === "digest" && t.subjectId === e),
116
+ dueHeld: async (e) => a.filter((t) => t.flushAt <= e).sort((e, t) => e.flushAt < t.flushAt ? -1 : 1),
117
+ clearHeld: async (e) => {
118
+ let t = new Set(e);
119
+ for (let e = a.length - 1; e >= 0; e--) t.has(a[e].id) && a.splice(e, 1);
120
+ }
121
+ };
122
+ }, R = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "", z = (e) => {
123
+ let t = async (t, n, r, i) => await e.query({
124
+ table: t,
125
+ predicate: n,
126
+ order: r,
127
+ take: i,
128
+ skip: void 0,
129
+ projection: void 0
130
+ }), n = (e) => ({
131
+ id: String(e.id),
132
+ subjectId: String(e.subjectId),
133
+ category: String(e.category),
134
+ title: String(e.title),
135
+ body: String(e.body),
136
+ data: e.data ?? {},
137
+ readAt: e.readAt == null ? null : R(e.readAt),
138
+ createdAt: R(e.createdAt),
139
+ tenantId: e.tenantId ?? null
140
+ }), r = (e) => ({
141
+ subjectId: String(e.subjectId),
142
+ category: String(e.category),
143
+ channel: String(e.channel),
144
+ enabled: !!e.enabled
145
+ }), i = (e) => {
146
+ let t = {
147
+ id: String(e.id),
148
+ to: String(e.recipient),
149
+ category: String(e.category),
150
+ channel: String(e.channel),
151
+ status: e.status,
152
+ at: R(e.at)
153
+ };
154
+ return e.error == null ? t : {
155
+ ...t,
156
+ error: String(e.error)
157
+ };
158
+ }, a = (e) => ({
159
+ topic: String(e.topic),
160
+ subjectId: String(e.subjectId),
161
+ tenantId: e.tenantId ?? null
162
+ }), o = (e) => ({
163
+ subjectId: String(e.subjectId),
164
+ startMinute: Number(e.startMinute),
165
+ endMinute: Number(e.endMinute),
166
+ tz: String(e.tz ?? "UTC"),
167
+ policy: e.policy === "drop" ? "drop" : "hold"
168
+ }), s = (e) => ({
169
+ id: String(e.id),
170
+ kind: e.kind === "quiet" ? "quiet" : "digest",
171
+ subjectId: String(e.subjectId),
172
+ send: e.send ?? {},
173
+ flushAt: R(e.flushAt)
174
+ });
175
+ return {
176
+ addInbox: async (t) => {
177
+ await e.insert("notification_inbox", {
178
+ id: t.id,
179
+ subjectId: t.subjectId,
180
+ category: t.category,
181
+ title: t.title,
182
+ body: t.body,
183
+ data: t.data ?? {},
184
+ readAt: null,
185
+ createdAt: t.createdAt,
186
+ tenantId: t.tenantId ?? null
187
+ });
188
+ },
189
+ listInbox: async (e, r) => {
190
+ let i = r?.unreadOnly ? m(g("subjectId", e), b("readAt")) : g("subjectId", e);
191
+ return (await t("notification_inbox", i, [{
192
+ column: "createdAt",
193
+ direction: "desc"
194
+ }], r?.limit)).map(n);
195
+ },
196
+ markRead: async (n, r) => (await t("notification_inbox", m(g("id", n), g("subjectId", r)), [], 1))[0] ? (await e.update("notification_inbox", n, { readAt: (/* @__PURE__ */ new Date()).toISOString() }), !0) : !1,
197
+ unreadCount: async (e) => (await t("notification_inbox", m(g("subjectId", e), b("readAt")), [], void 0)).length,
198
+ getPreferences: async (e) => (await t("notification_preferences", g("subjectId", e), [], void 0)).map(r),
199
+ setPreference: async (t) => {
200
+ let n = _({
201
+ kind: "typeid",
202
+ prefix: "notifpref"
203
+ }, "notification_preferences");
204
+ await e.upsert("notification_preferences", {
205
+ id: n,
206
+ subjectId: t.subjectId,
207
+ category: t.category,
208
+ channel: t.channel,
209
+ enabled: t.enabled
210
+ }, {
211
+ conflictColumns: [
212
+ "subjectId",
213
+ "category",
214
+ "channel"
215
+ ],
216
+ update: ["enabled"]
217
+ });
218
+ },
219
+ recordDelivery: async (t) => {
220
+ await e.insert("notification_deliveries", {
221
+ id: t.id,
222
+ recipient: t.to,
223
+ category: t.category,
224
+ channel: t.channel,
225
+ status: t.status,
226
+ error: t.error ?? null,
227
+ at: t.at
228
+ });
229
+ },
230
+ listDeliveries: async (e) => (await t("notification_deliveries", void 0, [{
231
+ column: "at",
232
+ direction: "desc"
233
+ }], e?.limit)).map(i),
234
+ subscribeTopic: async (t) => {
235
+ let n = _({
236
+ kind: "typeid",
237
+ prefix: "notiftsub"
238
+ }, "notification_topic_subscriptions");
239
+ await e.upsert("notification_topic_subscriptions", {
240
+ id: n,
241
+ topic: t.topic,
242
+ subjectId: t.subjectId,
243
+ tenantId: t.tenantId ?? null
244
+ }, {
245
+ conflictColumns: ["topic", "subjectId"],
246
+ update: ["tenantId"]
247
+ });
248
+ },
249
+ unsubscribeTopic: async (n, r) => {
250
+ let i = await t("notification_topic_subscriptions", m(g("topic", n), g("subjectId", r)), [], 1);
251
+ i[0] && await e.delete("notification_topic_subscriptions", String(i[0].id));
252
+ },
253
+ listTopicSubscribers: async (e) => (await t("notification_topic_subscriptions", g("topic", e), [], void 0)).map(a),
254
+ getQuietHours: async (e) => {
255
+ let n = await t("notification_quiet_hours", g("subjectId", e), [], 1);
256
+ return n[0] ? o(n[0]) : null;
257
+ },
258
+ setQuietHours: async (t) => {
259
+ let n = _({
260
+ kind: "typeid",
261
+ prefix: "notifqh"
262
+ }, "notification_quiet_hours");
263
+ await e.upsert("notification_quiet_hours", {
264
+ id: n,
265
+ subjectId: t.subjectId,
266
+ startMinute: t.startMinute,
267
+ endMinute: t.endMinute,
268
+ tz: t.tz ?? "UTC",
269
+ policy: t.policy ?? "hold"
270
+ }, {
271
+ conflictColumns: ["subjectId"],
272
+ update: [
273
+ "startMinute",
274
+ "endMinute",
275
+ "tz",
276
+ "policy"
277
+ ]
278
+ });
279
+ },
280
+ clearQuietHours: async (n) => {
281
+ let r = await t("notification_quiet_hours", g("subjectId", n), [], 1);
282
+ r[0] && await e.delete("notification_quiet_hours", String(r[0].id));
283
+ },
284
+ enqueueHeld: async (t) => {
285
+ await e.insert("notification_held", {
286
+ id: t.id,
287
+ kind: t.kind,
288
+ subjectId: t.subjectId,
289
+ send: t.send,
290
+ flushAt: t.flushAt
291
+ });
292
+ },
293
+ pendingDigest: async (e) => (await t("notification_held", m(g("kind", "digest"), g("subjectId", e)), [{
294
+ column: "flushAt",
295
+ direction: "asc"
296
+ }], void 0)).map(s),
297
+ dueHeld: async (e) => (await t("notification_held", S("flushAt", e), [{
298
+ column: "flushAt",
299
+ direction: "asc"
300
+ }], void 0)).map(s),
301
+ clearHeld: async (t) => {
302
+ for (let n of t) await e.delete("notification_held", n);
303
+ }
304
+ };
305
+ }, B = (e) => ({
306
+ id: "inApp",
307
+ deliver: async (t) => {
308
+ await e.addInbox({
309
+ id: k(),
310
+ subjectId: t.to,
311
+ category: t.category,
312
+ title: t.title,
313
+ body: t.body,
314
+ data: t.data ?? {},
315
+ tenantId: t.tenantId ?? null,
316
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
317
+ });
318
+ }
319
+ }), V = (e, t) => {
320
+ try {
321
+ let n = new Intl.DateTimeFormat("en-US", {
322
+ timeZone: t,
323
+ hour: "2-digit",
324
+ minute: "2-digit",
325
+ hour12: !1
326
+ }).formatToParts(e), r = Number(n.find((e) => e.type === "hour")?.value ?? "0") % 24, i = Number(n.find((e) => e.type === "minute")?.value ?? "0");
327
+ return r * 60 + i;
328
+ } catch {
329
+ return e.getUTCHours() * 60 + e.getUTCMinutes();
330
+ }
331
+ }, H = (e, t) => {
332
+ if (e.startMinute === e.endMinute) return !1;
333
+ let n = V(t, e.tz ?? "UTC");
334
+ return e.startMinute < e.endMinute ? n >= e.startMinute && n < e.endMinute : n >= e.startMinute || n < e.endMinute;
335
+ }, U = (e, t) => {
336
+ let n = V(t, e.tz ?? "UTC"), r = e.endMinute - n;
337
+ return r <= 0 && (r += 1440), new Date(t.getTime() + r * 6e4);
338
+ }, W = (e) => {
339
+ 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) => {
340
+ let a = await e.store.getPreferences(i.to), o = I(n, i.category, i.channels, a, i.to), s = n.filter((e) => !o.includes(e)), c = [], l = [], u = {
341
+ to: i.to,
342
+ category: i.category,
343
+ title: i.title,
344
+ body: i.body,
345
+ ...i.data ? { data: i.data } : {},
346
+ tenantId: i.tenantId ?? null
347
+ };
348
+ for (let n of o) {
349
+ let a = t.get(n);
350
+ if (!a) continue;
351
+ let o = {
352
+ id: k(),
353
+ to: i.to,
354
+ category: i.category,
355
+ channel: n,
356
+ at: r()
357
+ };
358
+ try {
359
+ await a.deliver(u), c.push(n), await e.store.recordDelivery({
360
+ ...o,
361
+ status: "sent"
362
+ });
363
+ } catch (t) {
364
+ let r = t instanceof Error ? t.message : String(t);
365
+ l.push({
366
+ channel: n,
367
+ error: r
368
+ }), await e.store.recordDelivery({
369
+ ...o,
370
+ status: "failed",
371
+ error: r
372
+ });
373
+ }
374
+ }
375
+ return {
376
+ delivered: c,
377
+ failed: l,
378
+ skipped: s
379
+ };
380
+ }, o = async (t) => {
381
+ let n = await e.store.getQuietHours(t.to), o = new Date(r());
382
+ if (n && H(n, o)) return (n.policy ?? "hold") === "drop" ? {
383
+ delivered: [],
384
+ failed: [],
385
+ skipped: [],
386
+ held: "quiet-drop"
387
+ } : (await e.store.enqueueHeld({
388
+ id: k(),
389
+ kind: "quiet",
390
+ subjectId: t.to,
391
+ send: t,
392
+ flushAt: U(n, o).toISOString()
393
+ }), {
394
+ delivered: [],
395
+ failed: [],
396
+ skipped: [],
397
+ held: "quiet-held"
398
+ });
399
+ if (i > 0 && !t.channels) {
400
+ let n = (await e.store.pendingDigest(t.to))[0]?.flushAt ?? new Date(o.getTime() + i).toISOString();
401
+ return await e.store.enqueueHeld({
402
+ id: k(),
403
+ kind: "digest",
404
+ subjectId: t.to,
405
+ send: t,
406
+ flushAt: n
407
+ }), {
408
+ delivered: [],
409
+ failed: [],
410
+ skipped: [],
411
+ held: "digest"
412
+ };
413
+ }
414
+ return a(t);
415
+ };
416
+ return {
417
+ send: o,
418
+ inbox: (t, n) => e.store.listInbox(t, n),
419
+ unreadCount: (t) => e.store.unreadCount(t),
420
+ markRead: (t, n) => e.store.markRead(t, n),
421
+ getPreferences: (t) => e.store.getPreferences(t),
422
+ setPreference: (t) => e.store.setPreference(t),
423
+ subscribe: (t, n, r) => e.store.subscribeTopic({
424
+ topic: t,
425
+ subjectId: n,
426
+ tenantId: r ?? null
427
+ }),
428
+ unsubscribe: (t, n) => e.store.unsubscribeTopic(t, n),
429
+ broadcast: async (t, n) => {
430
+ let r = await e.store.listTopicSubscribers(t), i = [];
431
+ for (let e of r) i.push(await o({
432
+ ...n,
433
+ to: e.subjectId,
434
+ tenantId: n.tenantId ?? e.tenantId ?? null
435
+ }));
436
+ return {
437
+ recipients: r.length,
438
+ results: i
439
+ };
440
+ },
441
+ setQuietHours: (t) => e.store.setQuietHours(t),
442
+ clearQuietHours: (t) => e.store.clearQuietHours(t),
443
+ getQuietHours: (t) => e.store.getQuietHours(t),
444
+ flushDue: async (t = new Date(r())) => {
445
+ let n = await e.store.dueHeld(t.toISOString());
446
+ if (n.length === 0) return 0;
447
+ for (let e of n.filter((e) => e.kind === "quiet")) await a(e.send);
448
+ let i = /* @__PURE__ */ new Map();
449
+ for (let e of n.filter((e) => e.kind === "digest")) {
450
+ let t = i.get(e.subjectId) ?? [];
451
+ t.push(e), i.set(e.subjectId, t);
452
+ }
453
+ for (let [e, t] of i) await a(G(e, t));
454
+ return await e.store.clearHeld(n.map((e) => e.id)), n.length;
455
+ }
456
+ };
457
+ }, G = (e, t) => {
458
+ if (t.length === 1) return t[0].send;
459
+ let n = t.map((e) => ({
460
+ category: e.send.category,
461
+ title: e.send.title,
462
+ body: e.send.body
463
+ }));
464
+ return {
465
+ to: e,
466
+ category: "digest",
467
+ title: `${t.length} new notifications`,
468
+ body: n.map((e) => `• ${e.title}`).join("\n"),
469
+ data: { items: n },
470
+ tenantId: t[0].send.tenantId ?? null
471
+ };
472
+ }, K = T("notification_inbox", {
473
+ id: v({ prefix: "notif" }),
474
+ subjectId: E(),
475
+ category: E(),
476
+ title: E(),
477
+ body: E(),
478
+ data: x(),
479
+ readAt: D().nullable(),
480
+ createdAt: D().default("now"),
481
+ tenantId: E().nullable()
482
+ }).index("bySubject", ["subjectId"]), q = T("notification_preferences", {
483
+ id: v({ prefix: "notifpref" }),
484
+ subjectId: E(),
485
+ category: E(),
486
+ channel: E(),
487
+ enabled: h().default(!0)
488
+ }).unique("bySubjectCategoryChannel", [
489
+ "subjectId",
490
+ "category",
491
+ "channel"
492
+ ]), J = T("notification_deliveries", {
493
+ id: v({ prefix: "notifdlv" }),
494
+ recipient: E(),
495
+ category: E(),
496
+ channel: E(),
497
+ status: E(),
498
+ error: E().nullable(),
499
+ at: D().default("now")
500
+ }).index("byAt", ["at"]), Y = T("notification_topic_subscriptions", {
501
+ id: v({ prefix: "notiftsub" }),
502
+ topic: E(),
503
+ subjectId: E(),
504
+ tenantId: E().nullable()
505
+ }).unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]), X = T("notification_quiet_hours", {
506
+ id: v({ prefix: "notifqh" }),
507
+ subjectId: E(),
508
+ startMinute: y(),
509
+ endMinute: y(),
510
+ tz: E().default("UTC"),
511
+ policy: E().default("hold")
512
+ }).unique("bySubject", ["subjectId"]), Z = T("notification_held", {
513
+ id: v({ prefix: "notifheld" }),
514
+ kind: E(),
515
+ subjectId: E(),
516
+ send: x(),
517
+ flushAt: D()
518
+ }).index("byFlushAt", ["flushAt"]).index("bySubjectKind", ["subjectId", "kind"]), Q = class extends u.Tag("@voltro/plugin-notifications/NotificationService")() {}, $ = (e) => e.request.subject.id ?? "anonymous", ne = (e) => e.request.subject.tenantId ?? null, re = (u = {}) => {
519
+ let p = u.name ? `@voltro/plugin-notifications#${u.name}` : "@voltro/plugin-notifications";
520
+ C({
521
+ table: "notification_deliveries",
522
+ timeColumn: "at",
523
+ ttlMs: w(process.env.VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS, 2160)
524
+ }), C({
525
+ table: "notification_inbox",
526
+ timeColumn: "createdAt",
527
+ ttlMs: w(process.env.VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS, 4320),
528
+ where: ee("readAt")
529
+ }), C({
530
+ table: "notification_held",
531
+ timeColumn: "flushAt",
532
+ ttlMs: w(process.env.VOLTRO_NOTIFICATION_HELD_TTL_HOURS, 168)
533
+ });
534
+ let m = u.store === void 0, h = { current: u.store ?? L() }, g = {
535
+ addInbox: (e) => h.current.addInbox(e),
536
+ listInbox: (e, t) => h.current.listInbox(e, t),
537
+ markRead: (e, t) => h.current.markRead(e, t),
538
+ unreadCount: (e) => h.current.unreadCount(e),
539
+ getPreferences: (e) => h.current.getPreferences(e),
540
+ setPreference: (e) => h.current.setPreference(e),
541
+ recordDelivery: (e) => h.current.recordDelivery(e),
542
+ listDeliveries: (e) => h.current.listDeliveries(e),
543
+ subscribeTopic: (e) => h.current.subscribeTopic(e),
544
+ unsubscribeTopic: (e, t) => h.current.unsubscribeTopic(e, t),
545
+ listTopicSubscribers: (e) => h.current.listTopicSubscribers(e),
546
+ getQuietHours: (e) => h.current.getQuietHours(e),
547
+ setQuietHours: (e) => h.current.setQuietHours(e),
548
+ clearQuietHours: (e) => h.current.clearQuietHours(e),
549
+ enqueueHeld: (e) => h.current.enqueueHeld(e),
550
+ pendingDigest: (e) => h.current.pendingDigest(e),
551
+ dueHeld: (e) => h.current.dueHeld(e),
552
+ clearHeld: (e) => h.current.clearHeld(e)
553
+ }, _ = u.channels ?? [j()], v = _.some((e) => e.id === "inApp") ? _ : [..._, B(g)], y = u.digestWindowMs ?? 0, b = W({
554
+ channels: v,
555
+ store: g,
556
+ digestWindowMs: y
557
+ }), x, S = [
558
+ {
559
+ ...t,
560
+ description: "The caller's in-app inbox items.",
561
+ execute: (e, t) => d.promise(() => b.inbox($(t), e))
562
+ },
563
+ {
564
+ ...c,
565
+ description: "The caller's unread count.",
566
+ execute: (e, t) => d.promise(async () => ({ count: await b.unreadCount($(t)) }))
567
+ },
568
+ {
569
+ ...n,
570
+ description: "Mark an inbox item read.",
571
+ execute: (e, t) => d.promise(async () => ({ ok: await b.markRead(e.id, $(t)) }))
572
+ },
573
+ {
574
+ ...i,
575
+ description: "The caller's channel preferences.",
576
+ execute: (e, t) => d.promise(() => b.getPreferences($(t)))
577
+ },
578
+ {
579
+ ...a,
580
+ description: "Toggle a channel on/off for a category.",
581
+ execute: (e, t) => d.promise(async () => {
582
+ let { category: n, channel: r, enabled: i } = e;
583
+ return await b.setPreference({
584
+ subjectId: $(t),
585
+ category: n,
586
+ channel: r,
587
+ enabled: i
588
+ }), { ok: !0 };
589
+ })
590
+ },
591
+ {
592
+ ...s,
593
+ description: "Subscribe the caller to a broadcast topic.",
594
+ execute: (e, t) => d.promise(async () => (await b.subscribe(e.topic, $(t), ne(t)), { ok: !0 }))
595
+ },
596
+ {
597
+ ...l,
598
+ description: "Unsubscribe the caller from a broadcast topic.",
599
+ execute: (e, t) => d.promise(async () => (await b.unsubscribe(e.topic, $(t)), { ok: !0 }))
600
+ },
601
+ {
602
+ ...o,
603
+ description: "Set the caller's quiet-hours (DND) window.",
604
+ execute: (e, t) => d.promise(async () => {
605
+ let n = e;
606
+ return await b.setQuietHours({
607
+ subjectId: $(t),
608
+ startMinute: n.startMinute,
609
+ endMinute: n.endMinute,
610
+ ...n.tz ? { tz: n.tz } : {},
611
+ ...n.policy ? { policy: n.policy } : {}
612
+ }), { ok: !0 };
613
+ })
614
+ },
615
+ {
616
+ ...e,
617
+ description: "Clear the caller's quiet-hours window.",
618
+ execute: (e, t) => d.promise(async () => (await b.clearQuietHours($(t)), { ok: !0 }))
619
+ }
620
+ ], T = (e) => ({
621
+ kind: "json",
622
+ data: e
623
+ }), E = (e, t) => ({
624
+ kind: "json",
625
+ status: e,
626
+ data: { error: t }
627
+ }), D = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t);
628
+ return O({
629
+ name: p,
630
+ description: "Unified notifications — email/slack/sms/push/in-app with preference routing, inbox + delivery log.",
631
+ permissions: ["inspect:read", "store:write"],
632
+ extendSchema: { tables: [
633
+ K,
634
+ q,
635
+ J,
636
+ Y,
637
+ X,
638
+ Z
639
+ ] },
640
+ services: f.succeed(Q, b),
641
+ routes: S,
642
+ rpcClientDescriptors: r,
643
+ inspectEndpoints: [{
644
+ method: "GET",
645
+ path: "/deliveries",
646
+ description: "Recent delivery records (newest first) + per-channel + per-status counts.",
647
+ handler: () => d.promise(async () => {
648
+ let e = await g.listDeliveries({ limit: 200 }), t = {}, n = 0, r = 0;
649
+ for (let i of e) t[i.channel] = (t[i.channel] ?? 0) + 1, i.status === "sent" ? n++ : r++;
650
+ return T({
651
+ deliveries: e.slice(0, 100),
652
+ channels: v.map((e) => e.id),
653
+ stats: {
654
+ sent: n,
655
+ failed: r,
656
+ byChannel: t
657
+ }
658
+ });
659
+ })
660
+ }, {
661
+ method: "GET",
662
+ path: "/inbox",
663
+ description: "In-app inbox for a subject (?subjectId=…).",
664
+ handler: (e) => {
665
+ let t = D(e.url, "subjectId");
666
+ return t ? d.promise(async () => T({
667
+ items: await g.listInbox(t, { limit: 100 }),
668
+ unread: await g.unreadCount(t)
669
+ })) : d.succeed(E(400, "subjectId query param required"));
670
+ }
671
+ }],
672
+ bindDataStore: (e) => {
673
+ m && (h.current = z(e));
674
+ let t = u.flushIntervalMs ?? 3e4;
675
+ x = setInterval(() => {
676
+ b.flushDue().catch(() => {});
677
+ }, t), typeof x.unref == "function" && x.unref();
678
+ },
679
+ onActivate: (e) => d.sync(() => e.logger.info("notifications active", {
680
+ channels: v.map((e) => e.id),
681
+ digestWindowMs: y
682
+ })),
683
+ onDeactivate: () => d.sync(() => {
684
+ x && clearInterval(x), x = void 0;
685
+ })
686
+ });
687
+ };
688
+ //#endregion
689
+ export { Q as NotificationService, A as PushTokenRejected, W as buildNotificationService, j as consoleChannel, te as customChannel, z as dataStoreNotificationStore, N as emailChannel, B as inAppChannel, H as inQuietHours, L as memoryNotificationStore, V as minuteOfDayInZone, re as notificationsPlugin, F as pushChannel, U as quietHoursEnd, I as resolveChannels, P as smsChannel, M as webhookChannel };