@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.
@@ -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 };
@@ -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 };