@voltro/plugin-notifications 0.33.0 → 0.34.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/CHANGELOG.md +1801 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +90 -82
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -305,6 +305,48 @@ export declare interface NotificationsPluginOptions {
|
|
|
305
305
|
/** How often the scheduled flush drains due digest windows + quiet-hours
|
|
306
306
|
* deferrals. Default 30s. Only runs once the store is bound at boot. */
|
|
307
307
|
readonly flushIntervalMs?: number;
|
|
308
|
+
/**
|
|
309
|
+
* Namespace for this plugin's rpc tags + inspect endpoints. Default `notifications`.
|
|
310
|
+
*
|
|
311
|
+
* Set it when your app already publishes under that name — an exact tag
|
|
312
|
+
* collision is fatal at codegen, and this is the way out. Orthogonal to
|
|
313
|
+
* `name` below: `alias` REPLACES the namespace, `name` distinguishes two
|
|
314
|
+
* installations within it.
|
|
315
|
+
*
|
|
316
|
+
* The cost, stated because nothing else states it: the local and cloud
|
|
317
|
+
* dashboards fetch this plugin's panel at the DEFAULT slug, so an aliased
|
|
318
|
+
* install keeps working while its dashboard panel 404s. Alias to escape a
|
|
319
|
+
* collision, not for taste.
|
|
320
|
+
*/
|
|
321
|
+
readonly alias?: string;
|
|
322
|
+
/**
|
|
323
|
+
* Contribute this plugin's tables via `extendSchema.tables`. Default `true`.
|
|
324
|
+
*
|
|
325
|
+
* Set `false` when your app ALREADY declares equivalent tables and you want to
|
|
326
|
+
* keep them — the seam this exists for. The plugin then contributes no DDL and
|
|
327
|
+
* the declarative differ never proposes its tables; everything else (routes,
|
|
328
|
+
* inspect, interceptors) is unchanged.
|
|
329
|
+
*
|
|
330
|
+
* **What you take over, exactly:** all six tables this plugin writes BY NAME —
|
|
331
|
+
* `_voltro_notification_inbox`, `_voltro_notification_preferences`,
|
|
332
|
+
* `_voltro_notification_deliveries`, `_voltro_notification_topic_subscriptions`,
|
|
333
|
+
* `_voltro_notification_quiet_hours`, `_voltro_notification_held`. This is the
|
|
334
|
+
* 14 670-row overlap the seam was reported for; note it does NOT solve an rpc
|
|
335
|
+
* name collision — `alias` is the field for that.
|
|
336
|
+
*
|
|
337
|
+
* It is not offered on every plugin, and the omissions are deliberate rather
|
|
338
|
+
* than unfinished: a `tables: false` that quietly disables a table carrying an
|
|
339
|
+
* AUTHORIZATION or SAFETY decision — the SAML replay cache, SCIM provisioning
|
|
340
|
+
* state, billing's usage counters, cdc-out's outbox — is a security regression
|
|
341
|
+
* shipped as an ergonomics feature. Those plugins need a named store seam
|
|
342
|
+
* first, not a boolean.
|
|
343
|
+
*/
|
|
344
|
+
readonly tables?: boolean;
|
|
345
|
+
/**
|
|
346
|
+
* Discriminator for a SECOND installation of this plugin, when one app runs
|
|
347
|
+
* two (`@voltro/plugin-notifications#analytics`). Not a rename — for that use
|
|
348
|
+
* `alias`.
|
|
349
|
+
*/
|
|
308
350
|
readonly name?: string;
|
|
309
351
|
}
|
|
310
352
|
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
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
2
|
import { Context as d, Effect as f, Layer as p, Schema as m } from "effect";
|
|
3
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
|
|
5
|
-
import { randomUUID as
|
|
4
|
+
import { definePlugin as ie, pluginInstanceName as ae } from "@voltro/protocol";
|
|
5
|
+
import { randomUUID as k } from "node:crypto";
|
|
6
6
|
//#region src/errors.ts
|
|
7
|
-
var
|
|
7
|
+
var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
8
8
|
token: m.String,
|
|
9
9
|
reason: m.String
|
|
10
|
-
}) {},
|
|
10
|
+
}) {}, j = () => ({
|
|
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
|
-
}),
|
|
15
|
+
}), oe = (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 j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
25
25
|
});
|
|
26
26
|
if (!r.ok) throw Error(`webhook ${e.id ?? "slack"} POST ${r.status}`);
|
|
27
27
|
}
|
|
28
|
-
}),
|
|
28
|
+
}), M = (e) => ({
|
|
29
29
|
id: "email",
|
|
30
30
|
deliver: (t) => e({
|
|
31
31
|
to: t.to,
|
|
@@ -39,7 +39,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
39
39
|
}), P = (e, t) => ({
|
|
40
40
|
id: e,
|
|
41
41
|
deliver: t
|
|
42
|
-
}),
|
|
42
|
+
}), se = (e) => ({
|
|
43
43
|
id: e.id ?? "push",
|
|
44
44
|
deliver: async (t) => {
|
|
45
45
|
let n = await e.tokensFor(t.to), r = t.data?.badge, i = typeof r == "number" ? r : void 0;
|
|
@@ -54,17 +54,17 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
54
54
|
try {
|
|
55
55
|
await e.transport(n);
|
|
56
56
|
} catch (e) {
|
|
57
|
-
throw e instanceof
|
|
57
|
+
throw e instanceof A ? e : new A({
|
|
58
58
|
token: r,
|
|
59
59
|
reason: e instanceof Error ? e.message : String(e)
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
}),
|
|
64
|
+
}), F = (e, t, n, r, i) => {
|
|
65
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
66
|
return a.filter((e) => !o.has(e));
|
|
67
|
-
},
|
|
67
|
+
}, I = () => {
|
|
68
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
69
|
return {
|
|
70
70
|
addInbox: async (t) => {
|
|
@@ -145,7 +145,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
145
145
|
for (let e = a.length - 1; e >= 0; e--) t.has(a[e].id) && a.splice(e, 1);
|
|
146
146
|
}
|
|
147
147
|
};
|
|
148
|
-
},
|
|
148
|
+
}, L = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "", R = (e) => {
|
|
149
149
|
let t = async (t, n, r, i) => await e.query({
|
|
150
150
|
table: t,
|
|
151
151
|
predicate: n,
|
|
@@ -160,9 +160,9 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
160
160
|
title: String(e.title),
|
|
161
161
|
body: String(e.body),
|
|
162
162
|
data: e.data ?? {},
|
|
163
|
-
readAt: e.readAt == null ? null :
|
|
164
|
-
archivedAt: e.archivedAt == null ? null :
|
|
165
|
-
createdAt:
|
|
163
|
+
readAt: e.readAt == null ? null : L(e.readAt),
|
|
164
|
+
archivedAt: e.archivedAt == null ? null : L(e.archivedAt),
|
|
165
|
+
createdAt: L(e.createdAt),
|
|
166
166
|
tenantId: e.tenantId ?? null
|
|
167
167
|
}), r = (e) => ({
|
|
168
168
|
subjectId: String(e.subjectId),
|
|
@@ -176,7 +176,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
176
176
|
category: String(e.category),
|
|
177
177
|
channel: String(e.channel),
|
|
178
178
|
status: e.status,
|
|
179
|
-
at:
|
|
179
|
+
at: L(e.at)
|
|
180
180
|
};
|
|
181
181
|
return e.error == null ? t : {
|
|
182
182
|
...t,
|
|
@@ -197,7 +197,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
197
197
|
kind: e.kind === "quiet" ? "quiet" : "digest",
|
|
198
198
|
subjectId: String(e.subjectId),
|
|
199
199
|
send: e.send ?? {},
|
|
200
|
-
flushAt:
|
|
200
|
+
flushAt: L(e.flushAt)
|
|
201
201
|
});
|
|
202
202
|
return {
|
|
203
203
|
addInbox: async (t) => {
|
|
@@ -337,11 +337,11 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
337
337
|
for (let n of t) await e.delete("_voltro_notification_held", n);
|
|
338
338
|
}
|
|
339
339
|
};
|
|
340
|
-
},
|
|
340
|
+
}, z = (e) => ({
|
|
341
341
|
id: "inApp",
|
|
342
342
|
deliver: async (t) => {
|
|
343
343
|
await e.addInbox({
|
|
344
|
-
id:
|
|
344
|
+
id: k(),
|
|
345
345
|
subjectId: t.to,
|
|
346
346
|
category: t.category,
|
|
347
347
|
title: t.title,
|
|
@@ -351,7 +351,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
351
351
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
352
352
|
});
|
|
353
353
|
}
|
|
354
|
-
}),
|
|
354
|
+
}), B = (e, t) => {
|
|
355
355
|
try {
|
|
356
356
|
let n = new Intl.DateTimeFormat("en-US", {
|
|
357
357
|
timeZone: t,
|
|
@@ -363,16 +363,16 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
363
363
|
} catch {
|
|
364
364
|
return e.getUTCHours() * 60 + e.getUTCMinutes();
|
|
365
365
|
}
|
|
366
|
-
},
|
|
366
|
+
}, V = (e, t) => {
|
|
367
367
|
if (e.startMinute === e.endMinute) return !1;
|
|
368
|
-
let n =
|
|
368
|
+
let n = B(t, e.tz ?? "UTC");
|
|
369
369
|
return e.startMinute < e.endMinute ? n >= e.startMinute && n < e.endMinute : n >= e.startMinute || n < e.endMinute;
|
|
370
|
-
},
|
|
371
|
-
let n =
|
|
370
|
+
}, H = (e, t) => {
|
|
371
|
+
let n = B(t, e.tz ?? "UTC"), r = e.endMinute - n;
|
|
372
372
|
return r <= 0 && (r += 1440), new Date(t.getTime() + r * 6e4);
|
|
373
|
-
},
|
|
373
|
+
}, U = (e) => {
|
|
374
374
|
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 =
|
|
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 = {
|
|
376
376
|
to: i.to,
|
|
377
377
|
category: i.category,
|
|
378
378
|
title: i.title,
|
|
@@ -384,7 +384,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
384
384
|
let a = t.get(n);
|
|
385
385
|
if (!a) continue;
|
|
386
386
|
let o = {
|
|
387
|
-
id:
|
|
387
|
+
id: k(),
|
|
388
388
|
to: i.to,
|
|
389
389
|
category: i.category,
|
|
390
390
|
channel: n,
|
|
@@ -414,17 +414,17 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
414
414
|
};
|
|
415
415
|
}, o = async (t) => {
|
|
416
416
|
let n = await e.store.getQuietHours(t.to), o = new Date(r());
|
|
417
|
-
if (n &&
|
|
417
|
+
if (n && V(n, o)) return (n.policy ?? "hold") === "drop" ? {
|
|
418
418
|
delivered: [],
|
|
419
419
|
failed: [],
|
|
420
420
|
skipped: [],
|
|
421
421
|
held: "quiet-drop"
|
|
422
422
|
} : (await e.store.enqueueHeld({
|
|
423
|
-
id:
|
|
423
|
+
id: k(),
|
|
424
424
|
kind: "quiet",
|
|
425
425
|
subjectId: t.to,
|
|
426
426
|
send: t,
|
|
427
|
-
flushAt:
|
|
427
|
+
flushAt: H(n, o).toISOString()
|
|
428
428
|
}), {
|
|
429
429
|
delivered: [],
|
|
430
430
|
failed: [],
|
|
@@ -434,7 +434,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
434
434
|
if (i > 0 && !t.channels) {
|
|
435
435
|
let n = (await e.store.pendingDigest(t.to))[0]?.flushAt ?? new Date(o.getTime() + i).toISOString();
|
|
436
436
|
return await e.store.enqueueHeld({
|
|
437
|
-
id:
|
|
437
|
+
id: k(),
|
|
438
438
|
kind: "digest",
|
|
439
439
|
subjectId: t.to,
|
|
440
440
|
send: t,
|
|
@@ -488,11 +488,11 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
488
488
|
let t = i.get(e.subjectId) ?? [];
|
|
489
489
|
t.push(e), i.set(e.subjectId, t);
|
|
490
490
|
}
|
|
491
|
-
for (let [e, t] of i) await a(
|
|
491
|
+
for (let [e, t] of i) await a(W(e, t));
|
|
492
492
|
return await e.store.clearHeld(n.map((e) => e.id)), n.length;
|
|
493
493
|
}
|
|
494
494
|
};
|
|
495
|
-
},
|
|
495
|
+
}, W = (e, t) => {
|
|
496
496
|
if (t.length === 1) return t[0].send;
|
|
497
497
|
let n = t.map((e) => ({
|
|
498
498
|
category: e.send.category,
|
|
@@ -507,7 +507,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
507
507
|
data: { items: n },
|
|
508
508
|
tenantId: t[0].send.tenantId ?? null
|
|
509
509
|
};
|
|
510
|
-
},
|
|
510
|
+
}, G = E("_voltro_notification_inbox", {
|
|
511
511
|
id: y({ prefix: "notif" }),
|
|
512
512
|
subjectId: D(),
|
|
513
513
|
category: D(),
|
|
@@ -518,7 +518,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
518
518
|
archivedAt: O().nullable(),
|
|
519
519
|
createdAt: O().default("now"),
|
|
520
520
|
tenantId: D().nullable()
|
|
521
|
-
}).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }),
|
|
521
|
+
}).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }), K = E("_voltro_notification_preferences", {
|
|
522
522
|
id: y({ prefix: "notifpref" }),
|
|
523
523
|
subjectId: D(),
|
|
524
524
|
category: D(),
|
|
@@ -528,7 +528,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
528
528
|
"subjectId",
|
|
529
529
|
"category",
|
|
530
530
|
"channel"
|
|
531
|
-
]),
|
|
531
|
+
]), q = E("_voltro_notification_deliveries", {
|
|
532
532
|
id: y({ prefix: "notifdlv" }),
|
|
533
533
|
recipient: D(),
|
|
534
534
|
category: D(),
|
|
@@ -536,50 +536,57 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
536
536
|
status: D(),
|
|
537
537
|
error: D().nullable(),
|
|
538
538
|
at: O().default("now")
|
|
539
|
-
}).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]),
|
|
539
|
+
}).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]), J = E("_voltro_notification_topic_subscriptions", {
|
|
540
540
|
id: y({ prefix: "notiftsub" }),
|
|
541
541
|
topic: D(),
|
|
542
542
|
subjectId: D(),
|
|
543
543
|
tenantId: D().nullable()
|
|
544
|
-
}).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]),
|
|
544
|
+
}).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]), Y = E("_voltro_notification_quiet_hours", {
|
|
545
545
|
id: y({ prefix: "notifqh" }),
|
|
546
546
|
subjectId: D(),
|
|
547
547
|
startMinute: b(),
|
|
548
548
|
endMinute: b(),
|
|
549
549
|
tz: D().default("UTC"),
|
|
550
550
|
policy: D().default("hold")
|
|
551
|
-
}).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]),
|
|
551
|
+
}).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]), X = E("_voltro_notification_held", {
|
|
552
552
|
id: y({ prefix: "notifheld" }),
|
|
553
553
|
kind: D(),
|
|
554
554
|
subjectId: D(),
|
|
555
555
|
send: S(),
|
|
556
556
|
flushAt: O()
|
|
557
|
-
}).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]),
|
|
557
|
+
}).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]), Z = class extends d.Tag("@voltro/plugin-notifications/NotificationService")() {}, Q = (e) => async (t) => {
|
|
558
558
|
if (e !== void 0) try {
|
|
559
559
|
let n = await e(t);
|
|
560
560
|
if (typeof n == "string" && n !== "") return n;
|
|
561
561
|
} catch {}
|
|
562
562
|
return t.request.subject.id ?? "anonymous";
|
|
563
|
-
},
|
|
564
|
-
let m =
|
|
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
|
|
568
|
+
});
|
|
565
569
|
w({
|
|
570
|
+
source: "plugin",
|
|
566
571
|
table: "_voltro_notification_deliveries",
|
|
567
572
|
timeColumn: "at",
|
|
568
573
|
ttlMs: T(process.env.VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS, 2160),
|
|
569
574
|
envVar: "VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS"
|
|
570
575
|
}), w({
|
|
576
|
+
source: "plugin",
|
|
571
577
|
table: "_voltro_notification_inbox",
|
|
572
578
|
timeColumn: "createdAt",
|
|
573
579
|
ttlMs: T(process.env.VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS, 4320),
|
|
574
580
|
envVar: "VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS",
|
|
575
581
|
where: re("readAt")
|
|
576
582
|
}), w({
|
|
583
|
+
source: "plugin",
|
|
577
584
|
table: "_voltro_notification_held",
|
|
578
585
|
timeColumn: "flushAt",
|
|
579
586
|
ttlMs: T(process.env.VOLTRO_NOTIFICATION_HELD_TTL_HOURS, 168),
|
|
580
587
|
envVar: "VOLTRO_NOTIFICATION_HELD_TTL_HOURS"
|
|
581
588
|
});
|
|
582
|
-
let g = d.store === void 0, _ = { current: d.store ??
|
|
589
|
+
let g = d.store === void 0, _ = { current: d.store ?? I() }, v = {
|
|
583
590
|
addInbox: (e) => _.current.addInbox(e),
|
|
584
591
|
listInbox: (e, t) => _.current.listInbox(e, t),
|
|
585
592
|
markRead: (e, t) => _.current.markRead(e, t),
|
|
@@ -601,7 +608,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
601
608
|
pendingDigest: (e) => _.current.pendingDigest(e),
|
|
602
609
|
dueHeld: (e) => _.current.dueHeld(e),
|
|
603
610
|
clearHeld: (e) => _.current.clearHeld(e)
|
|
604
|
-
}, y = d.channels ?? [
|
|
611
|
+
}, y = d.channels ?? [j()], b = y.some((e) => e.id === "inApp") ? y : [...y, z(v)], x = d.digestWindowMs ?? 0, S = U({
|
|
605
612
|
channels: b,
|
|
606
613
|
store: v,
|
|
607
614
|
digestWindowMs: x
|
|
@@ -662,7 +669,7 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
662
669
|
{
|
|
663
670
|
...u,
|
|
664
671
|
description: "Subscribe the caller to a broadcast topic.",
|
|
665
|
-
execute: (e, t) => f.promise(async () => (await S.subscribe(e.topic, await m(t),
|
|
672
|
+
execute: (e, t) => f.promise(async () => (await S.subscribe(e.topic, await m(t), ce(t)), { ok: !0 }))
|
|
666
673
|
},
|
|
667
674
|
{
|
|
668
675
|
...ne,
|
|
@@ -695,53 +702,54 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
695
702
|
kind: "json",
|
|
696
703
|
status: e,
|
|
697
704
|
data: { error: t }
|
|
698
|
-
}),
|
|
699
|
-
|
|
705
|
+
}), k = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), A = [{
|
|
706
|
+
method: "GET",
|
|
707
|
+
path: "/deliveries",
|
|
708
|
+
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;
|
|
711
|
+
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),
|
|
715
|
+
stats: {
|
|
716
|
+
sent: n,
|
|
717
|
+
failed: r,
|
|
718
|
+
byChannel: t
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
})
|
|
722
|
+
}, {
|
|
723
|
+
method: "GET",
|
|
724
|
+
path: "/inbox",
|
|
725
|
+
description: "In-app inbox for a subject (?subjectId=…).",
|
|
726
|
+
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"));
|
|
732
|
+
}
|
|
733
|
+
}];
|
|
734
|
+
return ie({
|
|
700
735
|
name: h,
|
|
736
|
+
baseName: $,
|
|
701
737
|
description: "Unified notifications — email/slack/sms/push/in-app with preference routing, inbox + delivery log.",
|
|
702
738
|
permissions: ["inspect:read", "store:write"],
|
|
703
|
-
extendSchema: { tables: [
|
|
739
|
+
...d.tables === !1 ? {} : { extendSchema: { tables: [
|
|
740
|
+
G,
|
|
704
741
|
K,
|
|
705
742
|
q,
|
|
706
743
|
J,
|
|
707
744
|
Y,
|
|
708
|
-
X
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
services: p.succeed(Q, S),
|
|
745
|
+
X
|
|
746
|
+
] } },
|
|
747
|
+
services: p.succeed(Z, S),
|
|
712
748
|
routes: E,
|
|
713
749
|
rpcClientDescriptors: o,
|
|
714
|
-
inspectEndpoints:
|
|
715
|
-
method: "GET",
|
|
716
|
-
path: "/deliveries",
|
|
717
|
-
description: "Recent delivery records (newest first) + per-channel + per-status counts.",
|
|
718
|
-
handler: () => f.promise(async () => {
|
|
719
|
-
let e = await v.listDeliveries({ limit: 200 }), t = {}, n = 0, r = 0;
|
|
720
|
-
for (let i of e) t[i.channel] = (t[i.channel] ?? 0) + 1, i.status === "sent" ? n++ : r++;
|
|
721
|
-
return D({
|
|
722
|
-
deliveries: e.slice(0, 100),
|
|
723
|
-
channels: b.map((e) => e.id),
|
|
724
|
-
stats: {
|
|
725
|
-
sent: n,
|
|
726
|
-
failed: r,
|
|
727
|
-
byChannel: t
|
|
728
|
-
}
|
|
729
|
-
});
|
|
730
|
-
})
|
|
731
|
-
}, {
|
|
732
|
-
method: "GET",
|
|
733
|
-
path: "/inbox",
|
|
734
|
-
description: "In-app inbox for a subject (?subjectId=…).",
|
|
735
|
-
handler: (e) => {
|
|
736
|
-
let t = A(e.url, "subjectId");
|
|
737
|
-
return t ? f.promise(async () => D({
|
|
738
|
-
items: await v.listInbox(t, { limit: 100 }),
|
|
739
|
-
unread: await v.unreadCount(t)
|
|
740
|
-
})) : f.succeed(O(400, "subjectId query param required"));
|
|
741
|
-
}
|
|
742
|
-
}],
|
|
750
|
+
inspectEndpoints: A,
|
|
743
751
|
bindDataStore: (e) => {
|
|
744
|
-
g && (_.current =
|
|
752
|
+
g && (_.current = R(e));
|
|
745
753
|
let t = d.flushIntervalMs ?? 3e4;
|
|
746
754
|
C = setInterval(() => {
|
|
747
755
|
S.flushDue().catch(() => {});
|
|
@@ -757,4 +765,4 @@ var j = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
757
765
|
});
|
|
758
766
|
};
|
|
759
767
|
//#endregion
|
|
760
|
-
export {
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-notifications",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "One unified notification surface — email, Slack/webhook, SMS, push, in-app — with per-subject preference routing, an in-app inbox, and a delivery log. NotificationService + typed routes + useInbox/useUnreadCount hooks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"node": ">=24.0.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@voltro/client": "0.
|
|
47
|
-
"@voltro/database": "0.
|
|
48
|
-
"@voltro/protocol": "0.
|
|
46
|
+
"@voltro/client": "0.34.0",
|
|
47
|
+
"@voltro/database": "0.34.0",
|
|
48
|
+
"@voltro/protocol": "0.34.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"effect": "^3.22.0"
|