@voltro/plugin-notifications 0.86.0 → 0.88.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.
@@ -1,5 +1,5 @@
1
1
  import { Schema as e } from "effect";
2
- //#region src/sendNotification.ts
2
+ // src/sendNotification.ts
3
3
  var t = (e) => {
4
4
  if (typeof e == "object" && e) {
5
5
  let t = e;
@@ -38,5 +38,4 @@ var t = (e) => {
38
38
  ...i === void 0 ? {} : { tenantId: i }
39
39
  };
40
40
  };
41
- //#endregion
42
41
  export { t as defaultRecipients, r as sendInputFor, n as sendNotification };
package/dist/service.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { resolveChannels as e } from "./channels.js";
2
2
  import { randomUUID as t } from "node:crypto";
3
- //#region src/service.ts
3
+ // src/service.ts
4
4
  var n = (e) => ({
5
5
  id: "inApp",
6
6
  deliver: async (n) => {
@@ -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
  };
@@ -208,5 +242,4 @@ var n = (e) => ({
208
242
  tenantId: t[0].send.tenantId ?? null
209
243
  };
210
244
  };
211
- //#endregion
212
245
  export { o as buildNotificationService, n as inAppChannel, i as inQuietHours, r as minuteOfDayInZone, a as quietHoursEnd };
package/dist/store.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { and as e, eq as t, generateId as n, isNull as r, lte as i } from "@voltro/database";
2
- //#region src/store.ts
2
+ // src/store.ts
3
3
  var a = () => {
4
4
  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}`;
5
5
  return {
@@ -299,5 +299,4 @@ var a = () => {
299
299
  }
300
300
  };
301
301
  };
302
- //#endregion
303
302
  export { s as dataStoreNotificationStore, a as memoryNotificationStore };
package/dist/web.js CHANGED
@@ -2,7 +2,7 @@ import { BASE_NAME as e } from "./rpc.js";
2
2
  import { pluginTag as t } from "@voltro/protocol";
3
3
  import { useEffect as n, useState as r } from "react";
4
4
  import { useMutation as i, useSubscription as a } from "@voltro/client";
5
- //#region src/web.ts
5
+ // src/web.ts
6
6
  var o = (n) => t(e, n), s = (e = {}, t = "app") => {
7
7
  let { data: n } = a(t, o("inbox"), e);
8
8
  return n ?? [];
@@ -95,5 +95,4 @@ var o = (n) => t(e, n), s = (e = {}, t = "app") => {
95
95
  clear: () => n.mutate({})
96
96
  };
97
97
  };
98
- //#endregion
99
98
  export { s as useInbox, l as useMarkRead, m as useQuietHours, u as useSetNotificationPreference, d as useTopicSubscription, c as useUnreadCount, p as useWebPush };
package/dist/webPush.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { sendWebPush as e, vapidPublicKeyFor as t } from "./webPushProtocol.js";
2
2
  import { and as n, eq as r, generateId as i, id as a, table as o, text as s, timestamp as c } from "@voltro/database";
3
3
  import { createHash as l, randomBytes as u } from "node:crypto";
4
- //#region src/webPush.ts
4
+ // src/webPush.ts
5
5
  var d = o("_voltro_notification_push_subscriptions", {
6
6
  id: a({ prefix: "notifpush" }),
7
7
  subjectId: s(),
@@ -157,5 +157,4 @@ var d = o("_voltro_notification_push_subscriptions", {
157
157
  }
158
158
  return a;
159
159
  }, D = (e) => t(b(e));
160
- //#endregion
161
160
  export { w as assertWebPushConfigured, v as bindWebPushStore, S as buildPushPayload, h as dataStorePushSubscriptionStore, f as endpointHashOf, p as memoryPushSubscriptionStore, d as pushSubscriptionsTable, y as resetWebPushStoreForTest, b as resolveVapidPrivateKey, _ as webPushCell, C as webPushChannel, D as webPushPublicKey };
@@ -1,5 +1,5 @@
1
1
  import { createCipheriv as e, createDecipheriv as t, createECDH as n, createHmac as r, createPrivateKey as i, randomBytes as a, sign as o, timingSafeEqual as s } from "node:crypto";
2
- //#region src/webPushProtocol.ts
2
+ // src/webPushProtocol.ts
3
3
  var c = (e) => e.toString("base64url"), l = (e) => Buffer.from(e, "base64url"), u = (e, t) => r("sha256", e).update(t).digest(), d = (e, t, n) => u(e, Buffer.concat([t, Buffer.from([1])])).subarray(0, n), f = () => {
4
4
  let e = n("prime256v1");
5
5
  return e.generateKeys(), c(p(e.getPrivateKey()));
@@ -113,5 +113,4 @@ var c = (e) => e.toString("base64url"), l = (e) => Buffer.from(e, "base64url"),
113
113
  let n = Buffer.from(e), r = Buffer.from(t);
114
114
  return n.length === r.length && s(n, r);
115
115
  };
116
- //#endregion
117
116
  export { T as clickTokenMatches, C as decryptWebPushPayload, S as encryptWebPushPayload, f as generateVapidPrivateKey, w as sendWebPush, _ as vapidAuthorization, h as vapidPublicKeyFor };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-notifications",
3
- "version": "0.86.0",
3
+ "version": "0.88.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,12 +49,12 @@
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.88.0",
53
+ "@voltro/database": "0.88.0",
54
+ "@voltro/protocol": "0.88.0"
55
55
  },
56
56
  "peerDependencies": {
57
- "effect": "4.0.1",
57
+ "effect": "4.1.0",
58
58
  "react": "^19.0.0"
59
59
  },
60
60
  "publishConfig": {