@voltro/plugin-notifications 0.85.0 → 0.86.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.
@@ -0,0 +1,161 @@
1
+ import { sendWebPush as e, vapidPublicKeyFor as t } from "./webPushProtocol.js";
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
+ import { createHash as l, randomBytes as u } from "node:crypto";
4
+ //#region src/webPush.ts
5
+ var d = o("_voltro_notification_push_subscriptions", {
6
+ id: a({ prefix: "notifpush" }),
7
+ subjectId: s(),
8
+ endpointHash: s(),
9
+ endpoint: s(),
10
+ p256dh: s(),
11
+ auth: s(),
12
+ ua: s().nullable(),
13
+ createdAt: c().default("now"),
14
+ tenantId: s().nullable()
15
+ }).unique("byPushEndpointHash", ["endpointHash"]).index("byPushSubject", ["subjectId"]), f = (e) => l("sha256").update(e).digest("hex"), p = () => {
16
+ let e = /* @__PURE__ */ new Map();
17
+ return {
18
+ upsert: async (t) => {
19
+ e.set(t.endpointHash, t);
20
+ },
21
+ listForSubject: async (t) => [...e.values()].filter((e) => e.subjectId === t),
22
+ removeForSubject: async (t, n) => {
23
+ let r = e.get(t);
24
+ return !r || r.subjectId !== n ? !1 : (e.delete(t), !0);
25
+ },
26
+ removeByHash: async (t) => {
27
+ e.delete(t);
28
+ },
29
+ countForSubject: async (t) => [...e.values()].filter((e) => e.subjectId === t).length
30
+ };
31
+ }, m = "_voltro_notification_push_subscriptions", h = (e) => {
32
+ let t = async (t, n) => e.query({
33
+ table: m,
34
+ predicate: t,
35
+ order: [],
36
+ take: n,
37
+ skip: void 0,
38
+ projection: void 0
39
+ }), a = (e) => ({
40
+ subjectId: String(e.subjectId),
41
+ endpoint: String(e.endpoint),
42
+ endpointHash: String(e.endpointHash),
43
+ p256dh: String(e.p256dh),
44
+ auth: String(e.auth),
45
+ ua: e.ua == null ? null : String(e.ua),
46
+ tenantId: e.tenantId == null ? null : String(e.tenantId)
47
+ });
48
+ return {
49
+ upsert: async (t) => {
50
+ let n = i({
51
+ kind: "typeid",
52
+ prefix: "notifpush"
53
+ }, m);
54
+ await e.upsert(m, {
55
+ id: n,
56
+ subjectId: t.subjectId,
57
+ endpointHash: t.endpointHash,
58
+ endpoint: t.endpoint,
59
+ p256dh: t.p256dh,
60
+ auth: t.auth,
61
+ ua: t.ua,
62
+ tenantId: t.tenantId
63
+ }, {
64
+ conflictColumns: ["endpointHash"],
65
+ update: [
66
+ "subjectId",
67
+ "p256dh",
68
+ "auth",
69
+ "ua",
70
+ "tenantId"
71
+ ]
72
+ });
73
+ },
74
+ listForSubject: async (e) => (await t(r("subjectId", e))).map(a),
75
+ removeForSubject: async (i, a) => {
76
+ let o = await t(n(r("endpointHash", i), r("subjectId", a)), 1);
77
+ return o[0] ? (await e.delete(m, String(o[0].id)), !0) : !1;
78
+ },
79
+ removeByHash: async (n) => {
80
+ let i = await t(r("endpointHash", n), 1);
81
+ i[0] && await e.delete(m, String(i[0].id));
82
+ },
83
+ countForSubject: async (e) => (await t(r("subjectId", e))).length
84
+ };
85
+ }, g = Symbol.for("@voltro/plugin-notifications:web-push"), _ = () => {
86
+ let e = globalThis;
87
+ return e[g] ??= {
88
+ store: p(),
89
+ bound: !1
90
+ };
91
+ }, v = (e) => {
92
+ let t = _();
93
+ t.store = h(e), t.bound = !0;
94
+ }, y = () => {
95
+ let e = _();
96
+ e.store = p(), e.bound = !1;
97
+ }, b = (e) => {
98
+ let t = e ?? process.env.VOLTRO_VAPID_PRIVATE_KEY;
99
+ 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.");
100
+ return t;
101
+ }, x = 3800, S = (e, t) => {
102
+ let n = typeof e.data?.url == "string" ? e.data.url : void 0, r = {
103
+ title: e.title,
104
+ body: e.body,
105
+ category: e.category,
106
+ clickToken: t,
107
+ ...n === void 0 ? {} : { url: n },
108
+ ...e.data === void 0 ? {} : { data: e.data }
109
+ }, i = Buffer.from(JSON.stringify(r));
110
+ if (i.length <= x) return i;
111
+ let { data: a, ...o } = r;
112
+ if (i = Buffer.from(JSON.stringify(o)), i.length <= x) return i;
113
+ let s = i.length - Buffer.byteLength(o.body), c = Math.max(0, x - s - Buffer.byteLength("…")), l = Buffer.from(o.body).subarray(0, c).toString("utf8").replace(/�+$/u, "");
114
+ return Buffer.from(JSON.stringify({
115
+ ...o,
116
+ body: `${l}…`
117
+ }));
118
+ }, C = (e = {}) => (_().channelOptions = e, T(e)), w = () => {
119
+ let e = _().channelOptions;
120
+ t(b(e?.privateKey));
121
+ }, T = (e) => ({
122
+ id: e.id ?? "webPush",
123
+ deliver: async (t) => {
124
+ let n = await E(t, e), r = n.filter((e) => e.status === "failed");
125
+ if (n.length > 0 && r.length === n.length) throw Error(`web push failed for all ${r.length} endpoint(s): ${r[0].error ?? "unknown"}`);
126
+ },
127
+ deliverDetailed: (t) => E(t, e)
128
+ }), E = async (t, n) => {
129
+ let r = b(n.privateKey), i = await _().store.listForSubject(t.to), a = [];
130
+ for (let o of i) {
131
+ let i = u(16).toString("base64url"), s = {
132
+ privateKey: r,
133
+ ...n.contact === void 0 ? {} : { contact: n.contact },
134
+ ...n.ttl === void 0 ? {} : { ttl: n.ttl },
135
+ ...n.fetchImpl === void 0 ? {} : { fetchImpl: n.fetchImpl }
136
+ }, c = await e({
137
+ endpoint: o.endpoint,
138
+ keys: {
139
+ p256dh: o.p256dh,
140
+ auth: o.auth
141
+ }
142
+ }, S(t, i), s);
143
+ c.kind === "delivered" ? a.push({
144
+ endpoint: o.endpoint,
145
+ status: "sent",
146
+ clickToken: i
147
+ }) : c.kind === "gone" ? (await _().store.removeByHash(o.endpointHash), a.push({
148
+ endpoint: o.endpoint,
149
+ status: "failed",
150
+ error: `endpoint gone (${c.status}) — pruned`,
151
+ pruned: !0
152
+ })) : a.push({
153
+ endpoint: o.endpoint,
154
+ status: "failed",
155
+ error: `${c.status}: ${c.detail}`
156
+ });
157
+ }
158
+ return a;
159
+ }, D = (e) => t(b(e));
160
+ //#endregion
161
+ 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 };
@@ -0,0 +1,117 @@
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
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
+ let e = n("prime256v1");
5
+ return e.generateKeys(), c(p(e.getPrivateKey()));
6
+ }, p = (e) => {
7
+ if (e.length === 32) return e;
8
+ if (e.length > 32) throw Error(`P-256 scalar must be at most 32 bytes (got ${e.length})`);
9
+ return Buffer.concat([Buffer.alloc(32 - e.length), e]);
10
+ }, m = (e) => {
11
+ let t = l(e);
12
+ if (t.length !== 32) throw Error(`VOLTRO_VAPID_PRIVATE_KEY must be a base64url 32-byte P-256 scalar (got ${t.length} bytes)`);
13
+ return t;
14
+ }, h = (e) => {
15
+ let t = m(e), r = n("prime256v1");
16
+ return r.setPrivateKey(t), c(r.getPublicKey());
17
+ }, g = (e) => {
18
+ let t = m(e), r = n("prime256v1");
19
+ r.setPrivateKey(t);
20
+ let a = r.getPublicKey();
21
+ return i({
22
+ key: {
23
+ kty: "EC",
24
+ crv: "P-256",
25
+ d: c(t),
26
+ x: c(a.subarray(1, 33)),
27
+ y: c(a.subarray(33, 65))
28
+ },
29
+ format: "jwk"
30
+ });
31
+ }, _ = (e, t, n, r = Date.now()) => {
32
+ let i = new URL(e), a = {
33
+ aud: `${i.protocol}//${i.host}`,
34
+ exp: Math.floor(r / 1e3) + 43200,
35
+ ...n === void 0 ? {} : { sub: n }
36
+ }, s = `${c(Buffer.from(JSON.stringify({
37
+ typ: "JWT",
38
+ alg: "ES256"
39
+ })))}.${c(Buffer.from(JSON.stringify(a)))}`;
40
+ return `vapid t=${s}.${c(o("sha256", Buffer.from(s), {
41
+ key: g(t),
42
+ dsaEncoding: "ieee-p1363"
43
+ }))}, k=${h(t)}`;
44
+ }, v = Buffer.from("WebPush: info\0"), y = Buffer.from("Content-Encoding: aes128gcm\0"), b = Buffer.from("Content-Encoding: nonce\0"), x = 4096, S = (t, r, i = {}) => {
45
+ let o = l(t.p256dh), s = l(t.auth);
46
+ if (o.length !== 65 || o[0] !== 4) throw Error("push subscription p256dh must be a 65-byte uncompressed P-256 point");
47
+ if (s.length !== 16) throw Error("push subscription auth must be 16 bytes");
48
+ let c = n("prime256v1");
49
+ i.asPrivate ? c.setPrivateKey(i.asPrivate) : c.generateKeys();
50
+ let f = c.getPublicKey(), p = d(u(s, c.computeSecret(o)), Buffer.concat([
51
+ v,
52
+ o,
53
+ f
54
+ ]), 32), m = i.salt ?? a(16), h = u(m, p), g = d(h, y, 16), _ = d(h, b, 12), S = e("aes-128-gcm", g, _), C = Buffer.concat([
55
+ S.update(Buffer.concat([r, Buffer.from([2])])),
56
+ S.final(),
57
+ S.getAuthTag()
58
+ ]), w = Buffer.alloc(21);
59
+ return m.copy(w, 0), w.writeUInt32BE(x, 16), w.writeUInt8(f.length, 20), Buffer.concat([
60
+ w,
61
+ f,
62
+ C
63
+ ]);
64
+ }, C = (e, r) => {
65
+ let i = e.subarray(0, 16), a = e.readUInt8(20), o = e.subarray(21, 21 + a), s = e.subarray(21 + a), c = n("prime256v1");
66
+ c.setPrivateKey(r.uaPrivate);
67
+ let l = c.getPublicKey(), f = c.computeSecret(o), p = u(i, d(u(r.authSecret, f), Buffer.concat([
68
+ v,
69
+ l,
70
+ o
71
+ ]), 32)), m = d(p, y, 16), h = d(p, b, 12), g = t("aes-128-gcm", m, h);
72
+ g.setAuthTag(s.subarray(s.length - 16));
73
+ let _ = Buffer.concat([g.update(s.subarray(0, s.length - 16)), g.final()]), x = _.length - 1;
74
+ for (; x >= 0 && _[x] === 0;) x--;
75
+ if (_[x] !== 2) throw Error("web-push record missing the last-record delimiter");
76
+ return _.subarray(0, x);
77
+ }, w = async (e, t, n) => {
78
+ let r = S(e.keys, t), i = n.fetchImpl ?? fetch, a;
79
+ try {
80
+ a = await i(e.endpoint, {
81
+ method: "POST",
82
+ headers: {
83
+ "content-encoding": "aes128gcm",
84
+ "content-type": "application/octet-stream",
85
+ ttl: String(n.ttl ?? 86400),
86
+ urgency: "normal",
87
+ authorization: _(e.endpoint, n.privateKey, n.contact)
88
+ },
89
+ body: new Uint8Array(r)
90
+ });
91
+ } catch (e) {
92
+ return {
93
+ kind: "failed",
94
+ status: 0,
95
+ detail: e instanceof Error ? e.message : String(e)
96
+ };
97
+ }
98
+ if (a.status === 404 || a.status === 410) return {
99
+ kind: "gone",
100
+ status: a.status
101
+ };
102
+ if (a.status >= 200 && a.status < 300) return {
103
+ kind: "delivered",
104
+ status: a.status
105
+ };
106
+ let o = await a.text().catch(() => "");
107
+ return {
108
+ kind: "failed",
109
+ status: a.status,
110
+ detail: o.slice(0, 200)
111
+ };
112
+ }, T = (e, t) => {
113
+ let n = Buffer.from(e), r = Buffer.from(t);
114
+ return n.length === r.length && s(n, r);
115
+ };
116
+ //#endregion
117
+ 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.85.0",
3
+ "version": "0.86.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.85.0",
53
- "@voltro/database": "0.85.0",
54
- "@voltro/protocol": "0.85.0"
52
+ "@voltro/client": "0.86.0",
53
+ "@voltro/database": "0.86.0",
54
+ "@voltro/protocol": "0.86.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "effect": "4.0.1",