@voltro/plugin-notifications 0.52.0 → 0.53.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 +229 -0
- package/THIRD-PARTY-NOTICES.md +169 -1
- package/dist/index.d.ts +202 -4
- package/dist/index.js +626 -227
- package/dist/rpc.d.ts +27 -0
- package/dist/rpc.js +60 -2
- package/dist/web.d.ts +22 -0
- package/dist/web.js +81 -22
- package/package.json +8 -6
- package/sw.js +50 -0
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
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
|
|
2
|
-
import { Context as
|
|
3
|
-
import { and as
|
|
4
|
-
import { definePlugin as
|
|
5
|
-
import { randomUUID as
|
|
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 d, unreadCountDescriptor as f, unsubscribeDescriptor as p, webPushPublicKeyDescriptor as m, webPushRpcClientImports as ee, webPushStatusDescriptor as te, webPushSubscribeDescriptor as ne, webPushUnsubscribeDescriptor as re } from "./rpc.js";
|
|
2
|
+
import { Context as h, Effect as g, Layer as ie, 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 ae, 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 oe, pluginInstanceName as se } from "@voltro/protocol";
|
|
5
|
+
import { createCipheriv as M, createDecipheriv as ce, createECDH as N, createHash as le, createHmac as ue, createPrivateKey as de, randomBytes as fe, randomUUID as P, sign as pe, timingSafeEqual as me } from "node:crypto";
|
|
6
6
|
//#region src/errors.ts
|
|
7
|
-
var
|
|
8
|
-
token:
|
|
9
|
-
reason:
|
|
10
|
-
}) {},
|
|
7
|
+
var F = class extends _.TaggedError()("PushTokenRejected", {
|
|
8
|
+
token: _.String,
|
|
9
|
+
reason: _.String
|
|
10
|
+
}) {}, I = () => ({
|
|
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
|
+
}), he = (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 A = 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
|
+
}), ge = (e) => ({
|
|
29
29
|
id: "email",
|
|
30
30
|
deliver: (t) => e({
|
|
31
31
|
to: t.to,
|
|
@@ -33,38 +33,58 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
33
33
|
html: `<p>${t.body}</p>`,
|
|
34
34
|
text: t.body
|
|
35
35
|
})
|
|
36
|
-
}),
|
|
36
|
+
}), _e = (e) => ({
|
|
37
37
|
id: "sms",
|
|
38
38
|
deliver: (t) => e(t.to, `${t.title}: ${t.body}`)
|
|
39
|
-
}),
|
|
39
|
+
}), ve = (e, t) => ({
|
|
40
40
|
id: e,
|
|
41
41
|
deliver: t
|
|
42
|
-
}),
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
42
|
+
}), ye = (e) => {
|
|
43
|
+
let t = async (t, n, r) => {
|
|
44
|
+
let i = {
|
|
45
|
+
token: n,
|
|
46
|
+
title: t.title,
|
|
47
|
+
body: t.body,
|
|
48
|
+
...r === void 0 ? {} : { badge: r },
|
|
49
|
+
data: t.data ?? {}
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
return await e.transport(i), {
|
|
53
|
+
endpoint: n,
|
|
54
|
+
status: "sent"
|
|
55
|
+
};
|
|
56
|
+
} catch (t) {
|
|
57
|
+
let r = t instanceof F, i = r ? t.reason : t instanceof Error ? t.message : String(t);
|
|
58
|
+
if (r && e.onTokenRejected !== void 0) try {
|
|
59
|
+
await e.onTokenRejected(n, i);
|
|
60
|
+
} catch {}
|
|
61
|
+
return {
|
|
62
|
+
endpoint: n,
|
|
63
|
+
status: "failed",
|
|
64
|
+
error: i,
|
|
65
|
+
...r ? { pruned: e.onTokenRejected !== void 0 } : {}
|
|
53
66
|
};
|
|
54
|
-
try {
|
|
55
|
-
await e.transport(n);
|
|
56
|
-
} catch (e) {
|
|
57
|
-
throw e instanceof A ? e : new A({
|
|
58
|
-
token: r,
|
|
59
|
-
reason: e instanceof Error ? e.message : String(e)
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
67
|
}
|
|
63
|
-
}
|
|
64
|
-
|
|
68
|
+
}, n = async (n) => {
|
|
69
|
+
let r = await e.tokensFor(n.to), i = n.data?.badge, a = typeof i == "number" ? i : void 0, o = [];
|
|
70
|
+
for (let e of r) o.push(await t(n, e, a));
|
|
71
|
+
return o;
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
id: e.id ?? "push",
|
|
75
|
+
deliver: async (e) => {
|
|
76
|
+
let t = await n(e), r = t.filter((e) => e.status === "failed");
|
|
77
|
+
if (t.length > 0 && r.length === t.length) throw new F({
|
|
78
|
+
token: t[0].endpoint,
|
|
79
|
+
reason: r[0].error ?? "all tokens rejected"
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
deliverDetailed: n
|
|
83
|
+
};
|
|
84
|
+
}, L = (e, t, n, r, i) => {
|
|
65
85
|
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
86
|
return a.filter((e) => !o.has(e));
|
|
67
|
-
},
|
|
87
|
+
}, R = () => {
|
|
68
88
|
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
89
|
return {
|
|
70
90
|
addInbox: async (t) => {
|
|
@@ -121,6 +141,13 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
121
141
|
n.unshift(e);
|
|
122
142
|
},
|
|
123
143
|
listDeliveries: async (e) => e?.limit === void 0 ? [...n] : n.slice(0, e.limit),
|
|
144
|
+
markClicked: async (e) => {
|
|
145
|
+
let t = n.findIndex((t) => t.clickToken === e);
|
|
146
|
+
return t !== -1 && (n[t] = {
|
|
147
|
+
...n[t],
|
|
148
|
+
clickedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
149
|
+
}, !0);
|
|
150
|
+
},
|
|
124
151
|
subscribeTopic: async (e) => {
|
|
125
152
|
r.set(s(e.topic, e.subjectId), e);
|
|
126
153
|
},
|
|
@@ -145,7 +172,7 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
145
172
|
for (let e = a.length - 1; e >= 0; e--) t.has(a[e].id) && a.splice(e, 1);
|
|
146
173
|
}
|
|
147
174
|
};
|
|
148
|
-
},
|
|
175
|
+
}, z = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : "", be = (e) => {
|
|
149
176
|
let t = async (t, n, r, i) => await e.query({
|
|
150
177
|
table: t,
|
|
151
178
|
predicate: n,
|
|
@@ -160,29 +187,27 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
160
187
|
title: String(e.title),
|
|
161
188
|
body: String(e.body),
|
|
162
189
|
data: e.data ?? {},
|
|
163
|
-
readAt: e.readAt == null ? null :
|
|
164
|
-
archivedAt: e.archivedAt == null ? null :
|
|
165
|
-
createdAt:
|
|
190
|
+
readAt: e.readAt == null ? null : z(e.readAt),
|
|
191
|
+
archivedAt: e.archivedAt == null ? null : z(e.archivedAt),
|
|
192
|
+
createdAt: z(e.createdAt),
|
|
166
193
|
tenantId: e.tenantId ?? null
|
|
167
194
|
}), r = (e) => ({
|
|
168
195
|
subjectId: String(e.subjectId),
|
|
169
196
|
category: String(e.category),
|
|
170
197
|
channel: String(e.channel),
|
|
171
198
|
enabled: !!e.enabled
|
|
172
|
-
}), i = (e) => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
};
|
|
185
|
-
}, a = (e) => ({
|
|
199
|
+
}), i = (e) => ({
|
|
200
|
+
id: String(e.id),
|
|
201
|
+
to: String(e.recipient),
|
|
202
|
+
category: String(e.category),
|
|
203
|
+
channel: String(e.channel),
|
|
204
|
+
status: e.status,
|
|
205
|
+
at: z(e.at),
|
|
206
|
+
...e.error == null ? {} : { error: String(e.error) },
|
|
207
|
+
...e.endpoint == null ? {} : { endpoint: String(e.endpoint) },
|
|
208
|
+
...e.clickToken == null ? {} : { clickToken: String(e.clickToken) },
|
|
209
|
+
...e.clickedAt == null ? {} : { clickedAt: z(e.clickedAt) }
|
|
210
|
+
}), a = (e) => ({
|
|
186
211
|
topic: String(e.topic),
|
|
187
212
|
subjectId: String(e.subjectId),
|
|
188
213
|
tenantId: e.tenantId ?? null
|
|
@@ -197,7 +222,7 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
197
222
|
kind: e.kind === "quiet" ? "quiet" : "digest",
|
|
198
223
|
subjectId: String(e.subjectId),
|
|
199
224
|
send: e.send ?? {},
|
|
200
|
-
flushAt:
|
|
225
|
+
flushAt: z(e.flushAt)
|
|
201
226
|
});
|
|
202
227
|
return {
|
|
203
228
|
addInbox: async (t) => {
|
|
@@ -215,24 +240,24 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
215
240
|
});
|
|
216
241
|
},
|
|
217
242
|
listInbox: async (e, r) => {
|
|
218
|
-
let i = r?.unreadOnly ?
|
|
243
|
+
let i = r?.unreadOnly ? v(b("subjectId", e), w("readAt")) : b("subjectId", e);
|
|
219
244
|
return (await t("_voltro_notification_inbox", i, [{
|
|
220
245
|
column: "createdAt",
|
|
221
246
|
direction: "desc"
|
|
222
247
|
}], r?.limit)).map(n);
|
|
223
248
|
},
|
|
224
|
-
markRead: async (n, r) => (await t("_voltro_notification_inbox",
|
|
225
|
-
markUnread: async (n, r) => (await t("_voltro_notification_inbox",
|
|
249
|
+
markRead: async (n, r) => (await t("_voltro_notification_inbox", v(b("id", n), b("subjectId", r)), [], 1))[0] ? (await e.update("_voltro_notification_inbox", n, { readAt: (/* @__PURE__ */ new Date()).toISOString() }), !0) : !1,
|
|
250
|
+
markUnread: async (n, r) => (await t("_voltro_notification_inbox", v(b("id", n), b("subjectId", r)), [], 1))[0] ? (await e.update("_voltro_notification_inbox", n, { readAt: null }), !0) : !1,
|
|
226
251
|
markAllRead: async (n) => {
|
|
227
|
-
let r = await t("_voltro_notification_inbox",
|
|
252
|
+
let r = await t("_voltro_notification_inbox", v(b("subjectId", n), w("readAt")), [], void 0), i = (/* @__PURE__ */ new Date()).toISOString();
|
|
228
253
|
for (let t of r) await e.update("_voltro_notification_inbox", String(t.id), { readAt: i });
|
|
229
254
|
return r.length;
|
|
230
255
|
},
|
|
231
|
-
setArchived: async (n, r, i) => (await t("_voltro_notification_inbox",
|
|
232
|
-
unreadCount: async (e) => (await t("_voltro_notification_inbox",
|
|
233
|
-
getPreferences: async (e) => (await t("_voltro_notification_preferences",
|
|
256
|
+
setArchived: async (n, r, i) => (await t("_voltro_notification_inbox", v(b("id", n), b("subjectId", r)), [], 1))[0] ? (await e.update("_voltro_notification_inbox", n, { archivedAt: i ? (/* @__PURE__ */ new Date()).toISOString() : null }), !0) : !1,
|
|
257
|
+
unreadCount: async (e) => (await t("_voltro_notification_inbox", v(b("subjectId", e), w("readAt")), [], void 0)).length,
|
|
258
|
+
getPreferences: async (e) => (await t("_voltro_notification_preferences", b("subjectId", e), [], void 0)).map(r),
|
|
234
259
|
setPreference: async (t) => {
|
|
235
|
-
let n =
|
|
260
|
+
let n = x({
|
|
236
261
|
kind: "typeid",
|
|
237
262
|
prefix: "notifpref"
|
|
238
263
|
}, "_voltro_notification_preferences");
|
|
@@ -259,15 +284,22 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
259
284
|
channel: t.channel,
|
|
260
285
|
status: t.status,
|
|
261
286
|
error: t.error ?? null,
|
|
262
|
-
at: t.at
|
|
287
|
+
at: t.at,
|
|
288
|
+
endpoint: t.endpoint ?? null,
|
|
289
|
+
clickToken: t.clickToken ?? null,
|
|
290
|
+
clickedAt: t.clickedAt ?? null
|
|
263
291
|
});
|
|
264
292
|
},
|
|
293
|
+
markClicked: async (n) => {
|
|
294
|
+
let r = await t("_voltro_notification_deliveries", b("clickToken", n), [], 1);
|
|
295
|
+
return r[0] ? (await e.update("_voltro_notification_deliveries", String(r[0].id), { clickedAt: (/* @__PURE__ */ new Date()).toISOString() }), !0) : !1;
|
|
296
|
+
},
|
|
265
297
|
listDeliveries: async (e) => (await t("_voltro_notification_deliveries", void 0, [{
|
|
266
298
|
column: "at",
|
|
267
299
|
direction: "desc"
|
|
268
300
|
}], e?.limit)).map(i),
|
|
269
301
|
subscribeTopic: async (t) => {
|
|
270
|
-
let n =
|
|
302
|
+
let n = x({
|
|
271
303
|
kind: "typeid",
|
|
272
304
|
prefix: "notiftsub"
|
|
273
305
|
}, "_voltro_notification_topic_subscriptions");
|
|
@@ -282,16 +314,16 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
282
314
|
});
|
|
283
315
|
},
|
|
284
316
|
unsubscribeTopic: async (n, r) => {
|
|
285
|
-
let i = await t("_voltro_notification_topic_subscriptions",
|
|
317
|
+
let i = await t("_voltro_notification_topic_subscriptions", v(b("topic", n), b("subjectId", r)), [], 1);
|
|
286
318
|
i[0] && await e.delete("_voltro_notification_topic_subscriptions", String(i[0].id));
|
|
287
319
|
},
|
|
288
|
-
listTopicSubscribers: async (e) => (await t("_voltro_notification_topic_subscriptions",
|
|
320
|
+
listTopicSubscribers: async (e) => (await t("_voltro_notification_topic_subscriptions", b("topic", e), [], void 0)).map(a),
|
|
289
321
|
getQuietHours: async (e) => {
|
|
290
|
-
let n = await t("_voltro_notification_quiet_hours",
|
|
322
|
+
let n = await t("_voltro_notification_quiet_hours", b("subjectId", e), [], 1);
|
|
291
323
|
return n[0] ? o(n[0]) : null;
|
|
292
324
|
},
|
|
293
325
|
setQuietHours: async (t) => {
|
|
294
|
-
let n =
|
|
326
|
+
let n = x({
|
|
295
327
|
kind: "typeid",
|
|
296
328
|
prefix: "notifqh"
|
|
297
329
|
}, "_voltro_notification_quiet_hours");
|
|
@@ -313,7 +345,7 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
313
345
|
});
|
|
314
346
|
},
|
|
315
347
|
clearQuietHours: async (n) => {
|
|
316
|
-
let r = await t("_voltro_notification_quiet_hours",
|
|
348
|
+
let r = await t("_voltro_notification_quiet_hours", b("subjectId", n), [], 1);
|
|
317
349
|
r[0] && await e.delete("_voltro_notification_quiet_hours", String(r[0].id));
|
|
318
350
|
},
|
|
319
351
|
enqueueHeld: async (t) => {
|
|
@@ -325,11 +357,11 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
325
357
|
flushAt: t.flushAt
|
|
326
358
|
});
|
|
327
359
|
},
|
|
328
|
-
pendingDigest: async (e) => (await t("_voltro_notification_held",
|
|
360
|
+
pendingDigest: async (e) => (await t("_voltro_notification_held", v(b("kind", "digest"), b("subjectId", e)), [{
|
|
329
361
|
column: "flushAt",
|
|
330
362
|
direction: "asc"
|
|
331
363
|
}], void 0)).map(s),
|
|
332
|
-
dueHeld: async (e) => (await t("_voltro_notification_held",
|
|
364
|
+
dueHeld: async (e) => (await t("_voltro_notification_held", E("flushAt", e), [{
|
|
333
365
|
column: "flushAt",
|
|
334
366
|
direction: "asc"
|
|
335
367
|
}], void 0)).map(s),
|
|
@@ -337,11 +369,11 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
337
369
|
for (let n of t) await e.delete("_voltro_notification_held", n);
|
|
338
370
|
}
|
|
339
371
|
};
|
|
340
|
-
},
|
|
372
|
+
}, xe = (e) => ({
|
|
341
373
|
id: "inApp",
|
|
342
374
|
deliver: async (t) => {
|
|
343
375
|
await e.addInbox({
|
|
344
|
-
id:
|
|
376
|
+
id: P(),
|
|
345
377
|
subjectId: t.to,
|
|
346
378
|
category: t.category,
|
|
347
379
|
title: t.title,
|
|
@@ -363,16 +395,16 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
363
395
|
} catch {
|
|
364
396
|
return e.getUTCHours() * 60 + e.getUTCMinutes();
|
|
365
397
|
}
|
|
366
|
-
},
|
|
398
|
+
}, Se = (e, t) => {
|
|
367
399
|
if (e.startMinute === e.endMinute) return !1;
|
|
368
400
|
let n = B(t, e.tz ?? "UTC");
|
|
369
401
|
return e.startMinute < e.endMinute ? n >= e.startMinute && n < e.endMinute : n >= e.startMinute || n < e.endMinute;
|
|
370
|
-
},
|
|
402
|
+
}, Ce = (e, t) => {
|
|
371
403
|
let n = B(t, e.tz ?? "UTC"), r = e.endMinute - n;
|
|
372
404
|
return r <= 0 && (r += 1440), new Date(t.getTime() + r * 6e4);
|
|
373
|
-
},
|
|
405
|
+
}, we = (e) => {
|
|
374
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) => {
|
|
375
|
-
let a = await e.store.getPreferences(i.to), o =
|
|
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 = {
|
|
376
408
|
to: i.to,
|
|
377
409
|
category: i.category,
|
|
378
410
|
title: i.title,
|
|
@@ -383,8 +415,44 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
383
415
|
for (let n of o) {
|
|
384
416
|
let a = t.get(n);
|
|
385
417
|
if (!a) continue;
|
|
418
|
+
if (a.deliverDetailed !== void 0) {
|
|
419
|
+
try {
|
|
420
|
+
let t = await a.deliverDetailed(u);
|
|
421
|
+
for (let a of t) await e.store.recordDelivery({
|
|
422
|
+
id: P(),
|
|
423
|
+
to: i.to,
|
|
424
|
+
category: i.category,
|
|
425
|
+
channel: n,
|
|
426
|
+
at: r(),
|
|
427
|
+
status: a.status,
|
|
428
|
+
endpoint: a.endpoint,
|
|
429
|
+
...a.error === void 0 ? {} : { error: a.error },
|
|
430
|
+
...a.clickToken === void 0 ? {} : { clickToken: a.clickToken }
|
|
431
|
+
});
|
|
432
|
+
let o = t.filter((e) => e.status === "failed");
|
|
433
|
+
t.length === 0 || o.length < t.length ? c.push(n) : l.push({
|
|
434
|
+
channel: n,
|
|
435
|
+
error: o[0]?.error ?? "all endpoints failed"
|
|
436
|
+
});
|
|
437
|
+
} catch (t) {
|
|
438
|
+
let a = t instanceof Error ? t.message : String(t);
|
|
439
|
+
l.push({
|
|
440
|
+
channel: n,
|
|
441
|
+
error: a
|
|
442
|
+
}), await e.store.recordDelivery({
|
|
443
|
+
id: P(),
|
|
444
|
+
to: i.to,
|
|
445
|
+
category: i.category,
|
|
446
|
+
channel: n,
|
|
447
|
+
at: r(),
|
|
448
|
+
status: "failed",
|
|
449
|
+
error: a
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
386
454
|
let o = {
|
|
387
|
-
id:
|
|
455
|
+
id: P(),
|
|
388
456
|
to: i.to,
|
|
389
457
|
category: i.category,
|
|
390
458
|
channel: n,
|
|
@@ -414,17 +482,17 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
414
482
|
};
|
|
415
483
|
}, o = async (t) => {
|
|
416
484
|
let n = await e.store.getQuietHours(t.to), o = new Date(r());
|
|
417
|
-
if (n &&
|
|
485
|
+
if (n && Se(n, o)) return (n.policy ?? "hold") === "drop" ? {
|
|
418
486
|
delivered: [],
|
|
419
487
|
failed: [],
|
|
420
488
|
skipped: [],
|
|
421
489
|
held: "quiet-drop"
|
|
422
490
|
} : (await e.store.enqueueHeld({
|
|
423
|
-
id:
|
|
491
|
+
id: P(),
|
|
424
492
|
kind: "quiet",
|
|
425
493
|
subjectId: t.to,
|
|
426
494
|
send: t,
|
|
427
|
-
flushAt:
|
|
495
|
+
flushAt: Ce(n, o).toISOString()
|
|
428
496
|
}), {
|
|
429
497
|
delivered: [],
|
|
430
498
|
failed: [],
|
|
@@ -434,7 +502,7 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
434
502
|
if (i > 0 && !t.channels) {
|
|
435
503
|
let n = (await e.store.pendingDigest(t.to))[0]?.flushAt ?? new Date(o.getTime() + i).toISOString();
|
|
436
504
|
return await e.store.enqueueHeld({
|
|
437
|
-
id:
|
|
505
|
+
id: P(),
|
|
438
506
|
kind: "digest",
|
|
439
507
|
subjectId: t.to,
|
|
440
508
|
send: t,
|
|
@@ -488,11 +556,11 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
488
556
|
let t = i.get(e.subjectId) ?? [];
|
|
489
557
|
t.push(e), i.set(e.subjectId, t);
|
|
490
558
|
}
|
|
491
|
-
for (let [e, t] of i) await a(
|
|
559
|
+
for (let [e, t] of i) await a(Te(e, t));
|
|
492
560
|
return await e.store.clearHeld(n.map((e) => e.id)), n.length;
|
|
493
561
|
}
|
|
494
562
|
};
|
|
495
|
-
},
|
|
563
|
+
}, Te = (e, t) => {
|
|
496
564
|
if (t.length === 1) return t[0].send;
|
|
497
565
|
let n = t.map((e) => ({
|
|
498
566
|
category: e.send.category,
|
|
@@ -507,159 +575,423 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
507
575
|
data: { items: n },
|
|
508
576
|
tenantId: t[0].send.tenantId ?? null
|
|
509
577
|
};
|
|
510
|
-
},
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
578
|
+
}, V = (e) => e.toString("base64url"), H = (e) => Buffer.from(e, "base64url"), U = (e, t) => ue("sha256", e).update(t).digest(), W = (e, t, n) => U(e, Buffer.concat([t, Buffer.from([1])])).subarray(0, n), Ee = () => {
|
|
579
|
+
let e = N("prime256v1");
|
|
580
|
+
return e.generateKeys(), V(e.getPrivateKey());
|
|
581
|
+
}, G = (e) => {
|
|
582
|
+
let t = H(e);
|
|
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
|
+
let n = N("prime256v1");
|
|
585
|
+
return n.setPrivateKey(t), V(n.getPublicKey());
|
|
586
|
+
}, De = (e) => {
|
|
587
|
+
let t = H(e), n = N("prime256v1");
|
|
588
|
+
n.setPrivateKey(t);
|
|
589
|
+
let r = n.getPublicKey();
|
|
590
|
+
return de({
|
|
591
|
+
key: {
|
|
592
|
+
kty: "EC",
|
|
593
|
+
crv: "P-256",
|
|
594
|
+
d: V(t),
|
|
595
|
+
x: V(r.subarray(1, 33)),
|
|
596
|
+
y: V(r.subarray(33, 65))
|
|
597
|
+
},
|
|
598
|
+
format: "jwk"
|
|
599
|
+
});
|
|
600
|
+
}, K = (e, t, n, r = Date.now()) => {
|
|
601
|
+
let i = new URL(e), a = {
|
|
602
|
+
aud: `${i.protocol}//${i.host}`,
|
|
603
|
+
exp: Math.floor(r / 1e3) + 43200,
|
|
604
|
+
...n === void 0 ? {} : { sub: n }
|
|
605
|
+
}, o = `${V(Buffer.from(JSON.stringify({
|
|
606
|
+
typ: "JWT",
|
|
607
|
+
alg: "ES256"
|
|
608
|
+
})))}.${V(Buffer.from(JSON.stringify(a)))}`;
|
|
609
|
+
return `vapid t=${o}.${V(pe("sha256", Buffer.from(o), {
|
|
610
|
+
key: De(t),
|
|
611
|
+
dsaEncoding: "ieee-p1363"
|
|
612
|
+
}))}, k=${G(t)}`;
|
|
613
|
+
}, Oe = Buffer.from("WebPush: info\0"), ke = Buffer.from("Content-Encoding: aes128gcm\0"), Ae = Buffer.from("Content-Encoding: nonce\0"), je = 4096, Me = (e, t, n = {}) => {
|
|
614
|
+
let r = H(e.p256dh), i = H(e.auth);
|
|
615
|
+
if (r.length !== 65 || r[0] !== 4) throw Error("push subscription p256dh must be a 65-byte uncompressed P-256 point");
|
|
616
|
+
if (i.length !== 16) throw Error("push subscription auth must be 16 bytes");
|
|
617
|
+
let a = N("prime256v1");
|
|
618
|
+
n.asPrivate ? a.setPrivateKey(n.asPrivate) : a.generateKeys();
|
|
619
|
+
let o = a.getPublicKey(), s = W(U(i, a.computeSecret(r)), Buffer.concat([
|
|
620
|
+
Oe,
|
|
621
|
+
r,
|
|
622
|
+
o
|
|
623
|
+
]), 32), c = n.salt ?? fe(16), l = U(c, s), u = W(l, ke, 16), d = W(l, Ae, 12), f = M("aes-128-gcm", u, d), p = Buffer.concat([
|
|
624
|
+
f.update(Buffer.concat([t, Buffer.from([2])])),
|
|
625
|
+
f.final(),
|
|
626
|
+
f.getAuthTag()
|
|
627
|
+
]), m = Buffer.alloc(21);
|
|
628
|
+
return c.copy(m, 0), m.writeUInt32BE(je, 16), m.writeUInt8(o.length, 20), Buffer.concat([
|
|
629
|
+
m,
|
|
630
|
+
o,
|
|
631
|
+
p
|
|
632
|
+
]);
|
|
633
|
+
}, Ne = (e, t) => {
|
|
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
|
+
o.setPrivateKey(t.uaPrivate);
|
|
636
|
+
let s = o.getPublicKey(), c = o.computeSecret(i), l = U(n, W(U(t.authSecret, c), Buffer.concat([
|
|
637
|
+
Oe,
|
|
638
|
+
s,
|
|
639
|
+
i
|
|
640
|
+
]), 32)), u = W(l, ke, 16), d = W(l, Ae, 12), f = ce("aes-128-gcm", u, d);
|
|
641
|
+
f.setAuthTag(a.subarray(a.length - 16));
|
|
642
|
+
let p = Buffer.concat([f.update(a.subarray(0, a.length - 16)), f.final()]), m = p.length - 1;
|
|
643
|
+
for (; m >= 0 && p[m] === 0;) m--;
|
|
644
|
+
if (p[m] !== 2) throw Error("web-push record missing the last-record delimiter");
|
|
645
|
+
return p.subarray(0, m);
|
|
646
|
+
}, Pe = async (e, t, n) => {
|
|
647
|
+
let r = Me(e.keys, t), i = n.fetchImpl ?? fetch, a;
|
|
648
|
+
try {
|
|
649
|
+
a = await i(e.endpoint, {
|
|
650
|
+
method: "POST",
|
|
651
|
+
headers: {
|
|
652
|
+
"content-encoding": "aes128gcm",
|
|
653
|
+
"content-type": "application/octet-stream",
|
|
654
|
+
ttl: String(n.ttl ?? 86400),
|
|
655
|
+
urgency: "normal",
|
|
656
|
+
authorization: K(e.endpoint, n.privateKey, n.contact)
|
|
657
|
+
},
|
|
658
|
+
body: new Uint8Array(r)
|
|
659
|
+
});
|
|
660
|
+
} catch (e) {
|
|
661
|
+
return {
|
|
662
|
+
kind: "failed",
|
|
663
|
+
status: 0,
|
|
664
|
+
detail: e instanceof Error ? e.message : String(e)
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
if (a.status === 404 || a.status === 410) return {
|
|
668
|
+
kind: "gone",
|
|
669
|
+
status: a.status
|
|
670
|
+
};
|
|
671
|
+
if (a.status >= 200 && a.status < 300) return {
|
|
672
|
+
kind: "delivered",
|
|
673
|
+
status: a.status
|
|
674
|
+
};
|
|
675
|
+
let o = await a.text().catch(() => "");
|
|
676
|
+
return {
|
|
677
|
+
kind: "failed",
|
|
678
|
+
status: a.status,
|
|
679
|
+
detail: o.slice(0, 200)
|
|
680
|
+
};
|
|
681
|
+
}, Fe = (e, t) => {
|
|
682
|
+
let n = Buffer.from(e), r = Buffer.from(t);
|
|
683
|
+
return n.length === r.length && me(n, r);
|
|
684
|
+
}, Ie = k("_voltro_notification_push_subscriptions", {
|
|
685
|
+
id: S({ prefix: "notifpush" }),
|
|
686
|
+
subjectId: A(),
|
|
687
|
+
endpointHash: A(),
|
|
688
|
+
endpoint: A(),
|
|
689
|
+
p256dh: A(),
|
|
690
|
+
auth: A(),
|
|
691
|
+
ua: A().nullable(),
|
|
692
|
+
createdAt: j().default("now"),
|
|
693
|
+
tenantId: A().nullable()
|
|
694
|
+
}).unique("byPushEndpointHash", ["endpointHash"]).index("byPushSubject", ["subjectId"]), q = (e) => le("sha256").update(e).digest("hex"), J = () => {
|
|
695
|
+
let e = /* @__PURE__ */ new Map();
|
|
696
|
+
return {
|
|
697
|
+
upsert: async (t) => {
|
|
698
|
+
e.set(t.endpointHash, t);
|
|
699
|
+
},
|
|
700
|
+
listForSubject: async (t) => [...e.values()].filter((e) => e.subjectId === t),
|
|
701
|
+
removeForSubject: async (t, n) => {
|
|
702
|
+
let r = e.get(t);
|
|
703
|
+
return !r || r.subjectId !== n ? !1 : (e.delete(t), !0);
|
|
704
|
+
},
|
|
705
|
+
removeByHash: async (t) => {
|
|
706
|
+
e.delete(t);
|
|
707
|
+
},
|
|
708
|
+
countForSubject: async (t) => [...e.values()].filter((e) => e.subjectId === t).length
|
|
709
|
+
};
|
|
710
|
+
}, Y = "_voltro_notification_push_subscriptions", Le = (e) => {
|
|
711
|
+
let t = async (t, n) => e.query({
|
|
712
|
+
table: Y,
|
|
713
|
+
predicate: t,
|
|
714
|
+
order: [],
|
|
715
|
+
take: n,
|
|
716
|
+
skip: void 0,
|
|
717
|
+
projection: void 0
|
|
718
|
+
}), n = (e) => ({
|
|
719
|
+
subjectId: String(e.subjectId),
|
|
720
|
+
endpoint: String(e.endpoint),
|
|
721
|
+
endpointHash: String(e.endpointHash),
|
|
722
|
+
p256dh: String(e.p256dh),
|
|
723
|
+
auth: String(e.auth),
|
|
724
|
+
ua: e.ua == null ? null : String(e.ua),
|
|
725
|
+
tenantId: e.tenantId == null ? null : String(e.tenantId)
|
|
726
|
+
});
|
|
727
|
+
return {
|
|
728
|
+
upsert: async (t) => {
|
|
729
|
+
let n = x({
|
|
730
|
+
kind: "typeid",
|
|
731
|
+
prefix: "notifpush"
|
|
732
|
+
}, Y);
|
|
733
|
+
await e.upsert(Y, {
|
|
734
|
+
id: n,
|
|
735
|
+
subjectId: t.subjectId,
|
|
736
|
+
endpointHash: t.endpointHash,
|
|
737
|
+
endpoint: t.endpoint,
|
|
738
|
+
p256dh: t.p256dh,
|
|
739
|
+
auth: t.auth,
|
|
740
|
+
ua: t.ua,
|
|
741
|
+
tenantId: t.tenantId
|
|
742
|
+
}, {
|
|
743
|
+
conflictColumns: ["endpointHash"],
|
|
744
|
+
update: [
|
|
745
|
+
"subjectId",
|
|
746
|
+
"p256dh",
|
|
747
|
+
"auth",
|
|
748
|
+
"ua",
|
|
749
|
+
"tenantId"
|
|
750
|
+
]
|
|
751
|
+
});
|
|
752
|
+
},
|
|
753
|
+
listForSubject: async (e) => (await t(b("subjectId", e))).map(n),
|
|
754
|
+
removeForSubject: async (n, r) => {
|
|
755
|
+
let i = await t(v(b("endpointHash", n), b("subjectId", r)), 1);
|
|
756
|
+
return i[0] ? (await e.delete(Y, String(i[0].id)), !0) : !1;
|
|
757
|
+
},
|
|
758
|
+
removeByHash: async (n) => {
|
|
759
|
+
let r = await t(b("endpointHash", n), 1);
|
|
760
|
+
r[0] && await e.delete(Y, String(r[0].id));
|
|
761
|
+
},
|
|
762
|
+
countForSubject: async (e) => (await t(b("subjectId", e))).length
|
|
763
|
+
};
|
|
764
|
+
}, Re = Symbol.for("@voltro/plugin-notifications:web-push"), X = () => {
|
|
765
|
+
let e = globalThis;
|
|
766
|
+
return e[Re] ??= {
|
|
767
|
+
store: J(),
|
|
768
|
+
bound: !1
|
|
769
|
+
};
|
|
770
|
+
}, ze = (e) => {
|
|
771
|
+
let t = X();
|
|
772
|
+
t.store = Le(e), t.bound = !0;
|
|
773
|
+
}, Be = () => {
|
|
774
|
+
let e = X();
|
|
775
|
+
e.store = J(), e.bound = !1;
|
|
776
|
+
}, Z = (e) => {
|
|
777
|
+
let t = e ?? process.env.VOLTRO_VAPID_PRIVATE_KEY;
|
|
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
|
+
return t;
|
|
780
|
+
}, Q = 3800, Ve = (e, t) => {
|
|
781
|
+
let n = typeof e.data?.url == "string" ? e.data.url : void 0, r = {
|
|
782
|
+
title: e.title,
|
|
783
|
+
body: e.body,
|
|
784
|
+
category: e.category,
|
|
785
|
+
clickToken: t,
|
|
786
|
+
...n === void 0 ? {} : { url: n },
|
|
787
|
+
...e.data === void 0 ? {} : { data: e.data }
|
|
788
|
+
}, i = Buffer.from(JSON.stringify(r));
|
|
789
|
+
if (i.length <= Q) return i;
|
|
790
|
+
let { data: a, ...o } = r;
|
|
791
|
+
if (i = Buffer.from(JSON.stringify(o)), i.length <= Q) return i;
|
|
792
|
+
let s = i.length - Buffer.byteLength(o.body), c = Math.max(0, Q - s - Buffer.byteLength("…")), l = Buffer.from(o.body).subarray(0, c).toString("utf8").replace(/�+$/u, "");
|
|
793
|
+
return Buffer.from(JSON.stringify({
|
|
794
|
+
...o,
|
|
795
|
+
body: `${l}…`
|
|
796
|
+
}));
|
|
797
|
+
}, He = (e = {}) => (X().channelOptions = e, We(e)), Ue = () => {
|
|
798
|
+
let e = X().channelOptions;
|
|
799
|
+
G(Z(e?.privateKey));
|
|
800
|
+
}, We = (e) => ({
|
|
801
|
+
id: e.id ?? "webPush",
|
|
802
|
+
deliver: async (t) => {
|
|
803
|
+
let n = await Ge(t, e), r = n.filter((e) => e.status === "failed");
|
|
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
|
+
},
|
|
806
|
+
deliverDetailed: (t) => Ge(t, e)
|
|
807
|
+
}), Ge = async (e, t) => {
|
|
808
|
+
let n = Z(t.privateKey), r = await X().store.listForSubject(e.to), i = [];
|
|
809
|
+
for (let a of r) {
|
|
810
|
+
let r = fe(16).toString("base64url"), o = {
|
|
811
|
+
privateKey: n,
|
|
812
|
+
...t.contact === void 0 ? {} : { contact: t.contact },
|
|
813
|
+
...t.ttl === void 0 ? {} : { ttl: t.ttl },
|
|
814
|
+
...t.fetchImpl === void 0 ? {} : { fetchImpl: t.fetchImpl }
|
|
815
|
+
}, s = await Pe({
|
|
816
|
+
endpoint: a.endpoint,
|
|
817
|
+
keys: {
|
|
818
|
+
p256dh: a.p256dh,
|
|
819
|
+
auth: a.auth
|
|
820
|
+
}
|
|
821
|
+
}, Ve(e, r), o);
|
|
822
|
+
s.kind === "delivered" ? i.push({
|
|
823
|
+
endpoint: a.endpoint,
|
|
824
|
+
status: "sent",
|
|
825
|
+
clickToken: r
|
|
826
|
+
}) : s.kind === "gone" ? (await X().store.removeByHash(a.endpointHash), i.push({
|
|
827
|
+
endpoint: a.endpoint,
|
|
828
|
+
status: "failed",
|
|
829
|
+
error: `endpoint gone (${s.status}) — pruned`,
|
|
830
|
+
pruned: !0
|
|
831
|
+
})) : i.push({
|
|
832
|
+
endpoint: a.endpoint,
|
|
833
|
+
status: "failed",
|
|
834
|
+
error: `${s.status}: ${s.detail}`
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
return i;
|
|
838
|
+
}, Ke = (e) => G(Z(e)), qe = k("_voltro_notification_inbox", {
|
|
839
|
+
id: S({ prefix: "notif" }),
|
|
840
|
+
subjectId: A(),
|
|
841
|
+
category: A(),
|
|
842
|
+
title: A(),
|
|
843
|
+
body: A(),
|
|
844
|
+
data: T(),
|
|
845
|
+
readAt: j().nullable(),
|
|
846
|
+
archivedAt: j().nullable(),
|
|
847
|
+
createdAt: j().default("now"),
|
|
848
|
+
tenantId: A().nullable()
|
|
849
|
+
}).renamedFrom("notification_inbox").index("byInboxSubject", ["subjectId"]).index("byInboxSubjectCreatedAt", ["subjectId", "createdAt"]).index("byInboxSubjectUnread", ["subjectId"], { where: "\"readAt\" IS NULL" }), Je = k("_voltro_notification_preferences", {
|
|
850
|
+
id: S({ prefix: "notifpref" }),
|
|
851
|
+
subjectId: A(),
|
|
852
|
+
category: A(),
|
|
853
|
+
channel: A(),
|
|
854
|
+
enabled: y().default(!0)
|
|
527
855
|
}).renamedFrom("notification_preferences").unique("byPreferencesSubjectCategoryChannel", [
|
|
528
856
|
"subjectId",
|
|
529
857
|
"category",
|
|
530
858
|
"channel"
|
|
531
|
-
]),
|
|
532
|
-
id:
|
|
533
|
-
recipient:
|
|
534
|
-
category:
|
|
535
|
-
channel:
|
|
536
|
-
status:
|
|
537
|
-
error:
|
|
538
|
-
at:
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
859
|
+
]), Ye = k("_voltro_notification_deliveries", {
|
|
860
|
+
id: S({ prefix: "notifdlv" }),
|
|
861
|
+
recipient: A(),
|
|
862
|
+
category: A(),
|
|
863
|
+
channel: A(),
|
|
864
|
+
status: A(),
|
|
865
|
+
error: A().nullable(),
|
|
866
|
+
at: j().default("now"),
|
|
867
|
+
endpoint: A().nullable(),
|
|
868
|
+
clickToken: A().nullable(),
|
|
869
|
+
clickedAt: j().nullable()
|
|
870
|
+
}).renamedFrom("notification_deliveries").index("byDeliveriesAt", ["at"]).index("byDeliveriesClickToken", ["clickToken"]), Xe = k("_voltro_notification_topic_subscriptions", {
|
|
871
|
+
id: S({ prefix: "notiftsub" }),
|
|
872
|
+
topic: A(),
|
|
873
|
+
subjectId: A(),
|
|
874
|
+
tenantId: A().nullable()
|
|
875
|
+
}).renamedFrom("notification_topic_subscriptions").unique("byTopicSubject", ["topic", "subjectId"]).index("byTopic", ["topic"]), $ = k("_voltro_notification_quiet_hours", {
|
|
876
|
+
id: S({ prefix: "notifqh" }),
|
|
877
|
+
subjectId: A(),
|
|
878
|
+
startMinute: C(),
|
|
879
|
+
endMinute: C(),
|
|
880
|
+
tz: A().default("UTC"),
|
|
881
|
+
policy: A().default("hold")
|
|
882
|
+
}).renamedFrom("notification_quiet_hours").unique("byQuietHoursSubject", ["subjectId"]), Ze = k("_voltro_notification_held", {
|
|
883
|
+
id: S({ prefix: "notifheld" }),
|
|
884
|
+
kind: A(),
|
|
885
|
+
subjectId: A(),
|
|
886
|
+
send: T(),
|
|
887
|
+
flushAt: j()
|
|
888
|
+
}).renamedFrom("notification_held").index("byHeldFlushAt", ["flushAt"]).index("byHeldSubjectKind", ["subjectId", "kind"]), Qe = class extends h.Tag("@voltro/plugin-notifications/NotificationService")() {}, $e = (e) => async (t) => {
|
|
558
889
|
if (e !== void 0) try {
|
|
559
890
|
let n = await e(t);
|
|
560
891
|
if (typeof n == "string" && n !== "") return n;
|
|
561
892
|
} catch {}
|
|
562
893
|
return t.request.subject.id ?? "anonymous";
|
|
563
|
-
},
|
|
564
|
-
let
|
|
565
|
-
base:
|
|
566
|
-
alias:
|
|
567
|
-
instance:
|
|
894
|
+
}, et = (e) => e.request.subject.tenantId ?? null, tt = "@voltro/plugin-notifications", nt = (h = {}) => {
|
|
895
|
+
let _ = $e(h.resolveSubjectId), v = se({
|
|
896
|
+
base: tt,
|
|
897
|
+
alias: h.alias,
|
|
898
|
+
instance: h.name
|
|
568
899
|
});
|
|
569
|
-
|
|
900
|
+
D({
|
|
570
901
|
source: "plugin",
|
|
571
902
|
table: "_voltro_notification_deliveries",
|
|
572
903
|
timeColumn: "at",
|
|
573
|
-
ttlMs:
|
|
904
|
+
ttlMs: O(process.env.VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS, 2160),
|
|
574
905
|
envVar: "VOLTRO_NOTIFICATION_DELIVERIES_TTL_HOURS"
|
|
575
|
-
}),
|
|
906
|
+
}), D({
|
|
576
907
|
source: "plugin",
|
|
577
908
|
table: "_voltro_notification_inbox",
|
|
578
909
|
timeColumn: "createdAt",
|
|
579
|
-
ttlMs:
|
|
910
|
+
ttlMs: O(process.env.VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS, 4320),
|
|
580
911
|
envVar: "VOLTRO_NOTIFICATION_INBOX_READ_TTL_HOURS",
|
|
581
|
-
where:
|
|
582
|
-
}),
|
|
912
|
+
where: ae("readAt")
|
|
913
|
+
}), D({
|
|
583
914
|
source: "plugin",
|
|
584
915
|
table: "_voltro_notification_held",
|
|
585
916
|
timeColumn: "flushAt",
|
|
586
|
-
ttlMs:
|
|
917
|
+
ttlMs: O(process.env.VOLTRO_NOTIFICATION_HELD_TTL_HOURS, 168),
|
|
587
918
|
envVar: "VOLTRO_NOTIFICATION_HELD_TTL_HOURS"
|
|
588
919
|
});
|
|
589
|
-
let
|
|
590
|
-
addInbox: (e) =>
|
|
591
|
-
listInbox: (e, t) =>
|
|
592
|
-
markRead: (e, t) =>
|
|
593
|
-
markUnread: (e, t) =>
|
|
594
|
-
markAllRead: (e) =>
|
|
595
|
-
setArchived: (e, t, n) =>
|
|
596
|
-
unreadCount: (e) =>
|
|
597
|
-
getPreferences: (e) =>
|
|
598
|
-
setPreference: (e) =>
|
|
599
|
-
recordDelivery: (e) =>
|
|
600
|
-
listDeliveries: (e) =>
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
920
|
+
let y = h.store === void 0, b = { current: h.store ?? R() }, x = {
|
|
921
|
+
addInbox: (e) => b.current.addInbox(e),
|
|
922
|
+
listInbox: (e, t) => b.current.listInbox(e, t),
|
|
923
|
+
markRead: (e, t) => b.current.markRead(e, t),
|
|
924
|
+
markUnread: (e, t) => b.current.markUnread(e, t),
|
|
925
|
+
markAllRead: (e) => b.current.markAllRead(e),
|
|
926
|
+
setArchived: (e, t, n) => b.current.setArchived(e, t, n),
|
|
927
|
+
unreadCount: (e) => b.current.unreadCount(e),
|
|
928
|
+
getPreferences: (e) => b.current.getPreferences(e),
|
|
929
|
+
setPreference: (e) => b.current.setPreference(e),
|
|
930
|
+
recordDelivery: (e) => b.current.recordDelivery(e),
|
|
931
|
+
listDeliveries: (e) => b.current.listDeliveries(e),
|
|
932
|
+
markClicked: (e) => b.current.markClicked(e),
|
|
933
|
+
subscribeTopic: (e) => b.current.subscribeTopic(e),
|
|
934
|
+
unsubscribeTopic: (e, t) => b.current.unsubscribeTopic(e, t),
|
|
935
|
+
listTopicSubscribers: (e) => b.current.listTopicSubscribers(e),
|
|
936
|
+
getQuietHours: (e) => b.current.getQuietHours(e),
|
|
937
|
+
setQuietHours: (e) => b.current.setQuietHours(e),
|
|
938
|
+
clearQuietHours: (e) => b.current.clearQuietHours(e),
|
|
939
|
+
enqueueHeld: (e) => b.current.enqueueHeld(e),
|
|
940
|
+
pendingDigest: (e) => b.current.pendingDigest(e),
|
|
941
|
+
dueHeld: (e) => b.current.dueHeld(e),
|
|
942
|
+
clearHeld: (e) => b.current.clearHeld(e)
|
|
943
|
+
}, S = h.channels ?? [I()], C = S.some((e) => e.id === "inApp") ? S : [...S, xe(x)], w = h.digestWindowMs ?? 0, T = C.some((e) => e.id === "webPush"), E = we({
|
|
944
|
+
channels: C,
|
|
945
|
+
store: x,
|
|
946
|
+
digestWindowMs: w
|
|
947
|
+
}), k, A = [
|
|
616
948
|
{
|
|
617
949
|
...n,
|
|
618
950
|
description: "The caller's in-app inbox items.",
|
|
619
|
-
execute: (e, t) =>
|
|
951
|
+
execute: (e, t) => g.promise(async () => E.inbox(await _(t), e))
|
|
620
952
|
},
|
|
621
953
|
{
|
|
622
|
-
...
|
|
954
|
+
...f,
|
|
623
955
|
description: "The caller's unread count.",
|
|
624
|
-
execute: (e, t) =>
|
|
956
|
+
execute: (e, t) => g.promise(async () => ({ count: await E.unreadCount(await _(t)) }))
|
|
625
957
|
},
|
|
626
958
|
{
|
|
627
959
|
...i,
|
|
628
960
|
description: "Mark an inbox item read.",
|
|
629
|
-
execute: (e, t) =>
|
|
961
|
+
execute: (e, t) => g.promise(async () => ({ ok: await E.markRead(e.id, await _(t)) }))
|
|
630
962
|
},
|
|
631
963
|
{
|
|
632
964
|
...a,
|
|
633
965
|
description: "Mark an inbox item unread again.",
|
|
634
|
-
execute: (e, t) =>
|
|
966
|
+
execute: (e, t) => g.promise(async () => ({ ok: await E.markUnread(e.id, await _(t)) }))
|
|
635
967
|
},
|
|
636
968
|
{
|
|
637
969
|
...r,
|
|
638
970
|
description: "Mark every unread item in the caller's inbox read.",
|
|
639
|
-
execute: (e, t) =>
|
|
971
|
+
execute: (e, t) => g.promise(async () => ({ count: await E.markAllRead(await _(t)) }))
|
|
640
972
|
},
|
|
641
973
|
{
|
|
642
974
|
...e,
|
|
643
975
|
description: "Archive an inbox item — a different state from read; this is what empties the inbox.",
|
|
644
|
-
execute: (e, t) =>
|
|
976
|
+
execute: (e, t) => g.promise(async () => ({ ok: await E.setArchived(e.id, await _(t), !0) }))
|
|
645
977
|
},
|
|
646
978
|
{
|
|
647
|
-
...
|
|
979
|
+
...d,
|
|
648
980
|
description: "Return an archived item to the inbox.",
|
|
649
|
-
execute: (e, t) =>
|
|
981
|
+
execute: (e, t) => g.promise(async () => ({ ok: await E.setArchived(e.id, await _(t), !1) }))
|
|
650
982
|
},
|
|
651
983
|
{
|
|
652
984
|
...s,
|
|
653
985
|
description: "The caller's channel preferences.",
|
|
654
|
-
execute: (e, t) =>
|
|
986
|
+
execute: (e, t) => g.promise(async () => E.getPreferences(await _(t)))
|
|
655
987
|
},
|
|
656
988
|
{
|
|
657
989
|
...c,
|
|
658
990
|
description: "Toggle a channel on/off for a category.",
|
|
659
|
-
execute: (e, t) =>
|
|
991
|
+
execute: (e, t) => g.promise(async () => {
|
|
660
992
|
let { category: n, channel: r, enabled: i } = e;
|
|
661
|
-
return await
|
|
662
|
-
subjectId: await
|
|
993
|
+
return await E.setPreference({
|
|
994
|
+
subjectId: await _(t),
|
|
663
995
|
category: n,
|
|
664
996
|
channel: r,
|
|
665
997
|
enabled: i
|
|
@@ -669,20 +1001,20 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
669
1001
|
{
|
|
670
1002
|
...u,
|
|
671
1003
|
description: "Subscribe the caller to a broadcast topic.",
|
|
672
|
-
execute: (e, t) =>
|
|
1004
|
+
execute: (e, t) => g.promise(async () => (await E.subscribe(e.topic, await _(t), et(t)), { ok: !0 }))
|
|
673
1005
|
},
|
|
674
1006
|
{
|
|
675
|
-
...
|
|
1007
|
+
...p,
|
|
676
1008
|
description: "Unsubscribe the caller from a broadcast topic.",
|
|
677
|
-
execute: (e, t) =>
|
|
1009
|
+
execute: (e, t) => g.promise(async () => (await E.unsubscribe(e.topic, await _(t)), { ok: !0 }))
|
|
678
1010
|
},
|
|
679
1011
|
{
|
|
680
1012
|
...l,
|
|
681
1013
|
description: "Set the caller's quiet-hours (DND) window.",
|
|
682
|
-
execute: (e, t) =>
|
|
1014
|
+
execute: (e, t) => g.promise(async () => {
|
|
683
1015
|
let n = e;
|
|
684
|
-
return await
|
|
685
|
-
subjectId: await
|
|
1016
|
+
return await E.setQuietHours({
|
|
1017
|
+
subjectId: await _(t),
|
|
686
1018
|
startMinute: n.startMinute,
|
|
687
1019
|
endMinute: n.endMinute,
|
|
688
1020
|
...n.tz ? { tz: n.tz } : {},
|
|
@@ -693,25 +1025,58 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
693
1025
|
{
|
|
694
1026
|
...t,
|
|
695
1027
|
description: "Clear the caller's quiet-hours window.",
|
|
696
|
-
execute: (e, t) =>
|
|
1028
|
+
execute: (e, t) => g.promise(async () => (await E.clearQuietHours(await _(t)), { ok: !0 }))
|
|
1029
|
+
}
|
|
1030
|
+
], j = T ? [
|
|
1031
|
+
{
|
|
1032
|
+
...m,
|
|
1033
|
+
description: "The VAPID application-server key a subscribing browser needs.",
|
|
1034
|
+
execute: () => g.sync(() => ({ key: Ke(X().channelOptions?.privateKey) }))
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
...ne,
|
|
1038
|
+
description: "Register the calling subject's browser push subscription (one row per endpoint).",
|
|
1039
|
+
execute: (e, t) => g.promise(async () => {
|
|
1040
|
+
let n = e;
|
|
1041
|
+
return await X().store.upsert({
|
|
1042
|
+
subjectId: await _(t),
|
|
1043
|
+
endpoint: n.endpoint,
|
|
1044
|
+
endpointHash: q(n.endpoint),
|
|
1045
|
+
p256dh: n.p256dh,
|
|
1046
|
+
auth: n.auth,
|
|
1047
|
+
ua: n.ua ?? null,
|
|
1048
|
+
tenantId: et(t)
|
|
1049
|
+
}), { ok: !0 };
|
|
1050
|
+
})
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
...re,
|
|
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 _(t)) }))
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
...te,
|
|
1059
|
+
description: "How many push endpoints the calling subject has registered.",
|
|
1060
|
+
execute: (e, t) => g.promise(async () => ({ endpoints: await X().store.countForSubject(await _(t)) }))
|
|
697
1061
|
}
|
|
698
|
-
],
|
|
1062
|
+
] : [], M = (e) => ({
|
|
699
1063
|
kind: "json",
|
|
700
1064
|
data: e
|
|
701
|
-
}),
|
|
1065
|
+
}), ce = (e, t) => ({
|
|
702
1066
|
kind: "json",
|
|
703
1067
|
status: e,
|
|
704
1068
|
data: { error: t }
|
|
705
|
-
}),
|
|
1069
|
+
}), N = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), le = [{
|
|
706
1070
|
method: "GET",
|
|
707
1071
|
path: "/deliveries",
|
|
708
1072
|
description: "Recent delivery records (newest first) + per-channel + per-status counts.",
|
|
709
|
-
handler: () =>
|
|
710
|
-
let e = await
|
|
1073
|
+
handler: () => g.promise(async () => {
|
|
1074
|
+
let e = await x.listDeliveries({ limit: 200 }), t = {}, n = 0, r = 0;
|
|
711
1075
|
for (let i of e) t[i.channel] = (t[i.channel] ?? 0) + 1, i.status === "sent" ? n++ : r++;
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1076
|
+
let i = e.slice(0, 100).map(({ clickToken: e, ...t }) => t);
|
|
1077
|
+
return M({
|
|
1078
|
+
deliveries: i,
|
|
1079
|
+
channels: C.map((e) => e.id),
|
|
715
1080
|
stats: {
|
|
716
1081
|
sent: n,
|
|
717
1082
|
failed: r,
|
|
@@ -724,45 +1089,79 @@ var A = class extends m.TaggedError()("PushTokenRejected", {
|
|
|
724
1089
|
path: "/inbox",
|
|
725
1090
|
description: "In-app inbox for a subject (?subjectId=…).",
|
|
726
1091
|
handler: (e) => {
|
|
727
|
-
let t =
|
|
728
|
-
return t ?
|
|
729
|
-
items: await
|
|
730
|
-
unread: await
|
|
731
|
-
})) :
|
|
1092
|
+
let t = N(e.url, "subjectId");
|
|
1093
|
+
return t ? g.promise(async () => M({
|
|
1094
|
+
items: await x.listInbox(t, { limit: 100 }),
|
|
1095
|
+
unread: await x.unreadCount(t)
|
|
1096
|
+
})) : g.succeed(ce(400, "subjectId query param required"));
|
|
732
1097
|
}
|
|
733
1098
|
}];
|
|
734
|
-
return
|
|
735
|
-
name:
|
|
736
|
-
baseName:
|
|
1099
|
+
return oe({
|
|
1100
|
+
name: v,
|
|
1101
|
+
baseName: tt,
|
|
737
1102
|
description: "Unified notifications — email/slack/sms/push/in-app with preference routing, inbox + delivery log.",
|
|
738
1103
|
permissions: ["inspect:read", "store:write"],
|
|
739
|
-
...
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1104
|
+
...h.tables === !1 ? {} : { extendSchema: { tables: [
|
|
1105
|
+
qe,
|
|
1106
|
+
Je,
|
|
1107
|
+
Ye,
|
|
1108
|
+
Xe,
|
|
1109
|
+
$,
|
|
1110
|
+
Ze,
|
|
1111
|
+
Ie
|
|
746
1112
|
] } },
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1113
|
+
...T ? {
|
|
1114
|
+
declaredEnv: [{
|
|
1115
|
+
name: "VOLTRO_VAPID_PRIVATE_KEY",
|
|
1116
|
+
required: !0,
|
|
1117
|
+
secret: !0,
|
|
1118
|
+
generate: "p256",
|
|
1119
|
+
description: "VAPID P-256 private scalar (base64url) for web push. Minted per project by `voltro dev`; production sets its own — never a shared or default value."
|
|
1120
|
+
}],
|
|
1121
|
+
httpRoutes: [{
|
|
1122
|
+
method: "POST",
|
|
1123
|
+
path: "/_voltro/notifications/clicked",
|
|
1124
|
+
handle: async (e) => {
|
|
1125
|
+
let t;
|
|
1126
|
+
try {
|
|
1127
|
+
t = JSON.parse(Buffer.from(e.rawBody).toString("utf8")).token;
|
|
1128
|
+
} catch {}
|
|
1129
|
+
if (typeof t != "string" || t === "" || !Fe(t, t)) return {
|
|
1130
|
+
status: 400,
|
|
1131
|
+
body: JSON.stringify({ error: "token required" }),
|
|
1132
|
+
contentType: "application/json"
|
|
1133
|
+
};
|
|
1134
|
+
let n = await x.markClicked(t);
|
|
1135
|
+
return {
|
|
1136
|
+
status: n ? 200 : 404,
|
|
1137
|
+
body: JSON.stringify({ ok: n }),
|
|
1138
|
+
contentType: "application/json"
|
|
1139
|
+
};
|
|
1140
|
+
},
|
|
1141
|
+
maxBodyBytes: 4096
|
|
1142
|
+
}]
|
|
1143
|
+
} : {},
|
|
1144
|
+
services: ie.succeed(Qe, E),
|
|
1145
|
+
routes: [...A, ...j],
|
|
1146
|
+
rpcClientDescriptors: T ? [...o, ...ee] : o,
|
|
1147
|
+
inspectEndpoints: le,
|
|
751
1148
|
bindDataStore: (e) => {
|
|
752
|
-
|
|
753
|
-
let t =
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}, t), typeof
|
|
757
|
-
},
|
|
758
|
-
onActivate: (e) =>
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1149
|
+
y && (b.current = be(e)), ze(e);
|
|
1150
|
+
let t = h.flushIntervalMs ?? 3e4;
|
|
1151
|
+
k = setInterval(() => {
|
|
1152
|
+
E.flushDue().catch(() => {});
|
|
1153
|
+
}, t), typeof k.unref == "function" && k.unref();
|
|
1154
|
+
},
|
|
1155
|
+
onActivate: (e) => g.sync(() => {
|
|
1156
|
+
T && Ue(), e.logger.info("notifications active", {
|
|
1157
|
+
channels: C.map((e) => e.id),
|
|
1158
|
+
digestWindowMs: w
|
|
1159
|
+
});
|
|
1160
|
+
}),
|
|
1161
|
+
onDeactivate: () => g.sync(() => {
|
|
1162
|
+
k && clearInterval(k), k = void 0;
|
|
764
1163
|
})
|
|
765
1164
|
});
|
|
766
1165
|
};
|
|
767
1166
|
//#endregion
|
|
768
|
-
export {
|
|
1167
|
+
export { Qe as NotificationService, F as PushTokenRejected, we as buildNotificationService, I as consoleChannel, ve as customChannel, be as dataStoreNotificationStore, Le as dataStorePushSubscriptionStore, Ne as decryptWebPushPayload, Ye as deliveriesTable, ge as emailChannel, Me as encryptWebPushPayload, q as endpointHashOf, Ee as generateVapidPrivateKey, Ze as heldTable, xe as inAppChannel, Se as inQuietHours, qe as inboxTable, $e as makeSubjectId, R as memoryNotificationStore, J as memoryPushSubscriptionStore, B as minuteOfDayInZone, nt as notificationsPlugin, Je as preferencesTable, ye as pushChannel, Ie as pushSubscriptionsTable, Ce as quietHoursEnd, $ as quietHoursTable, Be as resetWebPushStoreForTest, L as resolveChannels, Pe as sendWebPush, _e as smsChannel, Xe as topicSubscriptionsTable, K as vapidAuthorization, G as vapidPublicKeyFor, He as webPushChannel, Ke as webPushPublicKey, he as webhookChannel };
|