@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/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 = n({
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
- }), s = n({
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
- }), c = t({
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
- }), l = t({
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
- }), u = t({
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
- }), d = t({
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
- }), f = t({
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
- }), p = n({
49
+ }), h = n({
45
50
  name: "notifications.preferences",
51
+ source: o,
46
52
  input: e.Struct({}),
47
- output: e.Array(e.Record(e.String, e.Unknown)),
53
+ output: e.Array(s),
48
54
  openAccess: "self-scoped read: returns only the calling subject's own channel preferences"
49
- }), m = t({
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
- }), h = t({
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
- }), g = t({
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
- }), _ = t({
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
- }), v = t({
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
- }), y = n({
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
- }), b = t({
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
- }), x = t({
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
- }), S = n({
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
- }), C = [
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
- ], w = [
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, d as archiveDescriptor, v as clearQuietHoursDescriptor, o as inboxDescriptor, a as inboxFeed, u as markAllReadDescriptor, c as markReadDescriptor, l as markUnreadDescriptor, w as notificationsRpcClientImports, p as preferencesDescriptor, m as setPreferenceDescriptor, _ as setQuietHoursDescriptor, h as subscribeDescriptor, f as unarchiveDescriptor, s as unreadCountDescriptor, g as unsubscribeDescriptor, y as webPushPublicKeyDescriptor, C as webPushRpcClientImports, S as webPushStatusDescriptor, b as webPushSubscribeDescriptor, x as webPushUnsubscribeDescriptor };
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 = async (i) => {
39
- 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 = [], d = [], f = {
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(f);
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) : d.push({
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
- d.push({
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(f), u.push(e), await n.store.recordDelivery({
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
- d.push({
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: d,
140
+ failed: p,
113
141
  skipped: l
114
142
  };
115
- }, d = async (e) => {
116
- let r = await n.store.getQuietHours(e.to), o = new Date(c());
117
- if (r && i(r, o)) return (r.policy ?? "hold") === "drop" ? {
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: e.to,
126
- send: e,
127
- flushAt: a(r, o).toISOString()
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 && !e.channels) {
135
- let r = (await n.store.pendingDigest(e.to))[0]?.flushAt ?? new Date(o.getTime() + l).toISOString();
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: e.to,
140
- send: e,
141
- flushAt: r
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 u(e);
179
+ return p(r);
150
180
  };
151
181
  return {
152
- send: d,
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 d({
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 u(e.send);
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
- let t = r.get(e.subjectId) ?? [];
189
- t.push(e), r.set(e.subjectId, t);
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 [e, t] of r) await u(s(e, t));
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.86.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.86.0",
53
- "@voltro/database": "0.86.0",
54
- "@voltro/protocol": "0.86.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",