@voltro/plugin-webhooks 0.24.0 → 0.26.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 +670 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +196 -9
- package/dist/index.js +368 -249
- package/dist/mixin.d.ts +48 -0
- package/dist/mixin.js +1 -0
- package/dist/providers/index.js +1 -1
- package/dist/{signing-BOUxHdAo.js → signing-DUG4JWwk.js} +29 -32
- package/package.json +7 -7
package/dist/mixin.d.ts
CHANGED
|
@@ -118,6 +118,30 @@ export declare const _voltroWebhookTargetsTable: Table<"_voltro_webhook_targets"
|
|
|
118
118
|
readonly signing: ColumnBuilder<unknown, "json", boolean>;
|
|
119
119
|
/** Serialised `RetryPolicy` — see `retry.ts`. */
|
|
120
120
|
readonly retry: ColumnBuilder<unknown, "json", boolean>;
|
|
121
|
+
/**
|
|
122
|
+
* The APP's own scoping dimension. Opaque to the framework.
|
|
123
|
+
*
|
|
124
|
+
* Stored and returned verbatim, never interpreted — the framework does not
|
|
125
|
+
* know what a team, a project or a workspace is, and does not need to. Reads
|
|
126
|
+
* filter on equality against the json you wrote:
|
|
127
|
+
*
|
|
128
|
+
* subscribe({ event, url, scope: { teamId: 'q970…' } })
|
|
129
|
+
* listTargets({ scope: { teamId } })
|
|
130
|
+
*
|
|
131
|
+
* It exists because `.with(tenant())` is one level too coarse for real
|
|
132
|
+
* deployments. A consumer's endpoints are scoped to a TEAM and a tenant has
|
|
133
|
+
* many teams; every read filters by it and every write guards on it, so
|
|
134
|
+
* without this column the plugin cannot hold their rows at all and they keep
|
|
135
|
+
* a parallel table.
|
|
136
|
+
*
|
|
137
|
+
* The precedent is `_voltro_presence.meta`, and it is worth stating because
|
|
138
|
+
* it decided a migration: that column is json the framework stores and never
|
|
139
|
+
* interprets, and it is the ONLY reason the same consumer's presence
|
|
140
|
+
* migration was lossless — their three denormalised columns went straight in.
|
|
141
|
+
* The general form: a plugin that stores rows in an app's database on the
|
|
142
|
+
* app's behalf needs one place for the app's own dimension.
|
|
143
|
+
*/
|
|
144
|
+
readonly scope: ColumnBuilder<unknown, "json", boolean>;
|
|
121
145
|
/** Optional predicate filter (subset of `Predicate`) — the engine
|
|
122
146
|
* evaluates this against each emit's payload to decide whether
|
|
123
147
|
* this target receives the delivery. */
|
|
@@ -206,6 +230,30 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
|
|
|
206
230
|
readonly signing: ColumnBuilder<unknown, "json", boolean>;
|
|
207
231
|
/** Serialised `RetryPolicy` — see `retry.ts`. */
|
|
208
232
|
readonly retry: ColumnBuilder<unknown, "json", boolean>;
|
|
233
|
+
/**
|
|
234
|
+
* The APP's own scoping dimension. Opaque to the framework.
|
|
235
|
+
*
|
|
236
|
+
* Stored and returned verbatim, never interpreted — the framework does not
|
|
237
|
+
* know what a team, a project or a workspace is, and does not need to. Reads
|
|
238
|
+
* filter on equality against the json you wrote:
|
|
239
|
+
*
|
|
240
|
+
* subscribe({ event, url, scope: { teamId: 'q970…' } })
|
|
241
|
+
* listTargets({ scope: { teamId } })
|
|
242
|
+
*
|
|
243
|
+
* It exists because `.with(tenant())` is one level too coarse for real
|
|
244
|
+
* deployments. A consumer's endpoints are scoped to a TEAM and a tenant has
|
|
245
|
+
* many teams; every read filters by it and every write guards on it, so
|
|
246
|
+
* without this column the plugin cannot hold their rows at all and they keep
|
|
247
|
+
* a parallel table.
|
|
248
|
+
*
|
|
249
|
+
* The precedent is `_voltro_presence.meta`, and it is worth stating because
|
|
250
|
+
* it decided a migration: that column is json the framework stores and never
|
|
251
|
+
* interprets, and it is the ONLY reason the same consumer's presence
|
|
252
|
+
* migration was lossless — their three denormalised columns went straight in.
|
|
253
|
+
* The general form: a plugin that stores rows in an app's database on the
|
|
254
|
+
* app's behalf needs one place for the app's own dimension.
|
|
255
|
+
*/
|
|
256
|
+
readonly scope: ColumnBuilder<unknown, "json", boolean>;
|
|
209
257
|
/** Optional predicate filter (subset of `Predicate`) — the engine
|
|
210
258
|
* evaluates this against each emit's payload to decide whether
|
|
211
259
|
* this target receives the delivery. */
|
package/dist/mixin.js
CHANGED
package/dist/providers/index.js
CHANGED
|
@@ -1,41 +1,38 @@
|
|
|
1
1
|
import { createHmac as e, timingSafeEqual as t } from "node:crypto";
|
|
2
2
|
//#region src/dsl.ts
|
|
3
3
|
var n = (e) => ({
|
|
4
|
-
_tag: "outgoingEvent",
|
|
5
|
-
...e
|
|
6
|
-
}), r = (e) => ({
|
|
7
4
|
_tag: "incomingWebhook",
|
|
8
5
|
...e
|
|
9
|
-
}),
|
|
6
|
+
}), r = (e) => ({
|
|
10
7
|
_tag: "webhookProvider",
|
|
11
8
|
...e
|
|
12
|
-
}),
|
|
9
|
+
}), i = (e) => {
|
|
13
10
|
if (typeof e != "object" || !e) return !1;
|
|
14
11
|
let t = e._tag;
|
|
15
12
|
return t === "outgoingEvent" || t === "incomingWebhook" || t === "webhookProvider";
|
|
16
|
-
},
|
|
17
|
-
let
|
|
18
|
-
return
|
|
19
|
-
},
|
|
13
|
+
}, a = (e) => e === "hmacSha256" ? "sha256" : "sha1", o = (t, n, r, i = "hex") => {
|
|
14
|
+
let o = e(a(t), n);
|
|
15
|
+
return o.update(r), o.digest(i);
|
|
16
|
+
}, s = (e, t, n) => {
|
|
20
17
|
if (e._tag === "custom") return {
|
|
21
18
|
headerName: e.header,
|
|
22
19
|
headerValue: e.sign(t, n)
|
|
23
20
|
};
|
|
24
21
|
let r = e.encoding ?? "hex", i = e.versionPrefix ?? "";
|
|
25
22
|
if (e.includeTimestamp) {
|
|
26
|
-
let a = Math.floor(Date.now() / 1e3),
|
|
23
|
+
let a = Math.floor(Date.now() / 1e3), s = Buffer.concat([Buffer.from(`${a}.`, "utf8"), Buffer.from(t)]), c = o(e.algorithm, n, s, r);
|
|
27
24
|
return {
|
|
28
25
|
headerName: e.header,
|
|
29
26
|
headerValue: `t=${a},${i}${c}`,
|
|
30
27
|
timestamp: a
|
|
31
28
|
};
|
|
32
29
|
}
|
|
33
|
-
let a =
|
|
30
|
+
let a = o(e.algorithm, n, t, r);
|
|
34
31
|
return {
|
|
35
32
|
headerName: e.header,
|
|
36
33
|
headerValue: `${i}${a}`
|
|
37
34
|
};
|
|
38
|
-
},
|
|
35
|
+
}, c = (e, t, n, r) => {
|
|
39
36
|
if (!r) return {
|
|
40
37
|
ok: !1,
|
|
41
38
|
reason: "missing signature header"
|
|
@@ -44,50 +41,50 @@ var n = (e) => ({
|
|
|
44
41
|
ok: !1,
|
|
45
42
|
reason: "custom verifier rejected"
|
|
46
43
|
};
|
|
47
|
-
let i = e.encoding ?? "hex", a = e.versionPrefix ?? "",
|
|
44
|
+
let i = e.encoding ?? "hex", a = e.versionPrefix ?? "", s = (n) => {
|
|
48
45
|
if (e.includeTimestamp) {
|
|
49
|
-
let
|
|
46
|
+
let s = r.split(",").map((e) => e.trim()), c = s.find((e) => e.startsWith("t="));
|
|
50
47
|
if (!c) return {
|
|
51
48
|
ok: !1,
|
|
52
49
|
reason: "no timestamp in signature header"
|
|
53
50
|
};
|
|
54
|
-
let
|
|
55
|
-
if (!Number.isFinite(
|
|
51
|
+
let u = Number(c.slice(2));
|
|
52
|
+
if (!Number.isFinite(u)) return {
|
|
56
53
|
ok: !1,
|
|
57
54
|
reason: "invalid timestamp"
|
|
58
55
|
};
|
|
59
56
|
let d = e.replayWindowSeconds ?? 300, f = Math.floor(Date.now() / 1e3);
|
|
60
|
-
if (Math.abs(f -
|
|
57
|
+
if (Math.abs(f - u) > d) return {
|
|
61
58
|
ok: !1,
|
|
62
|
-
reason: `replay window exceeded (${Math.abs(f -
|
|
59
|
+
reason: `replay window exceeded (${Math.abs(f - u)}s > ${d}s)`
|
|
63
60
|
};
|
|
64
|
-
let p = a.length > 0 ?
|
|
61
|
+
let p = a.length > 0 ? s.find((e) => e.startsWith(a))?.slice(a.length) : s.find((e) => !e.startsWith("t="));
|
|
65
62
|
if (!p) return {
|
|
66
63
|
ok: !1,
|
|
67
64
|
reason: "no signature in header"
|
|
68
65
|
};
|
|
69
|
-
let m = Buffer.concat([Buffer.from(`${
|
|
70
|
-
return
|
|
66
|
+
let m = Buffer.concat([Buffer.from(`${u}.`, "utf8"), Buffer.from(t)]);
|
|
67
|
+
return l(o(e.algorithm, n, m, i), p) ? { ok: !0 } : {
|
|
71
68
|
ok: !1,
|
|
72
69
|
reason: "signature mismatch"
|
|
73
70
|
};
|
|
74
71
|
}
|
|
75
|
-
let
|
|
76
|
-
return
|
|
72
|
+
let s = a.length > 0 && r.startsWith(a) ? r.slice(a.length) : r;
|
|
73
|
+
return l(o(e.algorithm, n, t, i), s) ? { ok: !0 } : {
|
|
77
74
|
ok: !1,
|
|
78
75
|
reason: "signature mismatch"
|
|
79
76
|
};
|
|
80
|
-
}, c =
|
|
77
|
+
}, c = s(n);
|
|
81
78
|
if (c.ok) return c;
|
|
82
79
|
if (e.previousSecret !== void 0) {
|
|
83
|
-
let t =
|
|
80
|
+
let t = s(e.previousSecret);
|
|
84
81
|
if (t.ok) return t;
|
|
85
82
|
}
|
|
86
83
|
return c;
|
|
87
|
-
},
|
|
84
|
+
}, l = (e, n) => {
|
|
88
85
|
let r = Buffer.from(e, "utf8"), i = Buffer.from(n, "utf8");
|
|
89
86
|
return r.length === i.length ? t(r, i) : (t(r, Buffer.alloc(r.length)), !1);
|
|
90
|
-
},
|
|
87
|
+
}, u = (e = "Stripe-Signature") => ({
|
|
91
88
|
_tag: "hmac",
|
|
92
89
|
algorithm: "hmacSha256",
|
|
93
90
|
header: e,
|
|
@@ -95,19 +92,19 @@ var n = (e) => ({
|
|
|
95
92
|
replayWindowSeconds: 300,
|
|
96
93
|
encoding: "hex",
|
|
97
94
|
versionPrefix: "v1="
|
|
98
|
-
}),
|
|
95
|
+
}), d = () => ({
|
|
99
96
|
_tag: "hmac",
|
|
100
97
|
algorithm: "hmacSha256",
|
|
101
98
|
header: "X-Hub-Signature-256",
|
|
102
99
|
includeTimestamp: !1,
|
|
103
100
|
encoding: "hex",
|
|
104
101
|
versionPrefix: "sha256="
|
|
105
|
-
}),
|
|
102
|
+
}), f = () => ({
|
|
106
103
|
_tag: "custom",
|
|
107
104
|
header: "X-Slack-Signature",
|
|
108
|
-
sign: (e, t) => `v0=${
|
|
105
|
+
sign: (e, t) => `v0=${o("hmacSha256", t, `v0:${Math.floor(Date.now() / 1e3)}:${Buffer.from(e).toString("utf8")}`)}`,
|
|
109
106
|
verify: (e, t, n) => !1
|
|
110
|
-
}),
|
|
107
|
+
}), p = () => ({
|
|
111
108
|
_tag: "hmac",
|
|
112
109
|
algorithm: "hmacSha256",
|
|
113
110
|
header: "X-Webhook-Signature",
|
|
@@ -117,4 +114,4 @@ var n = (e) => ({
|
|
|
117
114
|
versionPrefix: "v1="
|
|
118
115
|
});
|
|
119
116
|
//#endregion
|
|
120
|
-
export {
|
|
117
|
+
export { u as a, r as c, f as i, i as l, d as n, c as o, s as r, n as s, p as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-webhooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@effect/workflow": "^0.19.0",
|
|
51
|
-
"@voltro/database": "0.
|
|
52
|
-
"@voltro/integration-http": "0.
|
|
53
|
-
"@voltro/logger": "0.
|
|
54
|
-
"@voltro/plugin-multitenancy": "0.
|
|
55
|
-
"@voltro/protocol": "0.
|
|
56
|
-
"@voltro/runtime": "0.
|
|
51
|
+
"@voltro/database": "0.26.0",
|
|
52
|
+
"@voltro/integration-http": "0.26.0",
|
|
53
|
+
"@voltro/logger": "0.26.0",
|
|
54
|
+
"@voltro/plugin-multitenancy": "0.26.0",
|
|
55
|
+
"@voltro/protocol": "0.26.0",
|
|
56
|
+
"@voltro/runtime": "0.26.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"effect": "^3.22.0"
|