@voltro/plugin-webhooks 0.84.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.
- package/CHANGELOG.md +1215 -0
- package/THIRD-PARTY-NOTICES.md +118 -600
- package/dist/autoDisable.js +41 -0
- package/dist/deferredEmit.js +17 -0
- package/dist/deliverWorkflow.js +336 -0
- package/dist/dsl.js +14 -0
- package/dist/encode.js +53 -0
- package/dist/errors.d.ts +23 -33
- package/dist/errors.js +10 -2
- package/dist/eventAudience.js +14 -0
- package/dist/idempotency.js +49 -0
- package/dist/incoming.js +205 -0
- package/dist/index.d.ts +45 -49
- package/dist/index.js +17 -1284
- package/dist/plugin.js +19 -0
- package/dist/providers/generic.js +19 -0
- package/dist/providers/github.js +16 -0
- package/dist/providers/index.js +5 -86
- package/dist/providers/slack.js +39 -0
- package/dist/providers/stripe.js +30 -0
- package/dist/rateLimit.js +61 -0
- package/dist/retry.js +65 -0
- package/dist/service.js +491 -0
- package/dist/signing.js +145 -0
- package/dist/standardWebhooks.js +88 -0
- package/package.json +8 -9
- package/dist/signing-DeHObNv6.js +0 -234
package/dist/service.js
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { asOutgoingEvent as e } from "./eventAudience.js";
|
|
2
|
+
import { generateStandardWebhooksSecret as t, isStandardWebhooksSecret as n } from "./standardWebhooks.js";
|
|
3
|
+
import { defaultOutgoingSignature as r } from "./signing.js";
|
|
4
|
+
import { defaultRetryPolicy as i } from "./retry.js";
|
|
5
|
+
import { WebhookDeliveryNotFound as a, WebhookPayloadInvalid as o, WebhookPayloadVersionInvalid as s, WebhookSubscribeInvalid as c, WebhookTargetNotFound as l } from "./errors.js";
|
|
6
|
+
import { Context as u, Effect as d, Result as f, Schema as p } from "effect";
|
|
7
|
+
import { randomBytes as m, randomUUID as h } from "node:crypto";
|
|
8
|
+
import { and as g, eq as _ } from "@voltro/database";
|
|
9
|
+
import { assertPublicUrl as v } from "@voltro/integration-http";
|
|
10
|
+
//#region src/service.ts
|
|
11
|
+
var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32, x = 32, S = () => m(b).toString("hex"), C = (e) => e.events ?? (e.event === void 0 ? [] : [e.event]), w = (e, t) => {
|
|
12
|
+
if (e !== void 0 && t?._tag === "standardWebhooks" && !n(e)) throw new c({
|
|
13
|
+
field: "secret",
|
|
14
|
+
reason: "a standard-webhooks target needs the spec's secret serialization (\"whsec_\" + base64 of 24..64 random bytes) — mint one with generateStandardWebhooksSecret(), or omit `secret` and let subscribe mint it. If the incompatible key is inherited, rotate the endpoint secret and update the receiver before adding the event"
|
|
15
|
+
});
|
|
16
|
+
}, T = (e) => {
|
|
17
|
+
let t;
|
|
18
|
+
try {
|
|
19
|
+
t = new URL(e.url);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
throw new c({
|
|
22
|
+
field: "url",
|
|
23
|
+
reason: `invalid subscriber url: ${e.message}`
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (t.protocol !== "http:" && t.protocol !== "https:") throw new c({
|
|
27
|
+
field: "url",
|
|
28
|
+
reason: `url protocol must be http or https, got ${t.protocol}`
|
|
29
|
+
});
|
|
30
|
+
if (process.env.NODE_ENV === "production") try {
|
|
31
|
+
v(e.url);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
throw new c({
|
|
34
|
+
field: "url",
|
|
35
|
+
reason: e.message
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
let n = C(e);
|
|
39
|
+
if (n.length === 0) throw new c({
|
|
40
|
+
field: "event",
|
|
41
|
+
reason: "event id required"
|
|
42
|
+
});
|
|
43
|
+
if (e.event !== void 0 && e.events !== void 0) throw new c({
|
|
44
|
+
field: "event",
|
|
45
|
+
reason: "pass `event` OR `events`, not both — the row set would be ambiguous"
|
|
46
|
+
});
|
|
47
|
+
if (n.some((e) => e.length === 0)) throw new c({
|
|
48
|
+
field: "events",
|
|
49
|
+
reason: "event id required"
|
|
50
|
+
});
|
|
51
|
+
if (new Set(n).size !== n.length) throw new c({
|
|
52
|
+
field: "events",
|
|
53
|
+
reason: "duplicate event — one row per event, so a repeat would deliver twice"
|
|
54
|
+
});
|
|
55
|
+
if (e.secret !== void 0 && e.secret.length < x) throw new c({
|
|
56
|
+
field: "secret",
|
|
57
|
+
reason: `secret must be at least ${x} chars`
|
|
58
|
+
});
|
|
59
|
+
if (w(e.secret, e.signing), e.retry !== void 0) {
|
|
60
|
+
if (e.retry.maxAttempts < 1) throw new c({
|
|
61
|
+
field: "retry",
|
|
62
|
+
reason: "retry.maxAttempts must be ≥ 1"
|
|
63
|
+
});
|
|
64
|
+
if (e.retry.maxAttempts > 1e3) throw new c({
|
|
65
|
+
field: "retry",
|
|
66
|
+
reason: "retry.maxAttempts must be ≤ 1000"
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (e.rateLimitPerMinute !== void 0 && e.rateLimitPerMinute < 1) throw new c({
|
|
70
|
+
field: "rateLimitPerMinute",
|
|
71
|
+
reason: "rateLimitPerMinute must be ≥ 1"
|
|
72
|
+
});
|
|
73
|
+
if (e.autoDisableAfter !== void 0 && (e.autoDisableAfter < 1 || !Number.isInteger(e.autoDisableAfter))) throw new c({
|
|
74
|
+
field: "autoDisableAfter",
|
|
75
|
+
reason: `autoDisableAfter must be a positive integer, got ${e.autoDisableAfter}`
|
|
76
|
+
});
|
|
77
|
+
}, E = (e, n, a, o) => {
|
|
78
|
+
let s = e.signing ?? n?.defaultSigning ?? r(), c = s._tag === "standardWebhooks" ? t : S, l = o ?? e.secret ?? c();
|
|
79
|
+
return w(l, s), {
|
|
80
|
+
id: h(),
|
|
81
|
+
tenantId: e.tenantId ?? null,
|
|
82
|
+
event: a ?? e.event ?? "",
|
|
83
|
+
url: e.url,
|
|
84
|
+
secret: l,
|
|
85
|
+
signing: s,
|
|
86
|
+
retry: e.retry ?? n?.defaultRetry ?? i(),
|
|
87
|
+
filter: e.filter ?? null,
|
|
88
|
+
scope: e.scope ?? null,
|
|
89
|
+
headers: e.headers ?? null,
|
|
90
|
+
rateLimitPerMinute: e.rateLimitPerMinute ?? null,
|
|
91
|
+
active: !0,
|
|
92
|
+
format: e.format ?? "json",
|
|
93
|
+
autoDisableAfter: e.autoDisableAfter ?? null,
|
|
94
|
+
consecutiveFailures: 0,
|
|
95
|
+
autoDisabledAt: null,
|
|
96
|
+
autoDisableReason: null,
|
|
97
|
+
payloadVersion: e.payloadVersion ?? n?.version ?? 1,
|
|
98
|
+
description: e.description ?? null
|
|
99
|
+
};
|
|
100
|
+
}, D = (e) => e instanceof Date ? e.getTime() : typeof e == "string" ? Date.parse(e) : 0, O = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : null, k = (e) => {
|
|
101
|
+
if (typeof e != "string") return e;
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(e);
|
|
104
|
+
} catch {
|
|
105
|
+
return e;
|
|
106
|
+
}
|
|
107
|
+
}, A = (e) => ({
|
|
108
|
+
id: String(e.id),
|
|
109
|
+
deliveryId: String(e.deliveryId),
|
|
110
|
+
targetId: String(e.targetId),
|
|
111
|
+
event: String(e.event),
|
|
112
|
+
eventId: e.eventId ?? null,
|
|
113
|
+
attempt: Number(e.attempt ?? 0),
|
|
114
|
+
status: e.status,
|
|
115
|
+
responseStatus: e.responseStatus ?? null,
|
|
116
|
+
errorMessage: e.errorMessage ?? null,
|
|
117
|
+
latencyMs: e.latencyMs ?? null,
|
|
118
|
+
scheduledAt: O(e.scheduledAt),
|
|
119
|
+
nextAttemptAt: O(e.nextAttemptAt),
|
|
120
|
+
createdAt: O(e.createdAt)
|
|
121
|
+
}), j = (e, t) => e === t ? "current" : e < t ? "behind" : "ahead", M = (e, t) => {
|
|
122
|
+
if (e === null) return !0;
|
|
123
|
+
let n = { payload: t };
|
|
124
|
+
for (let [t, r] of Object.entries(e)) if (!F(N(n, t), r)) return !1;
|
|
125
|
+
return !0;
|
|
126
|
+
}, N = (e, t) => {
|
|
127
|
+
let n = t.split("."), r = e;
|
|
128
|
+
for (let e of n) {
|
|
129
|
+
if (typeof r != "object" || !r) return;
|
|
130
|
+
r = r[e];
|
|
131
|
+
}
|
|
132
|
+
return r;
|
|
133
|
+
}, P = (e, t) => {
|
|
134
|
+
if (typeof e == "number" && typeof t == "number") return e - t;
|
|
135
|
+
if (typeof e == "string" && typeof t == "string") return e < t ? -1 : +(e > t);
|
|
136
|
+
}, F = (e, t) => {
|
|
137
|
+
if (typeof t == "object" && t && !Array.isArray(t)) {
|
|
138
|
+
let n = t;
|
|
139
|
+
return "eq" in n && e !== n.eq || "in" in n && (!Array.isArray(n.in) || !n.in.includes(e)) || "gt" in n && !((P(e, n.gt) ?? 0) > 0) || "gte" in n && !((P(e, n.gte) ?? -1) >= 0) || "lt" in n && !((P(e, n.lt) ?? 0) < 0) || "lte" in n && !((P(e, n.lte) ?? 1) <= 0) ? !1 : [
|
|
140
|
+
"eq",
|
|
141
|
+
"in",
|
|
142
|
+
"gt",
|
|
143
|
+
"gte",
|
|
144
|
+
"lt",
|
|
145
|
+
"lte"
|
|
146
|
+
].some((e) => e in n);
|
|
147
|
+
}
|
|
148
|
+
return e === t;
|
|
149
|
+
}, I = (e, t) => e !== null && Object.entries(t).every(([t, n]) => e[t] === n), L = 500, R = 200, z = async (e, t) => {
|
|
150
|
+
let n = [];
|
|
151
|
+
for (let r = 0; r < R; r += 1) {
|
|
152
|
+
let i = await e.store.query({
|
|
153
|
+
table: "_voltro_webhook_targets",
|
|
154
|
+
predicate: t,
|
|
155
|
+
order: [{
|
|
156
|
+
column: "id",
|
|
157
|
+
direction: "asc"
|
|
158
|
+
}],
|
|
159
|
+
take: L,
|
|
160
|
+
skip: r === 0 ? void 0 : r * L,
|
|
161
|
+
projection: void 0
|
|
162
|
+
});
|
|
163
|
+
if (n.push(...i), i.length < L) return n;
|
|
164
|
+
}
|
|
165
|
+
throw Error(`webhooks: more than ${R * L} rows in \`_voltro_webhook_targets\` — refusing to answer a scope lookup from a partial scan. A truncated scan mints a fresh secret for a live endpoint (breaking its signatures) or reports that a real scope matches nothing.`);
|
|
166
|
+
}, B = async (e, t) => {
|
|
167
|
+
let n = (await z(e)).filter((e) => I(e.scope, t));
|
|
168
|
+
if (n.length === 0) return;
|
|
169
|
+
let r = new Set(n.map((e) => e.secret));
|
|
170
|
+
if (r.size > 1) throw Error(`webhooks.subscribe: the scope ${JSON.stringify(t)} matches ${n.length} targets with ${r.size} different secrets, so it is not one endpoint. Pass \`secret\` explicitly, or narrow the scope.`);
|
|
171
|
+
return [...r][0];
|
|
172
|
+
}, V = async (e, t) => {
|
|
173
|
+
try {
|
|
174
|
+
let n = await e.store.query({
|
|
175
|
+
table: "_voltro_webhook_event_stats",
|
|
176
|
+
predicate: _("event", t),
|
|
177
|
+
order: [],
|
|
178
|
+
take: 1,
|
|
179
|
+
skip: void 0,
|
|
180
|
+
projection: void 0
|
|
181
|
+
}), r = /* @__PURE__ */ new Date(), i = n[0];
|
|
182
|
+
if (i === void 0) {
|
|
183
|
+
await e.store.insert("_voltro_webhook_event_stats", {
|
|
184
|
+
event: t,
|
|
185
|
+
emitCount: 1,
|
|
186
|
+
lastEmitAt: r,
|
|
187
|
+
createdAt: r,
|
|
188
|
+
updatedAt: r
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
await e.store.update("_voltro_webhook_event_stats", i.id, {
|
|
193
|
+
emitCount: (i.emitCount ?? 0) + 1,
|
|
194
|
+
lastEmitAt: r,
|
|
195
|
+
updatedAt: r
|
|
196
|
+
});
|
|
197
|
+
} catch {}
|
|
198
|
+
}, H = (n, r, i = {}) => {
|
|
199
|
+
let c = new Map((i.events ?? []).map((e) => [e.id, e])), u = {
|
|
200
|
+
subscribe: async (e) => {
|
|
201
|
+
T(e);
|
|
202
|
+
let r = C(e), i = e.secret === void 0 && e.scope !== void 0 ? await B(n, e.scope) : void 0, a = e.secret ?? i ?? t(), o = r.map((t) => E(e, c.get(t), t, a));
|
|
203
|
+
for (let e of o) await n.store.insert("_voltro_webhook_targets", {
|
|
204
|
+
...e,
|
|
205
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
206
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
207
|
+
});
|
|
208
|
+
let s = o[0];
|
|
209
|
+
return {
|
|
210
|
+
id: s.id,
|
|
211
|
+
event: s.event,
|
|
212
|
+
url: s.url,
|
|
213
|
+
secret: s.secret,
|
|
214
|
+
signing: s.signing,
|
|
215
|
+
retry: s.retry,
|
|
216
|
+
...o.length > 1 ? { targets: o.map((e) => ({
|
|
217
|
+
id: e.id,
|
|
218
|
+
event: e.event
|
|
219
|
+
})) } : {}
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
emit: async (t, i, a) => {
|
|
223
|
+
let s = typeof t == "object" && t.kind === "event" ? e(t) : void 0, l = typeof t == "string" ? c.get(t) : s ?? t, u = typeof t == "string" ? t : t.id ?? t.name;
|
|
224
|
+
if (l !== void 0) {
|
|
225
|
+
let e = p.decodeUnknownResult(l.payload)(i, { errors: "all" });
|
|
226
|
+
if (f.isFailure(e)) throw new o({
|
|
227
|
+
event: u,
|
|
228
|
+
issues: e.failure.message
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
let d = a?.eventId ?? h(), m = a?.tenantId, v = await z(n, m == null ? _("event", u) : g(_("event", u), _("tenantId", m)));
|
|
232
|
+
await V(n, u);
|
|
233
|
+
let y = v.filter((e) => M(e.filter, i)), b = [], x = JSON.stringify(i), S = async (e, t) => {
|
|
234
|
+
let i = h();
|
|
235
|
+
e.active ? (await r({
|
|
236
|
+
deliveryId: i,
|
|
237
|
+
targetId: e.id,
|
|
238
|
+
event: u,
|
|
239
|
+
eventId: d,
|
|
240
|
+
payloadJson: x
|
|
241
|
+
}), b[t] = {
|
|
242
|
+
targetId: e.id,
|
|
243
|
+
deliveryId: i,
|
|
244
|
+
status: "dispatched"
|
|
245
|
+
}) : (await n.store.insert("_voltro_webhook_deliveries", {
|
|
246
|
+
id: `${i}:1`,
|
|
247
|
+
deliveryId: i,
|
|
248
|
+
targetId: e.id,
|
|
249
|
+
tenantId: e.tenantId ?? null,
|
|
250
|
+
event: u,
|
|
251
|
+
eventId: d,
|
|
252
|
+
attempt: 1,
|
|
253
|
+
status: "pending",
|
|
254
|
+
payload: x,
|
|
255
|
+
scheduledAt: /* @__PURE__ */ new Date(),
|
|
256
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
257
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
258
|
+
}), b[t] = {
|
|
259
|
+
targetId: e.id,
|
|
260
|
+
deliveryId: i,
|
|
261
|
+
status: "queued"
|
|
262
|
+
});
|
|
263
|
+
}, C = 0, w = !1, T;
|
|
264
|
+
if (await Promise.all(Array.from({ length: Math.min(4, y.length) }, async () => {
|
|
265
|
+
for (; !w && C < y.length;) {
|
|
266
|
+
let e = C++;
|
|
267
|
+
try {
|
|
268
|
+
await S(y[e], e);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
w || (w = !0, T = e);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
})), w) throw T;
|
|
274
|
+
return {
|
|
275
|
+
eventId: d,
|
|
276
|
+
deliveries: b
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
replay: async (e) => {
|
|
280
|
+
let t = (await n.store.query({
|
|
281
|
+
table: "_voltro_webhook_deliveries",
|
|
282
|
+
predicate: _("deliveryId", e),
|
|
283
|
+
order: [{
|
|
284
|
+
column: "attempt",
|
|
285
|
+
direction: "asc"
|
|
286
|
+
}],
|
|
287
|
+
take: 1,
|
|
288
|
+
skip: void 0,
|
|
289
|
+
projection: void 0
|
|
290
|
+
}))[0];
|
|
291
|
+
if (t === void 0) throw new a({ deliveryId: e });
|
|
292
|
+
await r({
|
|
293
|
+
deliveryId: t.deliveryId,
|
|
294
|
+
targetId: t.targetId,
|
|
295
|
+
event: t.event,
|
|
296
|
+
eventId: t.eventId ?? h(),
|
|
297
|
+
payloadJson: typeof t.payload == "string" ? t.payload : JSON.stringify(t.payload),
|
|
298
|
+
attemptEpoch: Date.now()
|
|
299
|
+
});
|
|
300
|
+
},
|
|
301
|
+
resolveTargets: async (e) => {
|
|
302
|
+
if (typeof e == "string") {
|
|
303
|
+
if ((await n.store.query({
|
|
304
|
+
table: "_voltro_webhook_targets",
|
|
305
|
+
predicate: _("id", e),
|
|
306
|
+
order: [],
|
|
307
|
+
take: 1,
|
|
308
|
+
skip: void 0,
|
|
309
|
+
projection: ["id"]
|
|
310
|
+
})).length === 0) throw new l({ targetId: e });
|
|
311
|
+
return [e];
|
|
312
|
+
}
|
|
313
|
+
let t = (await z(n)).filter((t) => I(t.scope, e.scope)).map((e) => e.id);
|
|
314
|
+
if (t.length === 0) throw Error(`webhooks: no target matches scope ${JSON.stringify(e.scope)} — the operation would have silently done nothing`);
|
|
315
|
+
return t;
|
|
316
|
+
},
|
|
317
|
+
pauseTarget: async (e) => {
|
|
318
|
+
for (let t of await u.resolveTargets(e)) await n.store.update("_voltro_webhook_targets", t, {
|
|
319
|
+
active: !1,
|
|
320
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
resumeTarget: async (e) => {
|
|
324
|
+
for (let t of await u.resolveTargets(e)) {
|
|
325
|
+
await n.store.update("_voltro_webhook_targets", t, {
|
|
326
|
+
active: !0,
|
|
327
|
+
consecutiveFailures: 0,
|
|
328
|
+
autoDisabledAt: null,
|
|
329
|
+
autoDisableReason: null,
|
|
330
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
331
|
+
});
|
|
332
|
+
let e = await n.store.query({
|
|
333
|
+
table: "_voltro_webhook_deliveries",
|
|
334
|
+
predicate: g(_("targetId", t), _("status", "pending")),
|
|
335
|
+
order: [{
|
|
336
|
+
column: "createdAt",
|
|
337
|
+
direction: "asc"
|
|
338
|
+
}],
|
|
339
|
+
take: 1e4,
|
|
340
|
+
skip: void 0,
|
|
341
|
+
projection: void 0
|
|
342
|
+
}), i = Date.now();
|
|
343
|
+
for (let n of e) await r({
|
|
344
|
+
deliveryId: n.deliveryId,
|
|
345
|
+
targetId: t,
|
|
346
|
+
event: n.event,
|
|
347
|
+
eventId: n.eventId ?? h(),
|
|
348
|
+
payloadJson: typeof n.payload == "string" ? n.payload : JSON.stringify(n.payload),
|
|
349
|
+
attemptEpoch: i
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
deleteTarget: async (e) => {
|
|
354
|
+
for (let t of await u.resolveTargets(e)) await n.store.delete("_voltro_webhook_targets", t), await n.store.deleteMany("_voltro_webhook_deliveries", { where: g(_("targetId", t), _("status", "pending")) });
|
|
355
|
+
},
|
|
356
|
+
rotateSecret: async (e) => {
|
|
357
|
+
let r = t();
|
|
358
|
+
for (let t of await u.resolveTargets(e)) await n.store.update("_voltro_webhook_targets", t, {
|
|
359
|
+
secret: r,
|
|
360
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
361
|
+
});
|
|
362
|
+
return { secret: r };
|
|
363
|
+
},
|
|
364
|
+
updateTarget: async (e, t) => {
|
|
365
|
+
if (t.url !== void 0 && T({
|
|
366
|
+
event: "x",
|
|
367
|
+
url: t.url
|
|
368
|
+
}), t.rateLimitPerMinute !== void 0 && t.rateLimitPerMinute !== null && (!Number.isInteger(t.rateLimitPerMinute) || t.rateLimitPerMinute < 1)) throw Error("updateTarget: rateLimitPerMinute must be a positive integer or null");
|
|
369
|
+
let r = { updatedAt: /* @__PURE__ */ new Date() };
|
|
370
|
+
for (let e of [
|
|
371
|
+
"url",
|
|
372
|
+
"description",
|
|
373
|
+
"filter",
|
|
374
|
+
"scope",
|
|
375
|
+
"headers",
|
|
376
|
+
"rateLimitPerMinute",
|
|
377
|
+
"autoDisableAfter",
|
|
378
|
+
"format"
|
|
379
|
+
]) t[e] !== void 0 && (r[e] = t[e]);
|
|
380
|
+
for (let t of await u.resolveTargets(e)) await n.store.update("_voltro_webhook_targets", t, r);
|
|
381
|
+
},
|
|
382
|
+
testTarget: async (e, t) => {
|
|
383
|
+
let i = (await n.store.query({
|
|
384
|
+
table: "_voltro_webhook_targets",
|
|
385
|
+
predicate: _("id", e),
|
|
386
|
+
order: [],
|
|
387
|
+
take: 1,
|
|
388
|
+
skip: void 0,
|
|
389
|
+
projection: void 0
|
|
390
|
+
}))[0];
|
|
391
|
+
if (i === void 0) throw new l({ targetId: e });
|
|
392
|
+
let a = h(), o = JSON.stringify(t ?? {
|
|
393
|
+
test: !0,
|
|
394
|
+
targetId: e,
|
|
395
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
396
|
+
});
|
|
397
|
+
return i.active ? await r({
|
|
398
|
+
deliveryId: a,
|
|
399
|
+
targetId: i.id,
|
|
400
|
+
event: i.event,
|
|
401
|
+
eventId: h(),
|
|
402
|
+
payloadJson: o
|
|
403
|
+
}) : await n.store.insert("_voltro_webhook_deliveries", {
|
|
404
|
+
id: `${a}:1`,
|
|
405
|
+
deliveryId: a,
|
|
406
|
+
targetId: i.id,
|
|
407
|
+
tenantId: i.tenantId ?? null,
|
|
408
|
+
event: i.event,
|
|
409
|
+
eventId: null,
|
|
410
|
+
attempt: 1,
|
|
411
|
+
status: "pending",
|
|
412
|
+
payload: o,
|
|
413
|
+
scheduledAt: /* @__PURE__ */ new Date(),
|
|
414
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
415
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
416
|
+
}), { deliveryId: a };
|
|
417
|
+
},
|
|
418
|
+
listDeliveries: async (e) => {
|
|
419
|
+
let t = e?.scope === void 0 ? void 0 : new Set(await u.resolveTargets({ scope: e.scope })), r = await n.store.query({
|
|
420
|
+
table: "_voltro_webhook_deliveries",
|
|
421
|
+
predicate: e?.targetId === void 0 ? void 0 : _("targetId", e.targetId),
|
|
422
|
+
order: [{
|
|
423
|
+
column: "createdAt",
|
|
424
|
+
direction: "desc"
|
|
425
|
+
}],
|
|
426
|
+
take: Math.min((e?.limit ?? 200) * (e?.status !== void 0 || e?.since !== void 0 ? 10 : 1), 5e3),
|
|
427
|
+
skip: void 0,
|
|
428
|
+
projection: void 0
|
|
429
|
+
}), i = e?.since?.getTime();
|
|
430
|
+
return r.filter((e) => t === void 0 || t.has(String(e.targetId))).filter((t) => e?.status === void 0 || t.status === e.status).filter((e) => i === void 0 || D(e.createdAt) >= i).slice(0, e?.limit ?? 200).map(A);
|
|
431
|
+
},
|
|
432
|
+
getDelivery: async (e) => {
|
|
433
|
+
let t = (await n.store.query({
|
|
434
|
+
table: "_voltro_webhook_deliveries",
|
|
435
|
+
predicate: _("id", e),
|
|
436
|
+
order: [],
|
|
437
|
+
take: 1,
|
|
438
|
+
skip: void 0,
|
|
439
|
+
projection: void 0
|
|
440
|
+
}))[0];
|
|
441
|
+
return t === void 0 ? null : {
|
|
442
|
+
...A(t),
|
|
443
|
+
payload: k(t.payload),
|
|
444
|
+
responseBody: t.responseBody ?? null
|
|
445
|
+
};
|
|
446
|
+
},
|
|
447
|
+
updateTargetPayloadVersion: async (e, t) => {
|
|
448
|
+
if (t < 1 || !Number.isInteger(t)) throw new s({ version: t });
|
|
449
|
+
await u.resolveTargets(e), await n.store.update("_voltro_webhook_targets", e, {
|
|
450
|
+
payloadVersion: t,
|
|
451
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
452
|
+
});
|
|
453
|
+
},
|
|
454
|
+
listTargets: async (e, t) => {
|
|
455
|
+
let r = await n.store.query({
|
|
456
|
+
table: "_voltro_webhook_targets",
|
|
457
|
+
predicate: e === void 0 ? void 0 : _("event", e),
|
|
458
|
+
order: [{
|
|
459
|
+
column: "createdAt",
|
|
460
|
+
direction: "desc"
|
|
461
|
+
}],
|
|
462
|
+
take: 500,
|
|
463
|
+
skip: void 0,
|
|
464
|
+
projection: void 0
|
|
465
|
+
});
|
|
466
|
+
return (t === void 0 ? r : r.filter((e) => e.scope !== null && Object.entries(t).every(([t, n]) => e.scope?.[t] === n))).map((e) => ({
|
|
467
|
+
id: e.id,
|
|
468
|
+
event: e.event,
|
|
469
|
+
url: e.url,
|
|
470
|
+
active: e.active,
|
|
471
|
+
rateLimitPerMinute: e.rateLimitPerMinute,
|
|
472
|
+
description: e.description,
|
|
473
|
+
payloadVersion: e.payloadVersion,
|
|
474
|
+
autoDisableAfter: e.autoDisableAfter,
|
|
475
|
+
consecutiveFailures: e.consecutiveFailures ?? 0,
|
|
476
|
+
autoDisabledAt: e.autoDisabledAt === null || e.autoDisabledAt === void 0 ? null : e.autoDisabledAt instanceof Date ? e.autoDisabledAt.toISOString() : String(e.autoDisabledAt),
|
|
477
|
+
autoDisableReason: e.autoDisableReason,
|
|
478
|
+
scope: e.scope,
|
|
479
|
+
filter: e.filter ?? null
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
return u;
|
|
484
|
+
}, U = d.gen(function* () {
|
|
485
|
+
return yield* y;
|
|
486
|
+
}), W = (e) => {
|
|
487
|
+
if (e.webhooks === void 0) throw Error("useWebhooks: `ctx.webhooks` is not set. Declare an outgoing event with a `webhook:` block in a *.event.ts file, or an incoming endpoint in a *.webhook.tsx file — the service is wired the moment the app has either.");
|
|
488
|
+
return e.webhooks;
|
|
489
|
+
};
|
|
490
|
+
//#endregion
|
|
491
|
+
export { y as WebhooksService, H as buildWebhooksService, j as compareVersions, C as eventsOf, S as generateSecret, M as matchesFilter, E as resolveSubscribe, W as useWebhooks, U as useWebhooksEffect, T as validateSubscribe };
|
package/dist/signing.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { standardWebhooksHeaders as e, verifyStandardWebhooks as t } from "./standardWebhooks.js";
|
|
2
|
+
import { createHmac as n, timingSafeEqual as r } from "node:crypto";
|
|
3
|
+
//#region src/signing.ts
|
|
4
|
+
var i = (e) => e === "hmacSha256" ? "sha256" : "sha1", a = (e, t, r, a = "hex") => {
|
|
5
|
+
let o = n(i(e), t);
|
|
6
|
+
return o.update(r), o.digest(a);
|
|
7
|
+
};
|
|
8
|
+
function o(e, t) {
|
|
9
|
+
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)}`);
|
|
10
|
+
}
|
|
11
|
+
var s = (t, n) => {
|
|
12
|
+
let r = n.timestampSeconds ?? Math.floor(Date.now() / 1e3);
|
|
13
|
+
if (t._tag === "standardWebhooks") {
|
|
14
|
+
let i = t.previousSecret === void 0 ? [n.secret] : [n.secret, t.previousSecret];
|
|
15
|
+
return {
|
|
16
|
+
headers: e({
|
|
17
|
+
secrets: i,
|
|
18
|
+
messageId: n.messageId,
|
|
19
|
+
timestampSeconds: r,
|
|
20
|
+
rawBody: n.rawBody
|
|
21
|
+
}),
|
|
22
|
+
timestamp: r
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (t._tag === "custom") return { headers: { [t.header.toLowerCase()]: t.sign(n.rawBody, n.secret) } };
|
|
26
|
+
o(t, "sign");
|
|
27
|
+
let i = t.encoding ?? "hex", s = t.versionPrefix ?? "";
|
|
28
|
+
if (t.includeTimestamp) {
|
|
29
|
+
let e = Buffer.concat([Buffer.from(`${r}.`, "utf8"), Buffer.from(n.rawBody)]), o = a(t.algorithm, n.secret, e, i);
|
|
30
|
+
return {
|
|
31
|
+
headers: { [t.header.toLowerCase()]: `t=${r},${s}${o}` },
|
|
32
|
+
timestamp: r
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
let c = a(t.algorithm, n.secret, n.rawBody, i);
|
|
36
|
+
return { headers: { [t.header.toLowerCase()]: `${s}${c}` } };
|
|
37
|
+
}, c = (e, n) => {
|
|
38
|
+
if (e._tag === "standardWebhooks") {
|
|
39
|
+
let r = e.previousSecret === void 0 ? [n.secret] : [n.secret, e.previousSecret], i = t({
|
|
40
|
+
secrets: r,
|
|
41
|
+
rawBody: n.rawBody,
|
|
42
|
+
headers: n.headers,
|
|
43
|
+
toleranceSeconds: e.toleranceSeconds ?? 300,
|
|
44
|
+
...n.nowSeconds === void 0 ? {} : { nowSeconds: n.nowSeconds }
|
|
45
|
+
});
|
|
46
|
+
return i.ok ? { ok: !0 } : {
|
|
47
|
+
ok: !1,
|
|
48
|
+
reason: i.reason
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (e._tag === "custom") return e.verify(n.rawBody, n.secret, n.headers) ? { ok: !0 } : {
|
|
52
|
+
ok: !1,
|
|
53
|
+
reason: "custom verifier rejected"
|
|
54
|
+
};
|
|
55
|
+
o(e, "verify");
|
|
56
|
+
let r = n.headers[e.header.toLowerCase()];
|
|
57
|
+
if (!r) return {
|
|
58
|
+
ok: !1,
|
|
59
|
+
reason: "missing signature header"
|
|
60
|
+
};
|
|
61
|
+
let i = e.encoding ?? "hex", s = e.versionPrefix ?? "", c = (t) => {
|
|
62
|
+
if (e.includeTimestamp) {
|
|
63
|
+
let o = r.split(",").map((e) => e.trim()), c = o.find((e) => e.startsWith("t="));
|
|
64
|
+
if (!c) return {
|
|
65
|
+
ok: !1,
|
|
66
|
+
reason: "no timestamp in signature header"
|
|
67
|
+
};
|
|
68
|
+
let u = Number(c.slice(2));
|
|
69
|
+
if (!Number.isFinite(u)) return {
|
|
70
|
+
ok: !1,
|
|
71
|
+
reason: "invalid timestamp"
|
|
72
|
+
};
|
|
73
|
+
let d = e.replayWindowSeconds ?? 300, f = n.nowSeconds ?? Math.floor(Date.now() / 1e3);
|
|
74
|
+
if (Math.abs(f - u) > d) return {
|
|
75
|
+
ok: !1,
|
|
76
|
+
reason: `replay window exceeded (${Math.abs(f - u)}s > ${d}s)`
|
|
77
|
+
};
|
|
78
|
+
let p = s.length > 0 ? o.find((e) => e.startsWith(s))?.slice(s.length) : o.find((e) => !e.startsWith("t="));
|
|
79
|
+
if (!p) return {
|
|
80
|
+
ok: !1,
|
|
81
|
+
reason: "no signature in header"
|
|
82
|
+
};
|
|
83
|
+
let m = Buffer.concat([Buffer.from(`${u}.`, "utf8"), Buffer.from(n.rawBody)]);
|
|
84
|
+
return l(a(e.algorithm, t, m, i), p) ? { ok: !0 } : {
|
|
85
|
+
ok: !1,
|
|
86
|
+
reason: "signature mismatch"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
let o = s.length > 0 && r.startsWith(s) ? r.slice(s.length) : r;
|
|
90
|
+
return l(a(e.algorithm, t, n.rawBody, i), o) ? { ok: !0 } : {
|
|
91
|
+
ok: !1,
|
|
92
|
+
reason: "signature mismatch"
|
|
93
|
+
};
|
|
94
|
+
}, u = c(n.secret);
|
|
95
|
+
if (u.ok) return u;
|
|
96
|
+
if (e.previousSecret !== void 0) {
|
|
97
|
+
let t = c(e.previousSecret);
|
|
98
|
+
if (t.ok) return t;
|
|
99
|
+
}
|
|
100
|
+
return u;
|
|
101
|
+
}, l = (e, t) => {
|
|
102
|
+
let n = Buffer.from(e, "utf8"), i = Buffer.from(t, "utf8");
|
|
103
|
+
return n.length === i.length ? r(n, i) : (r(n, Buffer.alloc(n.length)), !1);
|
|
104
|
+
}, u = (e = "Stripe-Signature") => ({
|
|
105
|
+
_tag: "hmac",
|
|
106
|
+
algorithm: "hmacSha256",
|
|
107
|
+
header: e,
|
|
108
|
+
includeTimestamp: !0,
|
|
109
|
+
replayWindowSeconds: 300,
|
|
110
|
+
encoding: "hex",
|
|
111
|
+
versionPrefix: "v1="
|
|
112
|
+
}), d = () => ({
|
|
113
|
+
_tag: "hmac",
|
|
114
|
+
algorithm: "hmacSha256",
|
|
115
|
+
header: "X-Hub-Signature-256",
|
|
116
|
+
includeTimestamp: !1,
|
|
117
|
+
encoding: "hex",
|
|
118
|
+
versionPrefix: "sha256="
|
|
119
|
+
}), f = (e = {}) => ({
|
|
120
|
+
_tag: "custom",
|
|
121
|
+
header: "X-Slack-Signature",
|
|
122
|
+
sign: (e, t) => `v0=${a("hmacSha256", t, `v0:${Math.floor(Date.now() / 1e3)}:${Buffer.from(e).toString("utf8")}`)}`,
|
|
123
|
+
verify: (t, n, r) => {
|
|
124
|
+
let i = r["x-slack-signature"], o = r["x-slack-request-timestamp"];
|
|
125
|
+
if (!i || !o) return !1;
|
|
126
|
+
let s = Number(o);
|
|
127
|
+
if (!Number.isFinite(s)) return !1;
|
|
128
|
+
let c = e.replayWindowSeconds ?? 300;
|
|
129
|
+
return Math.abs(Math.floor(Date.now() / 1e3) - s) > c ? !1 : l(`v0=${a("hmacSha256", n, `v0:${s}:${Buffer.from(t).toString("utf8")}`)}`, i);
|
|
130
|
+
}
|
|
131
|
+
}), p = (e = {}) => ({
|
|
132
|
+
_tag: "standardWebhooks",
|
|
133
|
+
...e.toleranceSeconds === void 0 ? {} : { toleranceSeconds: e.toleranceSeconds },
|
|
134
|
+
...e.previousSecret === void 0 ? {} : { previousSecret: e.previousSecret }
|
|
135
|
+
}), m = () => p(), h = () => ({
|
|
136
|
+
_tag: "hmac",
|
|
137
|
+
algorithm: "hmacSha256",
|
|
138
|
+
header: "X-Webhook-Signature",
|
|
139
|
+
includeTimestamp: !0,
|
|
140
|
+
replayWindowSeconds: 300,
|
|
141
|
+
encoding: "hex",
|
|
142
|
+
versionPrefix: "v1="
|
|
143
|
+
});
|
|
144
|
+
//#endregion
|
|
145
|
+
export { m as defaultOutgoingSignature, h as genericHmacSignature, d as githubSignature, s as signRequest, f as slackSignature, p as standardWebhooksSignature, u as stripeSignature, c as verifyRequest };
|