@voltro/plugin-webhooks 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,88 @@
1
+ import { createHmac as e, randomBytes as t, timingSafeEqual as n } from "node:crypto";
2
+ //#region src/standardWebhooks.ts
3
+ var r = "webhook-id", i = "webhook-timestamp", a = "webhook-signature", o = "v1", s = "whsec_", c = 24, l = 64, u = 300, d = () => `${s}${t(32).toString("base64")}`, f = (e) => {
4
+ if (!e.startsWith("whsec_")) throw Error(`standard-webhooks: the signing secret must be the spec's serialization — "${s}" + base64(24..64 random bytes). Mint one with generateStandardWebhooksSecret(). (A hex secret is also valid base64, so accepting it would sign with the wrong key bytes and every conformant consumer would reject the delivery.)`);
5
+ let t = Buffer.from(e.slice(6), "base64");
6
+ if (t.length < 24 || t.length > 64) throw Error(`standard-webhooks: the decoded key is ${t.length} bytes; the spec requires 24..64.`);
7
+ return t;
8
+ }, p = (e) => e.startsWith(s), m = (e, t) => {
9
+ if (t.includes(".")) throw Error(`standard-webhooks: the ${e} may not contain "." — it is the field delimiter of the signed content (id.timestamp.payload), and a "." in it makes one signed string readable two ways. Got ${JSON.stringify(t)}.`);
10
+ if (t.length === 0) throw Error(`standard-webhooks: the ${e} may not be empty`);
11
+ }, h = (e, t, n) => {
12
+ m("webhook-id", e);
13
+ let r = String(t);
14
+ return m("webhook-timestamp", r), Buffer.concat([Buffer.from(`${e}.${r}.`, "utf8"), Buffer.from(n)]);
15
+ }, g = (t, n, r, i) => {
16
+ let a = e("sha256", f(t));
17
+ return a.update(h(n, r, i)), `v1,${a.digest("base64")}`;
18
+ }, _ = (e) => {
19
+ if (e.secrets.length === 0) throw Error("standard-webhooks: at least one signing secret is required");
20
+ let t = e.secrets.map((t) => g(t, e.messageId, e.timestampSeconds, e.rawBody));
21
+ return {
22
+ [r]: e.messageId,
23
+ [i]: String(e.timestampSeconds),
24
+ [a]: t.join(" ")
25
+ };
26
+ }, v = (e, t) => {
27
+ let r = Buffer.from(e, "utf8"), i = Buffer.from(t, "utf8");
28
+ return r.length === i.length ? n(r, i) : (n(r, Buffer.alloc(r.length)), !1);
29
+ }, y = (e) => {
30
+ let t = e.headers, n = t[r], o = t[i], s = t[a];
31
+ if (!n) return {
32
+ ok: !1,
33
+ reason: `missing ${r}`
34
+ };
35
+ if (!o) return {
36
+ ok: !1,
37
+ reason: `missing ${i}`
38
+ };
39
+ if (!s) return {
40
+ ok: !1,
41
+ reason: `missing ${a}`
42
+ };
43
+ if (n.includes(".")) return {
44
+ ok: !1,
45
+ reason: "webhook-id contains the \".\" field delimiter"
46
+ };
47
+ let c = Number(o);
48
+ if (!Number.isInteger(c)) return {
49
+ ok: !1,
50
+ reason: `${i} is not an integer unix timestamp`
51
+ };
52
+ let l = e.toleranceSeconds ?? 300, u = e.nowSeconds ?? Math.floor(Date.now() / 1e3), d = Math.abs(u - c);
53
+ if (d > l) return {
54
+ ok: !1,
55
+ reason: `timestamp outside the ${l}s tolerance (off by ${d}s)`
56
+ };
57
+ let f = s.split(" ").map((e) => e.trim()).filter((e) => e.length > 0);
58
+ if (f.length === 0) return {
59
+ ok: !1,
60
+ reason: "no signature in webhook-signature"
61
+ };
62
+ if (f.every((e) => e.startsWith("v1a,"))) return {
63
+ ok: !1,
64
+ reason: "only asymmetric (v1a / ed25519) signatures were offered; this verifier implements the symmetric v1 scheme only"
65
+ };
66
+ for (let t of e.secrets) {
67
+ let r;
68
+ try {
69
+ r = g(t, n, c, e.rawBody);
70
+ } catch (e) {
71
+ return {
72
+ ok: !1,
73
+ reason: e.message
74
+ };
75
+ }
76
+ for (let e of f) if (e.startsWith("v1,") && v(r, e)) return {
77
+ ok: !0,
78
+ messageId: n,
79
+ timestampSeconds: c
80
+ };
81
+ }
82
+ return {
83
+ ok: !1,
84
+ reason: "signature mismatch"
85
+ };
86
+ };
87
+ //#endregion
88
+ export { u as STANDARD_WEBHOOKS_DEFAULT_TOLERANCE_SECONDS, r as STANDARD_WEBHOOKS_ID_HEADER, l as STANDARD_WEBHOOKS_MAX_KEY_BYTES, c as STANDARD_WEBHOOKS_MIN_KEY_BYTES, s as STANDARD_WEBHOOKS_SECRET_PREFIX, a as STANDARD_WEBHOOKS_SIGNATURE_HEADER, i as STANDARD_WEBHOOKS_TIMESTAMP_HEADER, o as STANDARD_WEBHOOKS_VERSION, d as generateStandardWebhooksSecret, p as isStandardWebhooksSecret, _ as standardWebhooksHeaders, f as standardWebhooksKey, g as standardWebhooksSignatureToken, h as standardWebhooksSignedContent, y as verifyStandardWebhooks };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-webhooks",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "description": "Webhooks plugin — first-class outgoing (multi-target, durable-workflow delivery, HMAC signing, retry policies, rate limits) + incoming (signature verification, idempotency, provider templates for Stripe / GitHub / Slack / generic).",
5
5
  "keywords": [
6
6
  "voltro",
@@ -48,12 +48,12 @@
48
48
  "node": ">=24.0.0"
49
49
  },
50
50
  "dependencies": {
51
- "@voltro/database": "0.85.0",
52
- "@voltro/integration-http": "0.85.0",
53
- "@voltro/logger": "0.85.0",
54
- "@voltro/plugin-multitenancy": "0.85.0",
55
- "@voltro/protocol": "0.85.0",
56
- "@voltro/runtime": "0.85.0"
51
+ "@voltro/database": "0.86.0",
52
+ "@voltro/integration-http": "0.86.0",
53
+ "@voltro/logger": "0.86.0",
54
+ "@voltro/plugin-multitenancy": "0.86.0",
55
+ "@voltro/protocol": "0.86.0",
56
+ "@voltro/runtime": "0.86.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "effect": "4.0.1"
@@ -1,234 +0,0 @@
1
- import { createHmac as e, randomBytes as t, timingSafeEqual as n } from "node:crypto";
2
- //#region src/dsl.ts
3
- var r = (e) => ({
4
- _tag: "incomingWebhook",
5
- ...e
6
- }), i = (e) => ({
7
- _tag: "webhookProvider",
8
- ...e
9
- }), a = (e) => {
10
- if (typeof e != "object" || !e) return !1;
11
- let t = e._tag;
12
- return t === "outgoingEvent" || t === "incomingWebhook" || t === "webhookProvider";
13
- }, o = "webhook-id", s = "webhook-timestamp", c = "webhook-signature", l = "v1", u = "whsec_", d = 24, f = 64, p = 300, m = () => `${u}${t(32).toString("base64")}`, h = (e) => {
14
- if (!e.startsWith("whsec_")) throw Error(`standard-webhooks: the signing secret must be the spec's serialization — "${u}" + base64(24..64 random bytes). Mint one with generateStandardWebhooksSecret(). (A hex secret is also valid base64, so accepting it would sign with the wrong key bytes and every conformant consumer would reject the delivery.)`);
15
- let t = Buffer.from(e.slice(6), "base64");
16
- if (t.length < 24 || t.length > 64) throw Error(`standard-webhooks: the decoded key is ${t.length} bytes; the spec requires 24..64.`);
17
- return t;
18
- }, g = (e) => e.startsWith(u), _ = (e, t) => {
19
- if (t.includes(".")) throw Error(`standard-webhooks: the ${e} may not contain "." — it is the field delimiter of the signed content (id.timestamp.payload), and a "." in it makes one signed string readable two ways. Got ${JSON.stringify(t)}.`);
20
- if (t.length === 0) throw Error(`standard-webhooks: the ${e} may not be empty`);
21
- }, v = (e, t, n) => {
22
- _("webhook-id", e);
23
- let r = String(t);
24
- return _("webhook-timestamp", r), Buffer.concat([Buffer.from(`${e}.${r}.`, "utf8"), Buffer.from(n)]);
25
- }, y = (t, n, r, i) => {
26
- let a = e("sha256", h(t));
27
- return a.update(v(n, r, i)), `v1,${a.digest("base64")}`;
28
- }, b = (e) => {
29
- if (e.secrets.length === 0) throw Error("standard-webhooks: at least one signing secret is required");
30
- let t = e.secrets.map((t) => y(t, e.messageId, e.timestampSeconds, e.rawBody));
31
- return {
32
- [o]: e.messageId,
33
- [s]: String(e.timestampSeconds),
34
- [c]: t.join(" ")
35
- };
36
- }, x = (e, t) => {
37
- let r = Buffer.from(e, "utf8"), i = Buffer.from(t, "utf8");
38
- return r.length === i.length ? n(r, i) : (n(r, Buffer.alloc(r.length)), !1);
39
- }, S = (e) => {
40
- let t = e.headers, n = t[o], r = t[s], i = t[c];
41
- if (!n) return {
42
- ok: !1,
43
- reason: `missing ${o}`
44
- };
45
- if (!r) return {
46
- ok: !1,
47
- reason: `missing ${s}`
48
- };
49
- if (!i) return {
50
- ok: !1,
51
- reason: `missing ${c}`
52
- };
53
- if (n.includes(".")) return {
54
- ok: !1,
55
- reason: "webhook-id contains the \".\" field delimiter"
56
- };
57
- let a = Number(r);
58
- if (!Number.isInteger(a)) return {
59
- ok: !1,
60
- reason: `${s} is not an integer unix timestamp`
61
- };
62
- let l = e.toleranceSeconds ?? 300, u = e.nowSeconds ?? Math.floor(Date.now() / 1e3), d = Math.abs(u - a);
63
- if (d > l) return {
64
- ok: !1,
65
- reason: `timestamp outside the ${l}s tolerance (off by ${d}s)`
66
- };
67
- let f = i.split(" ").map((e) => e.trim()).filter((e) => e.length > 0);
68
- if (f.length === 0) return {
69
- ok: !1,
70
- reason: "no signature in webhook-signature"
71
- };
72
- if (f.every((e) => e.startsWith("v1a,"))) return {
73
- ok: !1,
74
- reason: "only asymmetric (v1a / ed25519) signatures were offered; this verifier implements the symmetric v1 scheme only"
75
- };
76
- for (let t of e.secrets) {
77
- let r;
78
- try {
79
- r = y(t, n, a, e.rawBody);
80
- } catch (e) {
81
- return {
82
- ok: !1,
83
- reason: e.message
84
- };
85
- }
86
- for (let e of f) if (e.startsWith("v1,") && x(r, e)) return {
87
- ok: !0,
88
- messageId: n,
89
- timestampSeconds: a
90
- };
91
- }
92
- return {
93
- ok: !1,
94
- reason: "signature mismatch"
95
- };
96
- }, C = (e) => e === "hmacSha256" ? "sha256" : "sha1", w = (t, n, r, i = "hex") => {
97
- let a = e(C(t), n);
98
- return a.update(r), a.digest(i);
99
- };
100
- function T(e, t) {
101
- if (e._tag !== "hmac" || typeof e.header != "string") throw Error(`webhook signing: cannot ${t} with this scheme — expected one of "standardWebhooks" | "hmac" | "custom" with a header, got ${JSON.stringify(e)}`);
102
- }
103
- var E = (e, t) => {
104
- let n = t.timestampSeconds ?? Math.floor(Date.now() / 1e3);
105
- if (e._tag === "standardWebhooks") return {
106
- headers: b({
107
- secrets: e.previousSecret === void 0 ? [t.secret] : [t.secret, e.previousSecret],
108
- messageId: t.messageId,
109
- timestampSeconds: n,
110
- rawBody: t.rawBody
111
- }),
112
- timestamp: n
113
- };
114
- if (e._tag === "custom") return { headers: { [e.header.toLowerCase()]: e.sign(t.rawBody, t.secret) } };
115
- T(e, "sign");
116
- let r = e.encoding ?? "hex", i = e.versionPrefix ?? "";
117
- if (e.includeTimestamp) {
118
- let a = Buffer.concat([Buffer.from(`${n}.`, "utf8"), Buffer.from(t.rawBody)]), o = w(e.algorithm, t.secret, a, r);
119
- return {
120
- headers: { [e.header.toLowerCase()]: `t=${n},${i}${o}` },
121
- timestamp: n
122
- };
123
- }
124
- let a = w(e.algorithm, t.secret, t.rawBody, r);
125
- return { headers: { [e.header.toLowerCase()]: `${i}${a}` } };
126
- }, D = (e, t) => {
127
- if (e._tag === "standardWebhooks") {
128
- let n = S({
129
- secrets: e.previousSecret === void 0 ? [t.secret] : [t.secret, e.previousSecret],
130
- rawBody: t.rawBody,
131
- headers: t.headers,
132
- toleranceSeconds: e.toleranceSeconds ?? 300,
133
- ...t.nowSeconds === void 0 ? {} : { nowSeconds: t.nowSeconds }
134
- });
135
- return n.ok ? { ok: !0 } : {
136
- ok: !1,
137
- reason: n.reason
138
- };
139
- }
140
- if (e._tag === "custom") return e.verify(t.rawBody, t.secret, t.headers) ? { ok: !0 } : {
141
- ok: !1,
142
- reason: "custom verifier rejected"
143
- };
144
- T(e, "verify");
145
- let n = t.headers[e.header.toLowerCase()];
146
- if (!n) return {
147
- ok: !1,
148
- reason: "missing signature header"
149
- };
150
- let r = e.encoding ?? "hex", i = e.versionPrefix ?? "", a = (a) => {
151
- if (e.includeTimestamp) {
152
- let o = n.split(",").map((e) => e.trim()), s = o.find((e) => e.startsWith("t="));
153
- if (!s) return {
154
- ok: !1,
155
- reason: "no timestamp in signature header"
156
- };
157
- let c = Number(s.slice(2));
158
- if (!Number.isFinite(c)) return {
159
- ok: !1,
160
- reason: "invalid timestamp"
161
- };
162
- let l = e.replayWindowSeconds ?? 300, u = t.nowSeconds ?? Math.floor(Date.now() / 1e3);
163
- if (Math.abs(u - c) > l) return {
164
- ok: !1,
165
- reason: `replay window exceeded (${Math.abs(u - c)}s > ${l}s)`
166
- };
167
- let d = i.length > 0 ? o.find((e) => e.startsWith(i))?.slice(i.length) : o.find((e) => !e.startsWith("t="));
168
- if (!d) return {
169
- ok: !1,
170
- reason: "no signature in header"
171
- };
172
- let f = Buffer.concat([Buffer.from(`${c}.`, "utf8"), Buffer.from(t.rawBody)]);
173
- return O(w(e.algorithm, a, f, r), d) ? { ok: !0 } : {
174
- ok: !1,
175
- reason: "signature mismatch"
176
- };
177
- }
178
- let o = i.length > 0 && n.startsWith(i) ? n.slice(i.length) : n;
179
- return O(w(e.algorithm, a, t.rawBody, r), o) ? { ok: !0 } : {
180
- ok: !1,
181
- reason: "signature mismatch"
182
- };
183
- }, o = a(t.secret);
184
- if (o.ok) return o;
185
- if (e.previousSecret !== void 0) {
186
- let t = a(e.previousSecret);
187
- if (t.ok) return t;
188
- }
189
- return o;
190
- }, O = (e, t) => {
191
- let r = Buffer.from(e, "utf8"), i = Buffer.from(t, "utf8");
192
- return r.length === i.length ? n(r, i) : (n(r, Buffer.alloc(r.length)), !1);
193
- }, k = (e = "Stripe-Signature") => ({
194
- _tag: "hmac",
195
- algorithm: "hmacSha256",
196
- header: e,
197
- includeTimestamp: !0,
198
- replayWindowSeconds: 300,
199
- encoding: "hex",
200
- versionPrefix: "v1="
201
- }), A = () => ({
202
- _tag: "hmac",
203
- algorithm: "hmacSha256",
204
- header: "X-Hub-Signature-256",
205
- includeTimestamp: !1,
206
- encoding: "hex",
207
- versionPrefix: "sha256="
208
- }), j = (e = {}) => ({
209
- _tag: "custom",
210
- header: "X-Slack-Signature",
211
- sign: (e, t) => `v0=${w("hmacSha256", t, `v0:${Math.floor(Date.now() / 1e3)}:${Buffer.from(e).toString("utf8")}`)}`,
212
- verify: (t, n, r) => {
213
- let i = r["x-slack-signature"], a = r["x-slack-request-timestamp"];
214
- if (!i || !a) return !1;
215
- let o = Number(a);
216
- if (!Number.isFinite(o)) return !1;
217
- let s = e.replayWindowSeconds ?? 300;
218
- return Math.abs(Math.floor(Date.now() / 1e3) - o) > s ? !1 : O(`v0=${w("hmacSha256", n, `v0:${o}:${Buffer.from(t).toString("utf8")}`)}`, i);
219
- }
220
- }), M = (e = {}) => ({
221
- _tag: "standardWebhooks",
222
- ...e.toleranceSeconds === void 0 ? {} : { toleranceSeconds: e.toleranceSeconds },
223
- ...e.previousSecret === void 0 ? {} : { previousSecret: e.previousSecret }
224
- }), N = () => M(), P = () => ({
225
- _tag: "hmac",
226
- algorithm: "hmacSha256",
227
- header: "X-Webhook-Signature",
228
- includeTimestamp: !0,
229
- replayWindowSeconds: 300,
230
- encoding: "hex",
231
- versionPrefix: "v1="
232
- });
233
- //#endregion
234
- export { S as C, a as E, v as S, i as T, m as _, j as a, h as b, D as c, f as d, d as f, l as g, s as h, E as i, p as l, c as m, P as n, M as o, u as p, A as r, k as s, N as t, o as u, g as v, r as w, y as x, b as y };