@voltro/plugin-notifications 0.54.0 → 0.55.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 +142 -2
- package/dist/index.d.ts +21 -8
- package/dist/index.js +169 -169
- package/dist/rpc.d.ts +12 -0
- package/dist/rpc.js +20 -20
- package/dist/web.js +44 -42
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Context as h, Effect as g, Layer as
|
|
3
|
-
import { and as v, boolean as y, eq as b, generateId as x, id as S, integer as C, isNotNull as
|
|
4
|
-
import { definePlugin as
|
|
5
|
-
import { createCipheriv as
|
|
1
|
+
import { BASE_NAME as e, archiveDescriptor as t, clearQuietHoursDescriptor as n, inboxDescriptor as r, markAllReadDescriptor as i, markReadDescriptor as a, markUnreadDescriptor as o, notificationsRpcClientImports as s, preferencesDescriptor as c, setPreferenceDescriptor as l, setQuietHoursDescriptor as u, subscribeDescriptor as d, unarchiveDescriptor as f, unreadCountDescriptor as p, unsubscribeDescriptor as m, webPushPublicKeyDescriptor as ee, webPushRpcClientImports as te, webPushStatusDescriptor as ne, webPushSubscribeDescriptor as re, webPushUnsubscribeDescriptor as ie } from "./rpc.js";
|
|
2
|
+
import { Context as h, Effect as g, Layer as ae, Schema as _ } from "effect";
|
|
3
|
+
import { and as v, boolean as y, eq as b, generateId as x, id as S, integer as C, isNotNull as oe, isNull as w, json as T, lte as E, registerRetention as D, retentionTtlMsFromEnv as O, table as k, text as A, timestamp as j } from "@voltro/database";
|
|
4
|
+
import { definePlugin as se, pluginInstanceName as ce } from "@voltro/protocol";
|
|
5
|
+
import { createCipheriv as le, createDecipheriv as M, createECDH as N, createHash as ue, createHmac as de, createPrivateKey as fe, randomBytes as pe, randomUUID as P, sign as me, timingSafeEqual as he } from "node:crypto";
|
|
6
6
|
//#region src/errors.ts
|
|
7
7
|
var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
8
8
|
token: _.String,
|
|
@@ -12,7 +12,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
12
12
|
deliver: async (e) => {
|
|
13
13
|
process.stdout.write(`[notify:${e.category}] → ${e.to}: ${e.title} — ${e.body}\n`);
|
|
14
14
|
}
|
|
15
|
-
}),
|
|
15
|
+
}), ge = (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 F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
25
25
|
});
|
|
26
26
|
if (!r.ok) throw Error(`webhook ${e.id ?? "slack"} POST ${r.status}`);
|
|
27
27
|
}
|
|
28
|
-
}),
|
|
28
|
+
}), _e = (e) => ({
|
|
29
29
|
id: "email",
|
|
30
30
|
deliver: (t) => e({
|
|
31
31
|
to: t.to,
|
|
@@ -33,13 +33,13 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
33
33
|
html: `<p>${t.body}</p>`,
|
|
34
34
|
text: t.body
|
|
35
35
|
})
|
|
36
|
-
}),
|
|
36
|
+
}), ve = (e) => ({
|
|
37
37
|
id: "sms",
|
|
38
38
|
deliver: (t) => e(t.to, `${t.title}: ${t.body}`)
|
|
39
|
-
}),
|
|
39
|
+
}), ye = (e, t) => ({
|
|
40
40
|
id: e,
|
|
41
41
|
deliver: t
|
|
42
|
-
}),
|
|
42
|
+
}), be = (e) => {
|
|
43
43
|
let t = async (t, n, r) => {
|
|
44
44
|
let i = {
|
|
45
45
|
token: n,
|
|
@@ -172,7 +172,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
172
172
|
for (let e = a.length - 1; e >= 0; e--) t.has(a[e].id) && a.splice(e, 1);
|
|
173
173
|
}
|
|
174
174
|
};
|
|
175
|
-
}, z = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "",
|
|
175
|
+
}, z = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "", xe = (e) => {
|
|
176
176
|
let t = async (t, n, r, i) => await e.query({
|
|
177
177
|
table: t,
|
|
178
178
|
predicate: n,
|
|
@@ -369,7 +369,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
369
369
|
for (let n of t) await e.delete("_voltro_notification_held", n);
|
|
370
370
|
}
|
|
371
371
|
};
|
|
372
|
-
},
|
|
372
|
+
}, Se = (e) => ({
|
|
373
373
|
id: "inApp",
|
|
374
374
|
deliver: async (t) => {
|
|
375
375
|
await e.addInbox({
|
|
@@ -395,14 +395,14 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
395
395
|
} catch {
|
|
396
396
|
return e.getUTCHours() * 60 + e.getUTCMinutes();
|
|
397
397
|
}
|
|
398
|
-
},
|
|
398
|
+
}, Ce = (e, t) => {
|
|
399
399
|
if (e.startMinute === e.endMinute) return !1;
|
|
400
400
|
let n = B(t, e.tz ?? "UTC");
|
|
401
401
|
return e.startMinute < e.endMinute ? n >= e.startMinute && n < e.endMinute : n >= e.startMinute || n < e.endMinute;
|
|
402
|
-
},
|
|
402
|
+
}, we = (e, t) => {
|
|
403
403
|
let n = B(t, e.tz ?? "UTC"), r = e.endMinute - n;
|
|
404
404
|
return r <= 0 && (r += 1440), new Date(t.getTime() + r * 6e4);
|
|
405
|
-
},
|
|
405
|
+
}, Te = (e) => {
|
|
406
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) => {
|
|
407
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 = {
|
|
408
408
|
to: i.to,
|
|
@@ -482,7 +482,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
482
482
|
};
|
|
483
483
|
}, o = async (t) => {
|
|
484
484
|
let n = await e.store.getQuietHours(t.to), o = new Date(r());
|
|
485
|
-
if (n &&
|
|
485
|
+
if (n && Ce(n, o)) return (n.policy ?? "hold") === "drop" ? {
|
|
486
486
|
delivered: [],
|
|
487
487
|
failed: [],
|
|
488
488
|
skipped: [],
|
|
@@ -492,7 +492,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
492
492
|
kind: "quiet",
|
|
493
493
|
subjectId: t.to,
|
|
494
494
|
send: t,
|
|
495
|
-
flushAt:
|
|
495
|
+
flushAt: we(n, o).toISOString()
|
|
496
496
|
}), {
|
|
497
497
|
delivered: [],
|
|
498
498
|
failed: [],
|
|
@@ -556,11 +556,11 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
556
556
|
let t = i.get(e.subjectId) ?? [];
|
|
557
557
|
t.push(e), i.set(e.subjectId, t);
|
|
558
558
|
}
|
|
559
|
-
for (let [e, t] of i) await a(
|
|
559
|
+
for (let [e, t] of i) await a(Ee(e, t));
|
|
560
560
|
return await e.store.clearHeld(n.map((e) => e.id)), n.length;
|
|
561
561
|
}
|
|
562
562
|
};
|
|
563
|
-
},
|
|
563
|
+
}, Ee = (e, t) => {
|
|
564
564
|
if (t.length === 1) return t[0].send;
|
|
565
565
|
let n = t.map((e) => ({
|
|
566
566
|
category: e.send.category,
|
|
@@ -575,7 +575,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
575
575
|
data: { items: n },
|
|
576
576
|
tenantId: t[0].send.tenantId ?? null
|
|
577
577
|
};
|
|
578
|
-
}, V = (e) => e.toString("base64url"), H = (e) => Buffer.from(e, "base64url"), U = (e, t) =>
|
|
578
|
+
}, V = (e) => e.toString("base64url"), H = (e) => Buffer.from(e, "base64url"), U = (e, t) => de("sha256", e).update(t).digest(), W = (e, t, n) => U(e, Buffer.concat([t, Buffer.from([1])])).subarray(0, n), De = () => {
|
|
579
579
|
let e = N("prime256v1");
|
|
580
580
|
return e.generateKeys(), V(e.getPrivateKey());
|
|
581
581
|
}, G = (e) => {
|
|
@@ -583,11 +583,11 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
583
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
584
|
let n = N("prime256v1");
|
|
585
585
|
return n.setPrivateKey(t), V(n.getPublicKey());
|
|
586
|
-
},
|
|
586
|
+
}, Oe = (e) => {
|
|
587
587
|
let t = H(e), n = N("prime256v1");
|
|
588
588
|
n.setPrivateKey(t);
|
|
589
589
|
let r = n.getPublicKey();
|
|
590
|
-
return
|
|
590
|
+
return fe({
|
|
591
591
|
key: {
|
|
592
592
|
kty: "EC",
|
|
593
593
|
crv: "P-256",
|
|
@@ -597,7 +597,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
597
597
|
},
|
|
598
598
|
format: "jwk"
|
|
599
599
|
});
|
|
600
|
-
},
|
|
600
|
+
}, ke = (e, t, n, r = Date.now()) => {
|
|
601
601
|
let i = new URL(e), a = {
|
|
602
602
|
aud: `${i.protocol}//${i.host}`,
|
|
603
603
|
exp: Math.floor(r / 1e3) + 43200,
|
|
@@ -606,45 +606,45 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
606
606
|
typ: "JWT",
|
|
607
607
|
alg: "ES256"
|
|
608
608
|
})))}.${V(Buffer.from(JSON.stringify(a)))}`;
|
|
609
|
-
return `vapid t=${o}.${V(
|
|
610
|
-
key:
|
|
609
|
+
return `vapid t=${o}.${V(me("sha256", Buffer.from(o), {
|
|
610
|
+
key: Oe(t),
|
|
611
611
|
dsaEncoding: "ieee-p1363"
|
|
612
612
|
}))}, k=${G(t)}`;
|
|
613
|
-
},
|
|
613
|
+
}, K = Buffer.from("WebPush: info\0"), Ae = Buffer.from("Content-Encoding: aes128gcm\0"), je = Buffer.from("Content-Encoding: nonce\0"), Me = 4096, Ne = (e, t, n = {}) => {
|
|
614
614
|
let r = H(e.p256dh), i = H(e.auth);
|
|
615
615
|
if (r.length !== 65 || r[0] !== 4) throw Error("push subscription p256dh must be a 65-byte uncompressed P-256 point");
|
|
616
616
|
if (i.length !== 16) throw Error("push subscription auth must be 16 bytes");
|
|
617
617
|
let a = N("prime256v1");
|
|
618
618
|
n.asPrivate ? a.setPrivateKey(n.asPrivate) : a.generateKeys();
|
|
619
619
|
let o = a.getPublicKey(), s = W(U(i, a.computeSecret(r)), Buffer.concat([
|
|
620
|
-
|
|
620
|
+
K,
|
|
621
621
|
r,
|
|
622
622
|
o
|
|
623
|
-
]), 32), c = n.salt ??
|
|
623
|
+
]), 32), c = n.salt ?? pe(16), l = U(c, s), u = W(l, Ae, 16), d = W(l, je, 12), f = le("aes-128-gcm", u, d), p = Buffer.concat([
|
|
624
624
|
f.update(Buffer.concat([t, Buffer.from([2])])),
|
|
625
625
|
f.final(),
|
|
626
626
|
f.getAuthTag()
|
|
627
627
|
]), m = Buffer.alloc(21);
|
|
628
|
-
return c.copy(m, 0), m.writeUInt32BE(
|
|
628
|
+
return c.copy(m, 0), m.writeUInt32BE(Me, 16), m.writeUInt8(o.length, 20), Buffer.concat([
|
|
629
629
|
m,
|
|
630
630
|
o,
|
|
631
631
|
p
|
|
632
632
|
]);
|
|
633
|
-
},
|
|
633
|
+
}, Pe = (e, t) => {
|
|
634
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
635
|
o.setPrivateKey(t.uaPrivate);
|
|
636
636
|
let s = o.getPublicKey(), c = o.computeSecret(i), l = U(n, W(U(t.authSecret, c), Buffer.concat([
|
|
637
|
-
|
|
637
|
+
K,
|
|
638
638
|
s,
|
|
639
639
|
i
|
|
640
|
-
]), 32)), u = W(l,
|
|
640
|
+
]), 32)), u = W(l, Ae, 16), d = W(l, je, 12), f = M("aes-128-gcm", u, d);
|
|
641
641
|
f.setAuthTag(a.subarray(a.length - 16));
|
|
642
642
|
let p = Buffer.concat([f.update(a.subarray(0, a.length - 16)), f.final()]), m = p.length - 1;
|
|
643
643
|
for (; m >= 0 && p[m] === 0;) m--;
|
|
644
644
|
if (p[m] !== 2) throw Error("web-push record missing the last-record delimiter");
|
|
645
645
|
return p.subarray(0, m);
|
|
646
|
-
},
|
|
647
|
-
let r =
|
|
646
|
+
}, Fe = async (e, t, n) => {
|
|
647
|
+
let r = Ne(e.keys, t), i = n.fetchImpl ?? fetch, a;
|
|
648
648
|
try {
|
|
649
649
|
a = await i(e.endpoint, {
|
|
650
650
|
method: "POST",
|
|
@@ -653,7 +653,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
653
653
|
"content-type": "application/octet-stream",
|
|
654
654
|
ttl: String(n.ttl ?? 86400),
|
|
655
655
|
urgency: "normal",
|
|
656
|
-
authorization:
|
|
656
|
+
authorization: ke(e.endpoint, n.privateKey, n.contact)
|
|
657
657
|
},
|
|
658
658
|
body: new Uint8Array(r)
|
|
659
659
|
});
|
|
@@ -678,10 +678,10 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
678
678
|
status: a.status,
|
|
679
679
|
detail: o.slice(0, 200)
|
|
680
680
|
};
|
|
681
|
-
},
|
|
681
|
+
}, Ie = (e, t) => {
|
|
682
682
|
let n = Buffer.from(e), r = Buffer.from(t);
|
|
683
|
-
return n.length === r.length &&
|
|
684
|
-
},
|
|
683
|
+
return n.length === r.length && he(n, r);
|
|
684
|
+
}, Le = k("_voltro_notification_push_subscriptions", {
|
|
685
685
|
id: S({ prefix: "notifpush" }),
|
|
686
686
|
subjectId: A(),
|
|
687
687
|
endpointHash: A(),
|
|
@@ -691,7 +691,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
691
691
|
ua: A().nullable(),
|
|
692
692
|
createdAt: j().default("now"),
|
|
693
693
|
tenantId: A().nullable()
|
|
694
|
-
}).unique("byPushEndpointHash", ["endpointHash"]).index("byPushSubject", ["subjectId"]), q = (e) =>
|
|
694
|
+
}).unique("byPushEndpointHash", ["endpointHash"]).index("byPushSubject", ["subjectId"]), q = (e) => ue("sha256").update(e).digest("hex"), J = () => {
|
|
695
695
|
let e = /* @__PURE__ */ new Map();
|
|
696
696
|
return {
|
|
697
697
|
upsert: async (t) => {
|
|
@@ -707,7 +707,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
707
707
|
},
|
|
708
708
|
countForSubject: async (t) => [...e.values()].filter((e) => e.subjectId === t).length
|
|
709
709
|
};
|
|
710
|
-
}, Y = "_voltro_notification_push_subscriptions",
|
|
710
|
+
}, Y = "_voltro_notification_push_subscriptions", Re = (e) => {
|
|
711
711
|
let t = async (t, n) => e.query({
|
|
712
712
|
table: Y,
|
|
713
713
|
predicate: t,
|
|
@@ -761,23 +761,23 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
761
761
|
},
|
|
762
762
|
countForSubject: async (e) => (await t(b("subjectId", e))).length
|
|
763
763
|
};
|
|
764
|
-
},
|
|
764
|
+
}, ze = Symbol.for("@voltro/plugin-notifications:web-push"), X = () => {
|
|
765
765
|
let e = globalThis;
|
|
766
|
-
return e[
|
|
766
|
+
return e[ze] ??= {
|
|
767
767
|
store: J(),
|
|
768
768
|
bound: !1
|
|
769
769
|
};
|
|
770
|
-
},
|
|
770
|
+
}, Be = (e) => {
|
|
771
771
|
let t = X();
|
|
772
|
-
t.store =
|
|
773
|
-
},
|
|
772
|
+
t.store = Re(e), t.bound = !0;
|
|
773
|
+
}, Ve = () => {
|
|
774
774
|
let e = X();
|
|
775
775
|
e.store = J(), e.bound = !1;
|
|
776
776
|
}, Z = (e) => {
|
|
777
777
|
let t = e ?? process.env.VOLTRO_VAPID_PRIVATE_KEY;
|
|
778
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
779
|
return t;
|
|
780
|
-
}, Q = 3800,
|
|
780
|
+
}, Q = 3800, He = (e, t) => {
|
|
781
781
|
let n = typeof e.data?.url == "string" ? e.data.url : void 0, r = {
|
|
782
782
|
title: e.title,
|
|
783
783
|
body: e.body,
|
|
@@ -794,31 +794,31 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
794
794
|
...o,
|
|
795
795
|
body: `${l}…`
|
|
796
796
|
}));
|
|
797
|
-
},
|
|
797
|
+
}, Ue = (e = {}) => (X().channelOptions = e, Ge(e)), We = () => {
|
|
798
798
|
let e = X().channelOptions;
|
|
799
799
|
G(Z(e?.privateKey));
|
|
800
|
-
},
|
|
800
|
+
}, Ge = (e) => ({
|
|
801
801
|
id: e.id ?? "webPush",
|
|
802
802
|
deliver: async (t) => {
|
|
803
|
-
let n = await
|
|
803
|
+
let n = await Ke(t, e), r = n.filter((e) => e.status === "failed");
|
|
804
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
805
|
},
|
|
806
|
-
deliverDetailed: (t) =>
|
|
807
|
-
}),
|
|
806
|
+
deliverDetailed: (t) => Ke(t, e)
|
|
807
|
+
}), Ke = async (e, t) => {
|
|
808
808
|
let n = Z(t.privateKey), r = await X().store.listForSubject(e.to), i = [];
|
|
809
809
|
for (let a of r) {
|
|
810
|
-
let r =
|
|
810
|
+
let r = pe(16).toString("base64url"), o = {
|
|
811
811
|
privateKey: n,
|
|
812
812
|
...t.contact === void 0 ? {} : { contact: t.contact },
|
|
813
813
|
...t.ttl === void 0 ? {} : { ttl: t.ttl },
|
|
814
814
|
...t.fetchImpl === void 0 ? {} : { fetchImpl: t.fetchImpl }
|
|
815
|
-
}, s = await
|
|
815
|
+
}, s = await Fe({
|
|
816
816
|
endpoint: a.endpoint,
|
|
817
817
|
keys: {
|
|
818
818
|
p256dh: a.p256dh,
|
|
819
819
|
auth: a.auth
|
|
820
820
|
}
|
|
821
|
-
},
|
|
821
|
+
}, He(e, r), o);
|
|
822
822
|
s.kind === "delivered" ? i.push({
|
|
823
823
|
endpoint: a.endpoint,
|
|
824
824
|
status: "sent",
|
|
@@ -835,7 +835,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
835
835
|
});
|
|
836
836
|
}
|
|
837
837
|
return i;
|
|
838
|
-
},
|
|
838
|
+
}, qe = (e) => G(Z(e)), Je = k("_voltro_notification_inbox", {
|
|
839
839
|
id: S({ prefix: "notif" }),
|
|
840
840
|
subjectId: A(),
|
|
841
841
|
category: A(),
|
|
@@ -846,7 +846,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
846
846
|
archivedAt: j().nullable(),
|
|
847
847
|
createdAt: j().default("now"),
|
|
848
848
|
tenantId: A().nullable()
|
|
849
|
-
}).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }),
|
|
849
|
+
}).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }), Ye = k("_voltro_notification_preferences", {
|
|
850
850
|
id: S({ prefix: "notifpref" }),
|
|
851
851
|
subjectId: A(),
|
|
852
852
|
category: A(),
|
|
@@ -856,7 +856,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
856
856
|
"subjectId",
|
|
857
857
|
"category",
|
|
858
858
|
"channel"
|
|
859
|
-
]),
|
|
859
|
+
]), Xe = k("_voltro_notification_deliveries", {
|
|
860
860
|
id: S({ prefix: "notifdlv" }),
|
|
861
861
|
recipient: A(),
|
|
862
862
|
category: A(),
|
|
@@ -867,35 +867,35 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
867
867
|
endpoint: A().nullable(),
|
|
868
868
|
clickToken: A().nullable(),
|
|
869
869
|
clickedAt: j().nullable()
|
|
870
|
-
}).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]).index("byDeliveriesClickToken", ["clickToken"]),
|
|
870
|
+
}).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]).index("byDeliveriesClickToken", ["clickToken"]), Ze = k("_voltro_notification_topic_subscriptions", {
|
|
871
871
|
id: S({ prefix: "notiftsub" }),
|
|
872
872
|
topic: A(),
|
|
873
873
|
subjectId: A(),
|
|
874
874
|
tenantId: A().nullable()
|
|
875
|
-
}).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]),
|
|
875
|
+
}).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]), Qe = k("_voltro_notification_quiet_hours", {
|
|
876
876
|
id: S({ prefix: "notifqh" }),
|
|
877
877
|
subjectId: A(),
|
|
878
878
|
startMinute: C(),
|
|
879
879
|
endMinute: C(),
|
|
880
880
|
tz: A().default("UTC"),
|
|
881
881
|
policy: A().default("hold")
|
|
882
|
-
}).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]),
|
|
882
|
+
}).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]), $e = k("_voltro_notification_held", {
|
|
883
883
|
id: S({ prefix: "notifheld" }),
|
|
884
884
|
kind: A(),
|
|
885
885
|
subjectId: A(),
|
|
886
886
|
send: T(),
|
|
887
887
|
flushAt: j()
|
|
888
|
-
}).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]),
|
|
888
|
+
}).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]), et = class extends h.Tag("@voltro/plugin-notifications/NotificationService")() {}, tt = (e) => async (t) => {
|
|
889
889
|
if (e !== void 0) try {
|
|
890
890
|
let n = await e(t);
|
|
891
891
|
if (typeof n == "string" && n !== "") return n;
|
|
892
892
|
} catch {}
|
|
893
893
|
return t.request.subject.id ?? "anonymous";
|
|
894
|
-
},
|
|
895
|
-
let
|
|
896
|
-
base:
|
|
897
|
-
alias:
|
|
898
|
-
instance:
|
|
894
|
+
}, nt = (e) => e.request.subject.tenantId ?? null, $ = e, rt = (e = {}) => {
|
|
895
|
+
let h = tt(e.resolveSubjectId), _ = ce({
|
|
896
|
+
base: $,
|
|
897
|
+
alias: e.alias,
|
|
898
|
+
instance: e.name
|
|
899
899
|
});
|
|
900
900
|
D({
|
|
901
901
|
source: "plugin",
|
|
@@ -909,7 +909,7 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
909
909
|
timeColumn: "createdAt",
|
|
910
910
|
ttlMs: O(process.env.VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS, 4320),
|
|
911
911
|
envVar: "VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS",
|
|
912
|
-
where:
|
|
912
|
+
where: oe("readAt")
|
|
913
913
|
}), D({
|
|
914
914
|
source: "plugin",
|
|
915
915
|
table: "_voltro_notification_held",
|
|
@@ -917,81 +917,81 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
917
917
|
ttlMs: O(process.env.VOLTRO_NOTIFICATION_HELD_TTL_HOURS, 168),
|
|
918
918
|
envVar: "VOLTRO_NOTIFICATION_HELD_TTL_HOURS"
|
|
919
919
|
});
|
|
920
|
-
let
|
|
921
|
-
addInbox: (e) =>
|
|
922
|
-
listInbox: (e, t) =>
|
|
923
|
-
markRead: (e, t) =>
|
|
924
|
-
markUnread: (e, t) =>
|
|
925
|
-
markAllRead: (e) =>
|
|
926
|
-
setArchived: (e, t, n) =>
|
|
927
|
-
unreadCount: (e) =>
|
|
928
|
-
getPreferences: (e) =>
|
|
929
|
-
setPreference: (e) =>
|
|
930
|
-
recordDelivery: (e) =>
|
|
931
|
-
listDeliveries: (e) =>
|
|
932
|
-
markClicked: (e) =>
|
|
933
|
-
subscribeTopic: (e) =>
|
|
934
|
-
unsubscribeTopic: (e, t) =>
|
|
935
|
-
listTopicSubscribers: (e) =>
|
|
936
|
-
getQuietHours: (e) =>
|
|
937
|
-
setQuietHours: (e) =>
|
|
938
|
-
clearQuietHours: (e) =>
|
|
939
|
-
enqueueHeld: (e) =>
|
|
940
|
-
pendingDigest: (e) =>
|
|
941
|
-
dueHeld: (e) =>
|
|
942
|
-
clearHeld: (e) =>
|
|
943
|
-
},
|
|
944
|
-
channels:
|
|
945
|
-
store:
|
|
946
|
-
digestWindowMs:
|
|
947
|
-
}),
|
|
920
|
+
let v = e.store === void 0, y = { current: e.store ?? R() }, b = {
|
|
921
|
+
addInbox: (e) => y.current.addInbox(e),
|
|
922
|
+
listInbox: (e, t) => y.current.listInbox(e, t),
|
|
923
|
+
markRead: (e, t) => y.current.markRead(e, t),
|
|
924
|
+
markUnread: (e, t) => y.current.markUnread(e, t),
|
|
925
|
+
markAllRead: (e) => y.current.markAllRead(e),
|
|
926
|
+
setArchived: (e, t, n) => y.current.setArchived(e, t, n),
|
|
927
|
+
unreadCount: (e) => y.current.unreadCount(e),
|
|
928
|
+
getPreferences: (e) => y.current.getPreferences(e),
|
|
929
|
+
setPreference: (e) => y.current.setPreference(e),
|
|
930
|
+
recordDelivery: (e) => y.current.recordDelivery(e),
|
|
931
|
+
listDeliveries: (e) => y.current.listDeliveries(e),
|
|
932
|
+
markClicked: (e) => y.current.markClicked(e),
|
|
933
|
+
subscribeTopic: (e) => y.current.subscribeTopic(e),
|
|
934
|
+
unsubscribeTopic: (e, t) => y.current.unsubscribeTopic(e, t),
|
|
935
|
+
listTopicSubscribers: (e) => y.current.listTopicSubscribers(e),
|
|
936
|
+
getQuietHours: (e) => y.current.getQuietHours(e),
|
|
937
|
+
setQuietHours: (e) => y.current.setQuietHours(e),
|
|
938
|
+
clearQuietHours: (e) => y.current.clearQuietHours(e),
|
|
939
|
+
enqueueHeld: (e) => y.current.enqueueHeld(e),
|
|
940
|
+
pendingDigest: (e) => y.current.pendingDigest(e),
|
|
941
|
+
dueHeld: (e) => y.current.dueHeld(e),
|
|
942
|
+
clearHeld: (e) => y.current.clearHeld(e)
|
|
943
|
+
}, x = e.channels ?? [I()], S = x.some((e) => e.id === "inApp") ? x : [...x, Se(b)], C = e.digestWindowMs ?? 0, w = S.some((e) => e.id === "webPush"), T = Te({
|
|
944
|
+
channels: S,
|
|
945
|
+
store: b,
|
|
946
|
+
digestWindowMs: C
|
|
947
|
+
}), E, k = [
|
|
948
948
|
{
|
|
949
|
-
...
|
|
949
|
+
...r,
|
|
950
950
|
description: "The caller's in-app inbox items.",
|
|
951
|
-
execute: (e, t) => g.promise(async () =>
|
|
951
|
+
execute: (e, t) => g.promise(async () => T.inbox(await h(t), e))
|
|
952
952
|
},
|
|
953
953
|
{
|
|
954
|
-
...
|
|
954
|
+
...p,
|
|
955
955
|
description: "The caller's unread count.",
|
|
956
|
-
execute: (e, t) => g.promise(async () => ({ count: await
|
|
956
|
+
execute: (e, t) => g.promise(async () => ({ count: await T.unreadCount(await h(t)) }))
|
|
957
957
|
},
|
|
958
958
|
{
|
|
959
|
-
...
|
|
959
|
+
...a,
|
|
960
960
|
description: "Mark an inbox item read.",
|
|
961
|
-
execute: (e, t) => g.promise(async () => ({ ok: await
|
|
961
|
+
execute: (e, t) => g.promise(async () => ({ ok: await T.markRead(e.id, await h(t)) }))
|
|
962
962
|
},
|
|
963
963
|
{
|
|
964
|
-
...
|
|
964
|
+
...o,
|
|
965
965
|
description: "Mark an inbox item unread again.",
|
|
966
|
-
execute: (e, t) => g.promise(async () => ({ ok: await
|
|
966
|
+
execute: (e, t) => g.promise(async () => ({ ok: await T.markUnread(e.id, await h(t)) }))
|
|
967
967
|
},
|
|
968
968
|
{
|
|
969
|
-
...
|
|
969
|
+
...i,
|
|
970
970
|
description: "Mark every unread item in the caller's inbox read.",
|
|
971
|
-
execute: (e, t) => g.promise(async () => ({ count: await
|
|
971
|
+
execute: (e, t) => g.promise(async () => ({ count: await T.markAllRead(await h(t)) }))
|
|
972
972
|
},
|
|
973
973
|
{
|
|
974
|
-
...
|
|
974
|
+
...t,
|
|
975
975
|
description: "Archive an inbox item — a different state from read; this is what empties the inbox.",
|
|
976
|
-
execute: (e, t) => g.promise(async () => ({ ok: await
|
|
976
|
+
execute: (e, t) => g.promise(async () => ({ ok: await T.setArchived(e.id, await h(t), !0) }))
|
|
977
977
|
},
|
|
978
978
|
{
|
|
979
|
-
...
|
|
979
|
+
...f,
|
|
980
980
|
description: "Return an archived item to the inbox.",
|
|
981
|
-
execute: (e, t) => g.promise(async () => ({ ok: await
|
|
981
|
+
execute: (e, t) => g.promise(async () => ({ ok: await T.setArchived(e.id, await h(t), !1) }))
|
|
982
982
|
},
|
|
983
983
|
{
|
|
984
|
-
...
|
|
984
|
+
...c,
|
|
985
985
|
description: "The caller's channel preferences.",
|
|
986
|
-
execute: (e, t) => g.promise(async () =>
|
|
986
|
+
execute: (e, t) => g.promise(async () => T.getPreferences(await h(t)))
|
|
987
987
|
},
|
|
988
988
|
{
|
|
989
|
-
...
|
|
989
|
+
...l,
|
|
990
990
|
description: "Toggle a channel on/off for a category.",
|
|
991
991
|
execute: (e, t) => g.promise(async () => {
|
|
992
992
|
let { category: n, channel: r, enabled: i } = e;
|
|
993
|
-
return await
|
|
994
|
-
subjectId: await
|
|
993
|
+
return await T.setPreference({
|
|
994
|
+
subjectId: await h(t),
|
|
995
995
|
category: n,
|
|
996
996
|
channel: r,
|
|
997
997
|
enabled: i
|
|
@@ -999,22 +999,22 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
999
999
|
})
|
|
1000
1000
|
},
|
|
1001
1001
|
{
|
|
1002
|
-
...
|
|
1002
|
+
...d,
|
|
1003
1003
|
description: "Subscribe the caller to a broadcast topic.",
|
|
1004
|
-
execute: (e, t) => g.promise(async () => (await
|
|
1004
|
+
execute: (e, t) => g.promise(async () => (await T.subscribe(e.topic, await h(t), nt(t)), { ok: !0 }))
|
|
1005
1005
|
},
|
|
1006
1006
|
{
|
|
1007
|
-
...
|
|
1007
|
+
...m,
|
|
1008
1008
|
description: "Unsubscribe the caller from a broadcast topic.",
|
|
1009
|
-
execute: (e, t) => g.promise(async () => (await
|
|
1009
|
+
execute: (e, t) => g.promise(async () => (await T.unsubscribe(e.topic, await h(t)), { ok: !0 }))
|
|
1010
1010
|
},
|
|
1011
1011
|
{
|
|
1012
|
-
...
|
|
1012
|
+
...u,
|
|
1013
1013
|
description: "Set the caller's quiet-hours (DND) window.",
|
|
1014
1014
|
execute: (e, t) => g.promise(async () => {
|
|
1015
1015
|
let n = e;
|
|
1016
|
-
return await
|
|
1017
|
-
subjectId: await
|
|
1016
|
+
return await T.setQuietHours({
|
|
1017
|
+
subjectId: await h(t),
|
|
1018
1018
|
startMinute: n.startMinute,
|
|
1019
1019
|
endMinute: n.endMinute,
|
|
1020
1020
|
...n.tz ? { tz: n.tz } : {},
|
|
@@ -1023,60 +1023,60 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
1023
1023
|
})
|
|
1024
1024
|
},
|
|
1025
1025
|
{
|
|
1026
|
-
...
|
|
1026
|
+
...n,
|
|
1027
1027
|
description: "Clear the caller's quiet-hours window.",
|
|
1028
|
-
execute: (e, t) => g.promise(async () => (await
|
|
1028
|
+
execute: (e, t) => g.promise(async () => (await T.clearQuietHours(await h(t)), { ok: !0 }))
|
|
1029
1029
|
}
|
|
1030
|
-
],
|
|
1030
|
+
], A = w ? [
|
|
1031
1031
|
{
|
|
1032
|
-
...
|
|
1032
|
+
...ee,
|
|
1033
1033
|
description: "The VAPID application-server key a subscribing browser needs.",
|
|
1034
|
-
execute: () => g.sync(() => ({ key:
|
|
1034
|
+
execute: () => g.sync(() => ({ key: qe(X().channelOptions?.privateKey) }))
|
|
1035
1035
|
},
|
|
1036
1036
|
{
|
|
1037
|
-
...
|
|
1037
|
+
...re,
|
|
1038
1038
|
description: "Register the calling subject's browser push subscription (one row per endpoint).",
|
|
1039
1039
|
execute: (e, t) => g.promise(async () => {
|
|
1040
1040
|
let n = e;
|
|
1041
1041
|
return await X().store.upsert({
|
|
1042
|
-
subjectId: await
|
|
1042
|
+
subjectId: await h(t),
|
|
1043
1043
|
endpoint: n.endpoint,
|
|
1044
1044
|
endpointHash: q(n.endpoint),
|
|
1045
1045
|
p256dh: n.p256dh,
|
|
1046
1046
|
auth: n.auth,
|
|
1047
1047
|
ua: n.ua ?? null,
|
|
1048
|
-
tenantId:
|
|
1048
|
+
tenantId: nt(t)
|
|
1049
1049
|
}), { ok: !0 };
|
|
1050
1050
|
})
|
|
1051
1051
|
},
|
|
1052
1052
|
{
|
|
1053
|
-
...
|
|
1053
|
+
...ie,
|
|
1054
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
|
|
1055
|
+
execute: (e, t) => g.promise(async () => ({ ok: await X().store.removeForSubject(q(e.endpoint), await h(t)) }))
|
|
1056
1056
|
},
|
|
1057
1057
|
{
|
|
1058
|
-
...
|
|
1058
|
+
...ne,
|
|
1059
1059
|
description: "How many push endpoints the calling subject has registered.",
|
|
1060
|
-
execute: (e, t) => g.promise(async () => ({ endpoints: await X().store.countForSubject(await
|
|
1060
|
+
execute: (e, t) => g.promise(async () => ({ endpoints: await X().store.countForSubject(await h(t)) }))
|
|
1061
1061
|
}
|
|
1062
|
-
] : [],
|
|
1062
|
+
] : [], j = (e) => ({
|
|
1063
1063
|
kind: "json",
|
|
1064
1064
|
data: e
|
|
1065
|
-
}),
|
|
1065
|
+
}), le = (e, t) => ({
|
|
1066
1066
|
kind: "json",
|
|
1067
1067
|
status: e,
|
|
1068
1068
|
data: { error: t }
|
|
1069
|
-
}),
|
|
1069
|
+
}), M = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), N = [{
|
|
1070
1070
|
method: "GET",
|
|
1071
1071
|
path: "/deliveries",
|
|
1072
1072
|
description: "Recent delivery records (newest first) + per-channel + per-status counts.",
|
|
1073
1073
|
handler: () => g.promise(async () => {
|
|
1074
|
-
let e = await
|
|
1074
|
+
let e = await b.listDeliveries({ limit: 200 }), t = {}, n = 0, r = 0;
|
|
1075
1075
|
for (let i of e) t[i.channel] = (t[i.channel] ?? 0) + 1, i.status === "sent" ? n++ : r++;
|
|
1076
1076
|
let i = e.slice(0, 100).map(({ clickToken: e, ...t }) => t);
|
|
1077
|
-
return
|
|
1077
|
+
return j({
|
|
1078
1078
|
deliveries: i,
|
|
1079
|
-
channels:
|
|
1079
|
+
channels: S.map((e) => e.id),
|
|
1080
1080
|
stats: {
|
|
1081
1081
|
sent: n,
|
|
1082
1082
|
failed: r,
|
|
@@ -1089,28 +1089,28 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
1089
1089
|
path: "/inbox",
|
|
1090
1090
|
description: "In-app inbox for a subject (?subjectId=…).",
|
|
1091
1091
|
handler: (e) => {
|
|
1092
|
-
let t =
|
|
1093
|
-
return t ? g.promise(async () =>
|
|
1094
|
-
items: await
|
|
1095
|
-
unread: await
|
|
1096
|
-
})) : g.succeed(
|
|
1092
|
+
let t = M(e.url, "subjectId");
|
|
1093
|
+
return t ? g.promise(async () => j({
|
|
1094
|
+
items: await b.listInbox(t, { limit: 100 }),
|
|
1095
|
+
unread: await b.unreadCount(t)
|
|
1096
|
+
})) : g.succeed(le(400, "subjectId query param required"));
|
|
1097
1097
|
}
|
|
1098
1098
|
}];
|
|
1099
|
-
return
|
|
1100
|
-
name:
|
|
1101
|
-
baseName:
|
|
1099
|
+
return se({
|
|
1100
|
+
name: _,
|
|
1101
|
+
baseName: $,
|
|
1102
1102
|
description: "Unified notifications — email/slack/sms/push/in-app with preference routing, inbox + delivery log.",
|
|
1103
1103
|
permissions: ["inspect:read", "store:write"],
|
|
1104
|
-
...
|
|
1105
|
-
qe,
|
|
1104
|
+
...e.tables === !1 ? {} : { extendSchema: { tables: [
|
|
1106
1105
|
Je,
|
|
1107
1106
|
Ye,
|
|
1108
1107
|
Xe,
|
|
1109
|
-
$,
|
|
1110
1108
|
Ze,
|
|
1111
|
-
|
|
1109
|
+
Qe,
|
|
1110
|
+
$e,
|
|
1111
|
+
Le
|
|
1112
1112
|
] } },
|
|
1113
|
-
...
|
|
1113
|
+
...w ? {
|
|
1114
1114
|
declaredEnv: [{
|
|
1115
1115
|
name: "VOLTRO_VAPID_PRIVATE_KEY",
|
|
1116
1116
|
required: !0,
|
|
@@ -1126,12 +1126,12 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
1126
1126
|
try {
|
|
1127
1127
|
t = JSON.parse(Buffer.from(e.rawBody).toString("utf8")).token;
|
|
1128
1128
|
} catch {}
|
|
1129
|
-
if (typeof t != "string" || t === "" || !
|
|
1129
|
+
if (typeof t != "string" || t === "" || !Ie(t, t)) return {
|
|
1130
1130
|
status: 400,
|
|
1131
1131
|
body: JSON.stringify({ error: "token required" }),
|
|
1132
1132
|
contentType: "application/json"
|
|
1133
1133
|
};
|
|
1134
|
-
let n = await
|
|
1134
|
+
let n = await b.markClicked(t);
|
|
1135
1135
|
return {
|
|
1136
1136
|
status: n ? 200 : 404,
|
|
1137
1137
|
body: JSON.stringify({ ok: n }),
|
|
@@ -1141,27 +1141,27 @@ var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
|
1141
1141
|
maxBodyBytes: 4096
|
|
1142
1142
|
}]
|
|
1143
1143
|
} : {},
|
|
1144
|
-
services:
|
|
1145
|
-
routes: [...
|
|
1146
|
-
rpcClientDescriptors:
|
|
1147
|
-
inspectEndpoints:
|
|
1148
|
-
bindDataStore: (
|
|
1149
|
-
|
|
1150
|
-
let
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
},
|
|
1144
|
+
services: ae.succeed(et, T),
|
|
1145
|
+
routes: [...k, ...A],
|
|
1146
|
+
rpcClientDescriptors: w ? [...s, ...te] : s,
|
|
1147
|
+
inspectEndpoints: N,
|
|
1148
|
+
bindDataStore: (t) => {
|
|
1149
|
+
v && (y.current = xe(t)), Be(t);
|
|
1150
|
+
let n = e.flushIntervalMs ?? 3e4;
|
|
1151
|
+
E = setInterval(() => {
|
|
1152
|
+
T.flushDue().catch(() => {});
|
|
1153
|
+
}, n), typeof E.unref == "function" && E.unref();
|
|
1154
1154
|
},
|
|
1155
1155
|
onActivate: (e) => g.sync(() => {
|
|
1156
|
-
|
|
1157
|
-
channels:
|
|
1158
|
-
digestWindowMs:
|
|
1156
|
+
w && We(), e.logger.info("notifications active", {
|
|
1157
|
+
channels: S.map((e) => e.id),
|
|
1158
|
+
digestWindowMs: C
|
|
1159
1159
|
});
|
|
1160
1160
|
}),
|
|
1161
1161
|
onDeactivate: () => g.sync(() => {
|
|
1162
|
-
|
|
1162
|
+
E && clearInterval(E), E = void 0;
|
|
1163
1163
|
})
|
|
1164
1164
|
});
|
|
1165
1165
|
};
|
|
1166
1166
|
//#endregion
|
|
1167
|
-
export {
|
|
1167
|
+
export { et as NotificationService, F as PushTokenRejected, Te as buildNotificationService, I as consoleChannel, ye as customChannel, xe as dataStoreNotificationStore, Re as dataStorePushSubscriptionStore, Pe as decryptWebPushPayload, Xe as deliveriesTable, _e as emailChannel, Ne as encryptWebPushPayload, q as endpointHashOf, De as generateVapidPrivateKey, $e as heldTable, Se as inAppChannel, Ce as inQuietHours, Je as inboxTable, tt as makeSubjectId, R as memoryNotificationStore, J as memoryPushSubscriptionStore, B as minuteOfDayInZone, rt as notificationsPlugin, Ye as preferencesTable, be as pushChannel, Le as pushSubscriptionsTable, we as quietHoursEnd, Qe as quietHoursTable, Ve as resetWebPushStoreForTest, L as resolveChannels, Fe as sendWebPush, ve as smsChannel, Ze as topicSubscriptionsTable, ke as vapidAuthorization, G as vapidPublicKeyFor, Ue as webPushChannel, qe as webPushPublicKey, ge as webhookChannel };
|