@voltro/plugin-notifications 0.86.0 → 0.87.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 +5321 -0
- package/THIRD-PARTY-NOTICES.md +1132 -13
- package/dist/index.d.ts +18 -0
- package/dist/index.js +137 -136
- package/dist/rpc.d.ts +17 -1
- package/dist/rpc.js +27 -21
- package/dist/service.js +60 -26
- package/package.json +4 -4
package/dist/rpc.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { Schema as e } from "effect";
|
|
2
2
|
import { defineMutation as t, defineQuery as n, reactivityChannel as r } from "@voltro/protocol";
|
|
3
3
|
//#region src/rpc.ts
|
|
4
|
-
var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o =
|
|
4
|
+
var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = r("notifications-preferences"), s = e.Struct({
|
|
5
|
+
subjectId: e.String,
|
|
6
|
+
category: e.String,
|
|
7
|
+
channel: e.String,
|
|
8
|
+
enabled: e.Boolean
|
|
9
|
+
}), c = n({
|
|
5
10
|
name: "notifications.inbox",
|
|
6
11
|
source: a,
|
|
7
12
|
input: e.Struct({
|
|
@@ -10,43 +15,44 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
|
|
|
10
15
|
}),
|
|
11
16
|
output: e.Array(e.Record(e.String, e.Unknown)),
|
|
12
17
|
openAccess: "self-scoped read: returns only the calling subject's own inbox items"
|
|
13
|
-
}),
|
|
18
|
+
}), l = n({
|
|
14
19
|
name: "notifications.unreadCount",
|
|
15
20
|
source: a,
|
|
16
21
|
input: e.Struct({}),
|
|
17
22
|
output: e.Struct({ count: e.Number }),
|
|
18
23
|
openAccess: "self-scoped read: counts only the calling subject's own unread items"
|
|
19
|
-
}),
|
|
24
|
+
}), u = t({
|
|
20
25
|
name: "notifications.markRead",
|
|
21
26
|
input: e.Struct({ id: e.String }),
|
|
22
27
|
output: e.Struct({ ok: e.Boolean }),
|
|
23
28
|
openAccess: "self-scoped write: marks an item read only within the calling subject's own inbox"
|
|
24
|
-
}),
|
|
29
|
+
}), d = t({
|
|
25
30
|
name: "notifications.markUnread",
|
|
26
31
|
input: e.Struct({ id: e.String }),
|
|
27
32
|
output: e.Struct({ ok: e.Boolean }),
|
|
28
33
|
openAccess: "self-scoped write: marks an item unread only within the calling subject's own inbox"
|
|
29
|
-
}),
|
|
34
|
+
}), f = t({
|
|
30
35
|
name: "notifications.markAllRead",
|
|
31
36
|
input: e.Struct({}),
|
|
32
37
|
output: e.Struct({ count: e.Number }),
|
|
33
38
|
openAccess: "self-scoped write: clears unread state only in the calling subject's own inbox"
|
|
34
|
-
}),
|
|
39
|
+
}), p = t({
|
|
35
40
|
name: "notifications.archive",
|
|
36
41
|
input: e.Struct({ id: e.String }),
|
|
37
42
|
output: e.Struct({ ok: e.Boolean }),
|
|
38
43
|
openAccess: "self-scoped write: archives an item only within the calling subject's own inbox"
|
|
39
|
-
}),
|
|
44
|
+
}), m = t({
|
|
40
45
|
name: "notifications.unarchive",
|
|
41
46
|
input: e.Struct({ id: e.String }),
|
|
42
47
|
output: e.Struct({ ok: e.Boolean }),
|
|
43
48
|
openAccess: "self-scoped write: unarchives an item only within the calling subject's own inbox"
|
|
44
|
-
}),
|
|
49
|
+
}), h = n({
|
|
45
50
|
name: "notifications.preferences",
|
|
51
|
+
source: o,
|
|
46
52
|
input: e.Struct({}),
|
|
47
|
-
output: e.Array(
|
|
53
|
+
output: e.Array(s),
|
|
48
54
|
openAccess: "self-scoped read: returns only the calling subject's own channel preferences"
|
|
49
|
-
}),
|
|
55
|
+
}), g = t({
|
|
50
56
|
name: "notifications.setPreference",
|
|
51
57
|
input: e.Struct({
|
|
52
58
|
category: e.String,
|
|
@@ -55,17 +61,17 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
|
|
|
55
61
|
}),
|
|
56
62
|
output: e.Struct({ ok: e.Boolean }),
|
|
57
63
|
openAccess: "self-scoped write: toggles a channel only on the calling subject's own preferences"
|
|
58
|
-
}),
|
|
64
|
+
}), _ = t({
|
|
59
65
|
name: "notifications.subscribe",
|
|
60
66
|
input: e.Struct({ topic: e.String }),
|
|
61
67
|
output: e.Struct({ ok: e.Boolean }),
|
|
62
68
|
openAccess: "self-service: subscribes the calling subject itself to a topic (the subject is never caller-supplied)"
|
|
63
|
-
}),
|
|
69
|
+
}), v = t({
|
|
64
70
|
name: "notifications.unsubscribe",
|
|
65
71
|
input: e.Struct({ topic: e.String }),
|
|
66
72
|
output: e.Struct({ ok: e.Boolean }),
|
|
67
73
|
openAccess: "self-service: unsubscribes the calling subject itself from a topic"
|
|
68
|
-
}),
|
|
74
|
+
}), y = t({
|
|
69
75
|
name: "notifications.setQuietHours",
|
|
70
76
|
input: e.Struct({
|
|
71
77
|
startMinute: e.Number,
|
|
@@ -75,17 +81,17 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
|
|
|
75
81
|
}),
|
|
76
82
|
output: e.Struct({ ok: e.Boolean }),
|
|
77
83
|
openAccess: "self-scoped write: sets only the calling subject's own quiet-hours window"
|
|
78
|
-
}),
|
|
84
|
+
}), b = t({
|
|
79
85
|
name: "notifications.clearQuietHours",
|
|
80
86
|
input: e.Struct({}),
|
|
81
87
|
output: e.Struct({ ok: e.Boolean }),
|
|
82
88
|
openAccess: "self-scoped write: clears only the calling subject's own quiet-hours window"
|
|
83
|
-
}),
|
|
89
|
+
}), x = n({
|
|
84
90
|
name: "notifications.webPushPublicKey",
|
|
85
91
|
input: e.Struct({}),
|
|
86
92
|
output: e.Struct({ key: e.String }),
|
|
87
93
|
openAccess: "public application-server key — the value every subscribing browser is handed by design"
|
|
88
|
-
}),
|
|
94
|
+
}), S = t({
|
|
89
95
|
name: "notifications.webPushSubscribe",
|
|
90
96
|
input: e.Struct({
|
|
91
97
|
endpoint: e.String,
|
|
@@ -95,17 +101,17 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
|
|
|
95
101
|
}),
|
|
96
102
|
output: e.Struct({ ok: e.Boolean }),
|
|
97
103
|
openAccess: "self-scoped write: registers the calling subject's own browser subscription (the subject is never caller-supplied)"
|
|
98
|
-
}),
|
|
104
|
+
}), C = t({
|
|
99
105
|
name: "notifications.webPushUnsubscribe",
|
|
100
106
|
input: e.Struct({ endpoint: e.String }),
|
|
101
107
|
output: e.Struct({ ok: e.Boolean }),
|
|
102
108
|
openAccess: "self-scoped write: removes only a subscription the calling subject owns"
|
|
103
|
-
}),
|
|
109
|
+
}), w = n({
|
|
104
110
|
name: "notifications.webPushStatus",
|
|
105
111
|
input: e.Struct({}),
|
|
106
112
|
output: e.Struct({ endpoints: e.Number }),
|
|
107
113
|
openAccess: "self-scoped read: counts only the calling subject's own subscriptions"
|
|
108
|
-
}),
|
|
114
|
+
}), T = [
|
|
109
115
|
{
|
|
110
116
|
tag: "notifications.webPushPublicKey",
|
|
111
117
|
kind: "query",
|
|
@@ -138,7 +144,7 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
|
|
|
138
144
|
name: "webPushStatusDescriptor"
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
|
-
],
|
|
147
|
+
], E = [
|
|
142
148
|
{
|
|
143
149
|
tag: "notifications.inbox",
|
|
144
150
|
kind: "query",
|
|
@@ -245,4 +251,4 @@ var i = "@voltro/plugin-notifications", a = r("notifications-inbox"), o = n({
|
|
|
245
251
|
}
|
|
246
252
|
];
|
|
247
253
|
//#endregion
|
|
248
|
-
export { i as BASE_NAME,
|
|
254
|
+
export { i as BASE_NAME, s as ChannelPreferenceSchema, p as archiveDescriptor, b as clearQuietHoursDescriptor, c as inboxDescriptor, a as inboxFeed, f as markAllReadDescriptor, u as markReadDescriptor, d as markUnreadDescriptor, E as notificationsRpcClientImports, h as preferencesDescriptor, o as preferencesFeed, g as setPreferenceDescriptor, y as setQuietHoursDescriptor, _ as subscribeDescriptor, m as unarchiveDescriptor, l as unreadCountDescriptor, v as unsubscribeDescriptor, x as webPushPublicKeyDescriptor, T as webPushRpcClientImports, w as webPushStatusDescriptor, S as webPushSubscribeDescriptor, C as webPushUnsubscribeDescriptor };
|
package/dist/service.js
CHANGED
|
@@ -35,8 +35,32 @@ var n = (e) => ({
|
|
|
35
35
|
let n = r(t, e.tz ?? "UTC"), i = e.endMinute - n;
|
|
36
36
|
return i <= 0 && (i += 1440), new Date(t.getTime() + i * 6e4);
|
|
37
37
|
}, o = (n) => {
|
|
38
|
-
let r = new Map(n.channels.map((e) => [e.id, e])), o = n.channels.map((e) => e.id), c = n.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()), l = n.digestWindowMs ?? 0, u =
|
|
39
|
-
|
|
38
|
+
let r = new Map(n.channels.map((e) => [e.id, e])), o = n.channels.map((e) => e.id), c = n.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()), l = n.digestWindowMs ?? 0, u = new Map(Object.entries(n.deliveryGuards ?? {}));
|
|
39
|
+
if (u.size > 256 || [...u].some(([e, t]) => !/^[a-z][a-zA-Z0-9._-]{0,127}$/.test(e) || typeof t != "function")) throw Error("Invalid notification delivery guard registry");
|
|
40
|
+
let d = async (e) => {
|
|
41
|
+
let t = e.deliveryGuard;
|
|
42
|
+
if (t === void 0) return !0;
|
|
43
|
+
if (!t || typeof t.id != "string" || !t.payload || typeof t.payload != "object" || Array.isArray(t.payload)) throw Error("Invalid notification delivery guard reference");
|
|
44
|
+
let n = u.get(t.id);
|
|
45
|
+
if (!n) throw Error("Notification delivery guard unavailable");
|
|
46
|
+
let r = JSON.stringify(t.payload);
|
|
47
|
+
if (typeof r != "string" || r.length > 8192) throw Error("Notification delivery guard payload exceeds budget");
|
|
48
|
+
let i = JSON.parse(r);
|
|
49
|
+
if (typeof i != "object" || !i || Array.isArray(i)) throw Error("Invalid notification delivery guard payload");
|
|
50
|
+
let a = await n({
|
|
51
|
+
recipientId: e.to,
|
|
52
|
+
tenantId: e.tenantId ?? null,
|
|
53
|
+
payload: i
|
|
54
|
+
});
|
|
55
|
+
if (a !== !0 && a !== !1) throw Error("Invalid notification delivery guard decision");
|
|
56
|
+
return a;
|
|
57
|
+
}, f = () => ({
|
|
58
|
+
delivered: [],
|
|
59
|
+
failed: [],
|
|
60
|
+
skipped: [...o]
|
|
61
|
+
}), p = async (i) => {
|
|
62
|
+
if (!await d(i)) return f();
|
|
63
|
+
let a = await n.store.getPreferences(i.to), s = e(o, i.category, i.channels, a, i.to), l = o.filter((e) => !s.includes(e)), u = [], p = [], m = {
|
|
40
64
|
to: i.to,
|
|
41
65
|
category: i.category,
|
|
42
66
|
title: i.title,
|
|
@@ -45,11 +69,15 @@ var n = (e) => ({
|
|
|
45
69
|
tenantId: i.tenantId ?? null
|
|
46
70
|
};
|
|
47
71
|
for (let e of s) {
|
|
72
|
+
if (!await d(i)) {
|
|
73
|
+
l.push(e);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
48
76
|
let a = r.get(e);
|
|
49
77
|
if (!a) continue;
|
|
50
78
|
if (a.deliverDetailed !== void 0) {
|
|
51
79
|
try {
|
|
52
|
-
let r = await a.deliverDetailed(
|
|
80
|
+
let r = await a.deliverDetailed(m);
|
|
53
81
|
for (let a of r) await n.store.recordDelivery({
|
|
54
82
|
id: t(),
|
|
55
83
|
to: i.to,
|
|
@@ -62,13 +90,13 @@ var n = (e) => ({
|
|
|
62
90
|
...a.clickToken === void 0 ? {} : { clickToken: a.clickToken }
|
|
63
91
|
});
|
|
64
92
|
let o = r.filter((e) => e.status === "failed");
|
|
65
|
-
r.length === 0 || o.length < r.length ? u.push(e) :
|
|
93
|
+
r.length === 0 || o.length < r.length ? u.push(e) : p.push({
|
|
66
94
|
channel: e,
|
|
67
95
|
error: o[0]?.error ?? "all endpoints failed"
|
|
68
96
|
});
|
|
69
97
|
} catch (r) {
|
|
70
98
|
let a = r instanceof Error ? r.message : String(r);
|
|
71
|
-
|
|
99
|
+
p.push({
|
|
72
100
|
channel: e,
|
|
73
101
|
error: a
|
|
74
102
|
}), await n.store.recordDelivery({
|
|
@@ -91,13 +119,13 @@ var n = (e) => ({
|
|
|
91
119
|
at: c()
|
|
92
120
|
};
|
|
93
121
|
try {
|
|
94
|
-
await a.deliver(
|
|
122
|
+
await a.deliver(m), u.push(e), await n.store.recordDelivery({
|
|
95
123
|
...o,
|
|
96
124
|
status: "sent"
|
|
97
125
|
});
|
|
98
126
|
} catch (t) {
|
|
99
127
|
let r = t instanceof Error ? t.message : String(t);
|
|
100
|
-
|
|
128
|
+
p.push({
|
|
101
129
|
channel: e,
|
|
102
130
|
error: r
|
|
103
131
|
}), await n.store.recordDelivery({
|
|
@@ -109,12 +137,14 @@ var n = (e) => ({
|
|
|
109
137
|
}
|
|
110
138
|
return {
|
|
111
139
|
delivered: u,
|
|
112
|
-
failed:
|
|
140
|
+
failed: p,
|
|
113
141
|
skipped: l
|
|
114
142
|
};
|
|
115
|
-
},
|
|
116
|
-
let r =
|
|
117
|
-
if (
|
|
143
|
+
}, m = async (e) => {
|
|
144
|
+
let r = structuredClone(e);
|
|
145
|
+
if (!await d(r)) return f();
|
|
146
|
+
let o = await n.store.getQuietHours(r.to), s = new Date(c());
|
|
147
|
+
if (o && i(o, s)) return (o.policy ?? "hold") === "drop" ? {
|
|
118
148
|
delivered: [],
|
|
119
149
|
failed: [],
|
|
120
150
|
skipped: [],
|
|
@@ -122,23 +152,23 @@ var n = (e) => ({
|
|
|
122
152
|
} : (await n.store.enqueueHeld({
|
|
123
153
|
id: t(),
|
|
124
154
|
kind: "quiet",
|
|
125
|
-
subjectId:
|
|
126
|
-
send:
|
|
127
|
-
flushAt: a(
|
|
155
|
+
subjectId: r.to,
|
|
156
|
+
send: r,
|
|
157
|
+
flushAt: a(o, s).toISOString()
|
|
128
158
|
}), {
|
|
129
159
|
delivered: [],
|
|
130
160
|
failed: [],
|
|
131
161
|
skipped: [],
|
|
132
162
|
held: "quiet-held"
|
|
133
163
|
});
|
|
134
|
-
if (l > 0 && !
|
|
135
|
-
let
|
|
164
|
+
if (l > 0 && !r.channels) {
|
|
165
|
+
let e = (await n.store.pendingDigest(r.to))[0]?.flushAt ?? new Date(s.getTime() + l).toISOString();
|
|
136
166
|
return await n.store.enqueueHeld({
|
|
137
167
|
id: t(),
|
|
138
168
|
kind: "digest",
|
|
139
|
-
subjectId:
|
|
140
|
-
send:
|
|
141
|
-
flushAt:
|
|
169
|
+
subjectId: r.to,
|
|
170
|
+
send: r,
|
|
171
|
+
flushAt: e
|
|
142
172
|
}), {
|
|
143
173
|
delivered: [],
|
|
144
174
|
failed: [],
|
|
@@ -146,10 +176,10 @@ var n = (e) => ({
|
|
|
146
176
|
held: "digest"
|
|
147
177
|
};
|
|
148
178
|
}
|
|
149
|
-
return
|
|
179
|
+
return p(r);
|
|
150
180
|
};
|
|
151
181
|
return {
|
|
152
|
-
send:
|
|
182
|
+
send: m,
|
|
153
183
|
inbox: (e, t) => n.store.listInbox(e, t),
|
|
154
184
|
unreadCount: (e) => n.store.unreadCount(e),
|
|
155
185
|
markRead: (e, t) => n.store.markRead(e, t),
|
|
@@ -166,7 +196,7 @@ var n = (e) => ({
|
|
|
166
196
|
unsubscribe: (e, t) => n.store.unsubscribeTopic(e, t),
|
|
167
197
|
broadcast: async (e, t) => {
|
|
168
198
|
let r = await n.store.listTopicSubscribers(e), i = [];
|
|
169
|
-
for (let e of r) i.push(await
|
|
199
|
+
for (let e of r) i.push(await m({
|
|
170
200
|
...t,
|
|
171
201
|
to: e.subjectId,
|
|
172
202
|
tenantId: t.tenantId ?? e.tenantId ?? null
|
|
@@ -182,13 +212,17 @@ var n = (e) => ({
|
|
|
182
212
|
flushDue: async (e = new Date(c())) => {
|
|
183
213
|
let t = await n.store.dueHeld(e.toISOString());
|
|
184
214
|
if (t.length === 0) return 0;
|
|
185
|
-
for (let e of t.filter((e) => e.kind === "quiet")) await
|
|
215
|
+
for (let e of t.filter((e) => e.kind === "quiet")) await p(e.send);
|
|
186
216
|
let r = /* @__PURE__ */ new Map();
|
|
187
217
|
for (let e of t.filter((e) => e.kind === "digest")) {
|
|
188
|
-
|
|
189
|
-
|
|
218
|
+
if (e.send.deliveryGuard !== void 0) {
|
|
219
|
+
await p(e.send);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
let t = JSON.stringify([e.send.tenantId ?? null, e.subjectId]), n = r.get(t) ?? [];
|
|
223
|
+
n.push(e), r.set(t, n);
|
|
190
224
|
}
|
|
191
|
-
for (let
|
|
225
|
+
for (let e of r.values()) await p(s(e[0].subjectId, e));
|
|
192
226
|
return await n.store.clearHeld(t.map((e) => e.id)), t.length;
|
|
193
227
|
}
|
|
194
228
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-notifications",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.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",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"node": ">=24.0.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@voltro/client": "0.
|
|
53
|
-
"@voltro/database": "0.
|
|
54
|
-
"@voltro/protocol": "0.
|
|
52
|
+
"@voltro/client": "0.87.0",
|
|
53
|
+
"@voltro/database": "0.87.0",
|
|
54
|
+
"@voltro/protocol": "0.87.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"effect": "4.0.1",
|