@voltro/plugin-webhooks 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1009 @@
1
+ import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "./signing-BOUxHdAo.js";
2
+ import { WebhookDeliveryNotFound as u, WebhookPayloadInvalid as d, WebhookPayloadUnrepresentable as f, WebhookPayloadVersionInvalid as p, WebhookSubscribeInvalid as m } from "./errors.js";
3
+ import { webhookTables as h } from "./mixin.js";
4
+ import { randomBytes as g, randomUUID as _ } from "node:crypto";
5
+ import { Context as v, Effect as y, Either as b, Schema as x } from "effect";
6
+ import { and as S, eq as C } from "@voltro/database";
7
+ import { assertPublicUrl as w } from "@voltro/integration-http";
8
+ import { publishServerError as ee } from "@voltro/protocol";
9
+ import { createLogger as T } from "@voltro/logger";
10
+ import { Activity as E, DurableClock as D, Workflow as te } from "@effect/workflow";
11
+ //#region src/retry.ts
12
+ var ne = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/, O = (e) => {
13
+ let t = ne.exec(e);
14
+ if (!t) throw Error(`invalid duration literal: ${e}`);
15
+ let n = Number(t[1]), r = t[2] ?? "s";
16
+ switch (r) {
17
+ case "ms": return n;
18
+ case "s": return n * 1e3;
19
+ case "m": return n * 6e4;
20
+ case "h": return n * 36e5;
21
+ case "d": return n * 864e5;
22
+ default: throw Error(`unreachable unit: ${r}`);
23
+ }
24
+ }, k = [
25
+ 408,
26
+ 425,
27
+ 429,
28
+ 500,
29
+ 502,
30
+ 503,
31
+ 504
32
+ ], A = (e, t, n) => {
33
+ if (t >= e.maxAttempts || n?.status !== void 0 && !(e.retryOn ?? k).includes(n.status)) return null;
34
+ if (e.honourRetryAfter !== !1 && n?.retryAfterSeconds !== void 0) {
35
+ let t = n.retryAfterSeconds * 1e3, r = O(e.maxDelay), i = Math.min(t, r);
36
+ return {
37
+ delayMs: i,
38
+ baseDelayMs: i
39
+ };
40
+ }
41
+ let r = O(e.initialDelay), i = O(e.maxDelay), a;
42
+ switch (e.strategy) {
43
+ case "fixed":
44
+ a = r;
45
+ break;
46
+ case "linear":
47
+ a = r * t;
48
+ break;
49
+ case "exponential":
50
+ a = r * 2 ** (t - 1);
51
+ break;
52
+ }
53
+ a = Math.min(a, i);
54
+ let o = a;
55
+ return e.jitter === "full" && (o = Math.random() * a), {
56
+ delayMs: Math.max(1, Math.floor(o)),
57
+ baseDelayMs: a
58
+ };
59
+ }, j = () => ({
60
+ strategy: "exponential",
61
+ maxAttempts: 8,
62
+ initialDelay: "5s",
63
+ maxDelay: "1h",
64
+ retryOn: k,
65
+ honourRetryAfter: !0,
66
+ jitter: "full"
67
+ }), re = () => ({
68
+ strategy: "fixed",
69
+ maxAttempts: 120,
70
+ initialDelay: "30s",
71
+ maxDelay: "30s",
72
+ retryOn: k,
73
+ honourRetryAfter: !0,
74
+ jitter: "full"
75
+ }), M = class extends v.Tag("@voltro/webhooks/WebhooksService")() {}, ie = 32, N = 32, P = () => g(ie).toString("hex"), F = (e) => {
76
+ let t;
77
+ try {
78
+ t = new URL(e.url);
79
+ } catch (e) {
80
+ throw new m({
81
+ field: "url",
82
+ reason: `invalid subscriber url: ${e.message}`
83
+ });
84
+ }
85
+ if (t.protocol !== "http:" && t.protocol !== "https:") throw new m({
86
+ field: "url",
87
+ reason: `url protocol must be http or https, got ${t.protocol}`
88
+ });
89
+ if (process.env.NODE_ENV === "production") try {
90
+ w(e.url);
91
+ } catch (e) {
92
+ throw new m({
93
+ field: "url",
94
+ reason: e.message
95
+ });
96
+ }
97
+ if (e.event.length === 0) throw new m({
98
+ field: "event",
99
+ reason: "event id required"
100
+ });
101
+ if (e.secret !== void 0 && e.secret.length < N) throw new m({
102
+ field: "secret",
103
+ reason: `secret must be at least ${N} chars`
104
+ });
105
+ if (e.retry !== void 0) {
106
+ if (e.retry.maxAttempts < 1) throw new m({
107
+ field: "retry",
108
+ reason: "retry.maxAttempts must be ≥ 1"
109
+ });
110
+ if (e.retry.maxAttempts > 1e3) throw new m({
111
+ field: "retry",
112
+ reason: "retry.maxAttempts must be ≤ 1000"
113
+ });
114
+ }
115
+ if (e.rateLimitPerMinute !== void 0 && e.rateLimitPerMinute < 1) throw new m({
116
+ field: "rateLimitPerMinute",
117
+ reason: "rateLimitPerMinute must be ≥ 1"
118
+ });
119
+ if (e.autoDisableAfter !== void 0 && (e.autoDisableAfter < 1 || !Number.isInteger(e.autoDisableAfter))) throw new m({
120
+ field: "autoDisableAfter",
121
+ reason: `autoDisableAfter must be a positive integer, got ${e.autoDisableAfter}`
122
+ });
123
+ }, I = (e, t) => ({
124
+ id: _(),
125
+ event: e.event,
126
+ url: e.url,
127
+ secret: e.secret ?? P(),
128
+ signing: e.signing ?? t?.defaultSigning ?? c(),
129
+ retry: e.retry ?? t?.defaultRetry ?? j(),
130
+ filter: e.filter ?? null,
131
+ headers: e.headers ?? null,
132
+ rateLimitPerMinute: e.rateLimitPerMinute ?? null,
133
+ active: !0,
134
+ format: e.format ?? "json",
135
+ autoDisableAfter: e.autoDisableAfter ?? null,
136
+ consecutiveFailures: 0,
137
+ autoDisabledAt: null,
138
+ autoDisableReason: null,
139
+ payloadVersion: e.payloadVersion ?? t?.version ?? 1,
140
+ description: e.description ?? null
141
+ }), ae = (e, t) => e === t ? "current" : e < t ? "behind" : "ahead", L = (e, t) => {
142
+ if (e === null) return !0;
143
+ let n = { payload: t };
144
+ for (let [t, r] of Object.entries(e)) if (!se(oe(n, t), r)) return !1;
145
+ return !0;
146
+ }, oe = (e, t) => {
147
+ let n = t.split("."), r = e;
148
+ for (let e of n) {
149
+ if (typeof r != "object" || !r) return;
150
+ r = r[e];
151
+ }
152
+ return r;
153
+ }, se = (e, t) => {
154
+ if (typeof t == "object" && t && !Array.isArray(t)) {
155
+ let n = t;
156
+ if ("eq" in n) return e === n.eq;
157
+ if ("gt" in n) return typeof e == "number" && typeof n.gt == "number" && e > n.gt;
158
+ if ("gte" in n) return typeof e == "number" && typeof n.gte == "number" && e >= n.gte;
159
+ if ("lt" in n) return typeof e == "number" && typeof n.lt == "number" && e < n.lt;
160
+ if ("lte" in n) return typeof e == "number" && typeof n.lte == "number" && e <= n.lte;
161
+ if ("in" in n && Array.isArray(n.in)) return n.in.includes(e);
162
+ }
163
+ return e === t;
164
+ }, ce = (e, t, n = {}) => {
165
+ let r = new Map((n.events ?? []).map((e) => [e.id, e]));
166
+ return {
167
+ subscribe: async (t) => {
168
+ F(t);
169
+ let n = I(t, r.get(t.event));
170
+ return await e.store.insert("_voltro_webhook_targets", {
171
+ ...n,
172
+ createdAt: /* @__PURE__ */ new Date(),
173
+ updatedAt: /* @__PURE__ */ new Date()
174
+ }), {
175
+ id: n.id,
176
+ event: n.event,
177
+ url: n.url,
178
+ secret: n.secret,
179
+ signing: n.signing,
180
+ retry: n.retry
181
+ };
182
+ },
183
+ emit: async (n, i) => {
184
+ let a = typeof n == "string" ? r.get(n) : n, o = typeof n == "string" ? n : n.id;
185
+ if (a !== void 0) {
186
+ let e = x.decodeUnknownEither(a.payload)(i, { errors: "all" });
187
+ if (b.isLeft(e)) throw new d({
188
+ event: o,
189
+ issues: e.left.message
190
+ });
191
+ }
192
+ let s = _(), c = (await e.store.query({
193
+ table: "_voltro_webhook_targets",
194
+ predicate: C("event", o),
195
+ order: [{
196
+ column: "createdAt",
197
+ direction: "asc"
198
+ }],
199
+ take: 1e3,
200
+ skip: void 0,
201
+ projection: void 0
202
+ })).filter((e) => L(e.filter, i)), l = [], u = JSON.stringify(i);
203
+ for (let n of c) {
204
+ let r = _();
205
+ n.active ? (await t({
206
+ deliveryId: r,
207
+ targetId: n.id,
208
+ event: o,
209
+ eventId: s,
210
+ payloadJson: u
211
+ }), l.push({
212
+ targetId: n.id,
213
+ deliveryId: r,
214
+ status: "dispatched"
215
+ })) : (await e.store.insert("_voltro_webhook_deliveries", {
216
+ id: `${r}:1`,
217
+ deliveryId: r,
218
+ targetId: n.id,
219
+ tenantId: n.tenantId ?? null,
220
+ event: o,
221
+ eventId: s,
222
+ attempt: 1,
223
+ status: "pending",
224
+ payload: u,
225
+ scheduledAt: /* @__PURE__ */ new Date(),
226
+ createdAt: /* @__PURE__ */ new Date(),
227
+ updatedAt: /* @__PURE__ */ new Date()
228
+ }), l.push({
229
+ targetId: n.id,
230
+ deliveryId: r,
231
+ status: "queued"
232
+ }));
233
+ }
234
+ return {
235
+ eventId: s,
236
+ deliveries: l
237
+ };
238
+ },
239
+ replay: async (n) => {
240
+ let r = (await e.store.query({
241
+ table: "_voltro_webhook_deliveries",
242
+ predicate: C("deliveryId", n),
243
+ order: [{
244
+ column: "attempt",
245
+ direction: "asc"
246
+ }],
247
+ take: 1,
248
+ skip: void 0,
249
+ projection: void 0
250
+ }))[0];
251
+ if (r === void 0) throw new u({ deliveryId: n });
252
+ await t({
253
+ deliveryId: r.deliveryId,
254
+ targetId: r.targetId,
255
+ event: r.event,
256
+ eventId: r.eventId ?? _(),
257
+ payloadJson: typeof r.payload == "string" ? r.payload : JSON.stringify(r.payload),
258
+ attemptEpoch: Date.now()
259
+ });
260
+ },
261
+ pauseTarget: async (t) => {
262
+ await e.store.update("_voltro_webhook_targets", t, {
263
+ active: !1,
264
+ updatedAt: /* @__PURE__ */ new Date()
265
+ });
266
+ },
267
+ resumeTarget: async (n) => {
268
+ await e.store.update("_voltro_webhook_targets", n, {
269
+ active: !0,
270
+ consecutiveFailures: 0,
271
+ autoDisabledAt: null,
272
+ autoDisableReason: null,
273
+ updatedAt: /* @__PURE__ */ new Date()
274
+ });
275
+ let r = await e.store.query({
276
+ table: "_voltro_webhook_deliveries",
277
+ predicate: S(C("targetId", n), C("status", "pending")),
278
+ order: [{
279
+ column: "createdAt",
280
+ direction: "asc"
281
+ }],
282
+ take: 1e4,
283
+ skip: void 0,
284
+ projection: void 0
285
+ }), i = Date.now();
286
+ for (let e of r) await t({
287
+ deliveryId: e.deliveryId,
288
+ targetId: n,
289
+ event: e.event,
290
+ eventId: e.eventId ?? _(),
291
+ payloadJson: typeof e.payload == "string" ? e.payload : JSON.stringify(e.payload),
292
+ attemptEpoch: i
293
+ });
294
+ },
295
+ deleteTarget: async (t) => {
296
+ await e.store.delete("_voltro_webhook_targets", t), await e.store.deleteMany("_voltro_webhook_deliveries", { where: S(C("targetId", t), C("status", "pending")) });
297
+ },
298
+ rotateSecret: async (t) => {
299
+ let n = P();
300
+ return await e.store.update("_voltro_webhook_targets", t, {
301
+ secret: n,
302
+ updatedAt: /* @__PURE__ */ new Date()
303
+ }), { secret: n };
304
+ },
305
+ updateTargetPayloadVersion: async (t, n) => {
306
+ if (n < 1 || !Number.isInteger(n)) throw new p({ version: n });
307
+ await e.store.update("_voltro_webhook_targets", t, {
308
+ payloadVersion: n,
309
+ updatedAt: /* @__PURE__ */ new Date()
310
+ });
311
+ },
312
+ listTargets: async (t) => (await e.store.query({
313
+ table: "_voltro_webhook_targets",
314
+ predicate: t === void 0 ? void 0 : C("event", t),
315
+ order: [{
316
+ column: "createdAt",
317
+ direction: "desc"
318
+ }],
319
+ take: 500,
320
+ skip: void 0,
321
+ projection: void 0
322
+ })).map((e) => ({
323
+ id: e.id,
324
+ event: e.event,
325
+ url: e.url,
326
+ active: e.active,
327
+ rateLimitPerMinute: e.rateLimitPerMinute,
328
+ description: e.description,
329
+ payloadVersion: e.payloadVersion,
330
+ autoDisableAfter: e.autoDisableAfter,
331
+ consecutiveFailures: e.consecutiveFailures ?? 0,
332
+ autoDisabledAt: e.autoDisabledAt === null || e.autoDisabledAt === void 0 ? null : e.autoDisabledAt instanceof Date ? e.autoDisabledAt.toISOString() : String(e.autoDisabledAt),
333
+ autoDisableReason: e.autoDisableReason
334
+ }))
335
+ };
336
+ }, le = y.gen(function* () {
337
+ return yield* M;
338
+ }), ue = (e) => {
339
+ if (e.webhooks === void 0) throw Error("useWebhooks: `ctx.webhooks` is not set. Add @voltro/plugin-webhooks to your app.config.ts plugin list and declare at least one *.webhook.tsx file.");
340
+ return e.webhooks;
341
+ }, de = 1e4, R = class {
342
+ capacity;
343
+ cache = /* @__PURE__ */ new Map();
344
+ constructor(e = de) {
345
+ this.capacity = e;
346
+ }
347
+ evictExpired() {
348
+ let e = Date.now();
349
+ for (let [t, n] of this.cache) n.expiresAt < e && this.cache.delete(t);
350
+ }
351
+ claim(e, t) {
352
+ let n = this.cache.get(e);
353
+ if (n !== void 0 && n.expiresAt > Date.now()) return this.cache.delete(e), this.cache.set(e, n), n.value === "inflight" ? "inflight" : "duplicate";
354
+ if (this.cache.set(e, {
355
+ value: "inflight",
356
+ expiresAt: Date.now() + t
357
+ }), this.cache.size > this.capacity) {
358
+ let e = this.cache.keys().next().value;
359
+ e !== void 0 && this.cache.delete(e);
360
+ }
361
+ return "fresh";
362
+ }
363
+ commit(e) {
364
+ let t = this.cache.get(e);
365
+ t !== void 0 && this.cache.set(e, {
366
+ value: "processed",
367
+ expiresAt: t.expiresAt
368
+ });
369
+ }
370
+ release(e) {
371
+ this.cache.delete(e);
372
+ }
373
+ size() {
374
+ return this.evictExpired(), this.cache.size;
375
+ }
376
+ clear() {
377
+ this.cache.clear();
378
+ }
379
+ }, z = null, B = () => (z === null && (z = new R()), z), V = {
380
+ "5m": 5 * 6e4,
381
+ "1h": 60 * 6e4,
382
+ "6h": 360 * 6e4,
383
+ "1d": 1440 * 6e4,
384
+ "7d": 10080 * 6e4,
385
+ "30d": 720 * 60 * 6e4
386
+ }, H = (e) => e === void 0 ? V["7d"] : V[e] ?? V["7d"], U = (e, t) => ({
387
+ status: e,
388
+ contentType: "application/json; charset=utf-8",
389
+ body: JSON.stringify(t)
390
+ }), fe = (e, t) => {
391
+ let n = e.provider, r = e.signature ?? n?.signature ?? null, i = e.idempotency ?? n?.idempotency ?? null, o = e.bodyType ?? n?.bodyType ?? "json", s = t.idempotencyCache ?? B(), c = H(i?.ttl), l = t.log ?? ((e) => {
392
+ T({ scope: `webhook:${e.webhookId}` }).debug("incoming webhook", {
393
+ status: e.status,
394
+ signatureOk: e.signatureOk,
395
+ idempotency: e.idempotency,
396
+ durationMs: e.durationMs,
397
+ ...e.errorMessage === void 0 ? {} : { errorMessage: e.errorMessage }
398
+ });
399
+ });
400
+ return async (n) => {
401
+ let u = Date.now();
402
+ if (n.method !== "POST") {
403
+ let t = U(405, {
404
+ error: "method not allowed",
405
+ expected: "POST"
406
+ });
407
+ return l({
408
+ webhookId: e.id,
409
+ status: 405,
410
+ signatureOk: "skipped",
411
+ idempotency: "skipped",
412
+ durationMs: Date.now() - u
413
+ }), t;
414
+ }
415
+ let d = "skipped";
416
+ if (r !== null) {
417
+ let i = await t.resolveSecret(e.id);
418
+ if (i === null) d = "skipped";
419
+ else {
420
+ let t = n.headers[r.header.toLowerCase()], o = t;
421
+ r._tag === "custom" && n.headers["x-slack-request-timestamp"] && (o = `${n.headers["x-slack-request-timestamp"]}|${t ?? ""}`);
422
+ let s = a(r, n.rawBody, i, o);
423
+ if (d = s.ok, !s.ok) {
424
+ let t = U(401, {
425
+ error: "invalid signature",
426
+ reason: s.reason
427
+ });
428
+ return l({
429
+ webhookId: e.id,
430
+ status: 401,
431
+ signatureOk: !1,
432
+ idempotency: "skipped",
433
+ durationMs: Date.now() - u,
434
+ errorMessage: s.reason
435
+ }), t;
436
+ }
437
+ }
438
+ }
439
+ let f;
440
+ try {
441
+ if (o === "json") {
442
+ let e = new TextDecoder().decode(n.rawBody);
443
+ f = e.length === 0 ? null : JSON.parse(e);
444
+ } else if (o === "form") {
445
+ let e = new TextDecoder().decode(n.rawBody), t = new URLSearchParams(e), r = {};
446
+ for (let [e, n] of t.entries()) r[e] = n;
447
+ f = r;
448
+ } else f = n.rawBody;
449
+ } catch (t) {
450
+ let n = U(400, {
451
+ error: "malformed body",
452
+ reason: t.message
453
+ });
454
+ return l({
455
+ webhookId: e.id,
456
+ status: 400,
457
+ signatureOk: d,
458
+ idempotency: "skipped",
459
+ durationMs: Date.now() - u,
460
+ errorMessage: t.message
461
+ }), n;
462
+ }
463
+ let p;
464
+ try {
465
+ p = x.decodeUnknownSync(e.payload)(f);
466
+ } catch (t) {
467
+ let n = U(422, {
468
+ error: "schema validation failed",
469
+ reason: t.message
470
+ });
471
+ return l({
472
+ webhookId: e.id,
473
+ status: 422,
474
+ signatureOk: d,
475
+ idempotency: "skipped",
476
+ durationMs: Date.now() - u,
477
+ errorMessage: t.message
478
+ }), n;
479
+ }
480
+ let m, h = "skipped";
481
+ if (i !== null) {
482
+ let t = i.from;
483
+ if (m = typeof t == "string" ? n.headers[t.toLowerCase()] : t(n.headers, n.rawBody), m !== void 0 && m.length > 0) {
484
+ let t = s.claim(m, c);
485
+ if (h = t, t === "duplicate") {
486
+ let t = U(200, {
487
+ received: !0,
488
+ duplicate: !0
489
+ });
490
+ return l({
491
+ webhookId: e.id,
492
+ status: 200,
493
+ signatureOk: d,
494
+ idempotency: "duplicate",
495
+ durationMs: Date.now() - u
496
+ }), t;
497
+ }
498
+ if (t === "inflight") {
499
+ let t = U(409, {
500
+ error: "request already in flight",
501
+ idempotencyKey: m
502
+ });
503
+ return l({
504
+ webhookId: e.id,
505
+ status: 409,
506
+ signatureOk: d,
507
+ idempotency: "inflight",
508
+ durationMs: Date.now() - u
509
+ }), t;
510
+ }
511
+ }
512
+ }
513
+ try {
514
+ let r = t.resolveWorkflows?.();
515
+ await e.handler({
516
+ body: p,
517
+ rawBody: n.rawBody,
518
+ headers: n.headers,
519
+ idempotencyKey: m ?? "",
520
+ ...r ? { workflows: r } : {}
521
+ }), m !== void 0 && s.commit(m);
522
+ let i = U(200, { received: !0 });
523
+ return l({
524
+ webhookId: e.id,
525
+ status: 200,
526
+ signatureOk: d,
527
+ idempotency: h,
528
+ durationMs: Date.now() - u
529
+ }), i;
530
+ } catch (t) {
531
+ m !== void 0 && s.release(m);
532
+ let n = U(500, {
533
+ error: "handler threw",
534
+ reason: t.message
535
+ });
536
+ return l({
537
+ webhookId: e.id,
538
+ status: 500,
539
+ signatureOk: d,
540
+ idempotency: h,
541
+ durationMs: Date.now() - u,
542
+ errorMessage: t.message
543
+ }), ee({
544
+ error: t,
545
+ source: "webhook",
546
+ name: e.id,
547
+ fields: { status: 500 }
548
+ }), n;
549
+ }
550
+ };
551
+ }, pe = (e) => `/webhooks/${e}`, W = "_voltro_webhook_rate_windows", G = 6e4, me = (e, t) => `${e}@${t}`, K = async (e, t, n) => (await e.query({
552
+ table: "_voltro_webhook_rate_windows",
553
+ predicate: S(C("scope", t), C("bucket", n)),
554
+ order: [],
555
+ take: 1,
556
+ skip: void 0,
557
+ projection: void 0
558
+ }))[0] ?? null, q = 32, J = async (e, t, n) => {
559
+ for (let r = 0; r < q; r++) {
560
+ r > 0 && await new Promise((e) => setTimeout(e, r));
561
+ let i = await K(e, t.key, n);
562
+ if (i === null) {
563
+ await e.insertIgnore(W, {
564
+ id: me(t.key, n),
565
+ scope: t.key,
566
+ bucket: n,
567
+ count: 0,
568
+ createdAt: /* @__PURE__ */ new Date(),
569
+ updatedAt: /* @__PURE__ */ new Date()
570
+ }, { conflictColumns: ["id"] });
571
+ continue;
572
+ }
573
+ if (i.count >= t.limit) return !1;
574
+ if (await e.updateMany("_voltro_webhook_rate_windows", {
575
+ count: i.count + 1,
576
+ updatedAt: /* @__PURE__ */ new Date()
577
+ }, { where: S(C("id", i.id), C("count", i.count)) }) === 1) return !0;
578
+ }
579
+ throw Error(`webhook rate window: contention on (${t.key}, ${n}) exceeded ${q} attempts`);
580
+ }, Y = async (e, t, n) => {
581
+ for (let r = 0; r < q; r++) {
582
+ r > 0 && await new Promise((e) => setTimeout(e, r));
583
+ let i = await K(e, t, n);
584
+ if (i === null || i.count <= 0 || await e.updateMany("_voltro_webhook_rate_windows", {
585
+ count: i.count - 1,
586
+ updatedAt: /* @__PURE__ */ new Date()
587
+ }, { where: S(C("id", i.id), C("count", i.count)) }) === 1) return;
588
+ }
589
+ throw Error(`webhook rate window: release contention on (${t}) exceeded ${q} attempts`);
590
+ }, X = async (e, t, n, r = G) => {
591
+ if (t.length === 0) return {
592
+ acquired: !0,
593
+ retryInMs: 0
594
+ };
595
+ let i = Math.floor(n / r), a = [];
596
+ for (let o of t) {
597
+ if (await J(e, o, i)) {
598
+ a.push(o);
599
+ continue;
600
+ }
601
+ for (let t of a) await Y(e, t.key, i);
602
+ return {
603
+ acquired: !1,
604
+ retryInMs: (i + 1) * r - n
605
+ };
606
+ }
607
+ return {
608
+ acquired: !0,
609
+ retryInMs: 0
610
+ };
611
+ }, Z = (e, t) => {
612
+ if (e === "json") return {
613
+ contentType: "application/json",
614
+ bytes: Buffer.from(t, "utf8")
615
+ };
616
+ let n;
617
+ try {
618
+ n = JSON.parse(t);
619
+ } catch (t) {
620
+ throw new f({
621
+ format: e,
622
+ reason: `payload is not valid JSON: ${t.message}`
623
+ });
624
+ }
625
+ return e === "form" ? {
626
+ contentType: "application/x-www-form-urlencoded",
627
+ bytes: Buffer.from(he(n), "utf8")
628
+ } : {
629
+ contentType: "application/xml",
630
+ bytes: Buffer.from(_e(n), "utf8")
631
+ };
632
+ }, he = (e) => {
633
+ if (typeof e != "object" || !e || Array.isArray(e)) throw new f({
634
+ format: "form",
635
+ reason: `form encoding requires a JSON object at the top level, got ${be(e)}`
636
+ });
637
+ let t = new URLSearchParams();
638
+ for (let [n, r] of Object.entries(e)) Q(t, n, r);
639
+ return t.toString();
640
+ }, Q = (e, t, n) => {
641
+ if (n === null) {
642
+ e.append(t, "");
643
+ return;
644
+ }
645
+ if (Array.isArray(n)) {
646
+ n.forEach((n, r) => Q(e, `${t}[${r}]`, n));
647
+ return;
648
+ }
649
+ if (typeof n == "object") {
650
+ for (let [r, i] of Object.entries(n)) Q(e, `${t}[${r}]`, i);
651
+ return;
652
+ }
653
+ e.append(t, String(n));
654
+ }, ge = /^[A-Za-z_][A-Za-z0-9_.-]*$/, _e = (e) => `<?xml version="1.0" encoding="UTF-8"?>${$("webhook", e)}`, $ = (e, t) => {
655
+ if (!ge.test(e)) throw new f({
656
+ format: "xml",
657
+ reason: `object key ${JSON.stringify(e)} is not a valid XML element name`
658
+ });
659
+ return `<${e}>${ve(e, t)}</${e}>`;
660
+ }, ve = (e, t) => t == null ? "" : Array.isArray(t) ? t.map((e) => $("item", e)).join("") : typeof t == "object" ? Object.entries(t).map(([e, t]) => $(e, t)).join("") : ye(String(t)), ye = (e) => e.replace(/[&<>"']/g, (e) => {
661
+ switch (e) {
662
+ case "&": return "&amp;";
663
+ case "<": return "&lt;";
664
+ case ">": return "&gt;";
665
+ case "\"": return "&quot;";
666
+ default: return "&apos;";
667
+ }
668
+ }), be = (e) => e === null ? "null" : Array.isArray(e) ? "array" : typeof e, xe = "_voltro_webhook_targets", Se = 32, Ce = async (e, t) => (await e.query({
669
+ table: "_voltro_webhook_targets",
670
+ predicate: C("id", t),
671
+ order: [],
672
+ take: 1,
673
+ skip: void 0,
674
+ projection: void 0
675
+ }))[0] ?? null, we = async (e, t, n, r, i = /* @__PURE__ */ new Date()) => {
676
+ for (let a = 0; a < Se; a++) {
677
+ a > 0 && await new Promise((e) => setTimeout(e, a));
678
+ let o = await Ce(e, t);
679
+ if (o === null) return {
680
+ consecutiveFailures: 0,
681
+ autoDisabled: !1
682
+ };
683
+ let s = o.consecutiveFailures ?? 0;
684
+ if (n) {
685
+ if (s === 0 || await e.updateMany("_voltro_webhook_targets", {
686
+ consecutiveFailures: 0,
687
+ updatedAt: i
688
+ }, { where: S(C("id", t), C("consecutiveFailures", s)) }) === 1) return {
689
+ consecutiveFailures: 0,
690
+ autoDisabled: !1
691
+ };
692
+ continue;
693
+ }
694
+ let c = s + 1, l = o.autoDisableAfter, u = typeof l == "number" && l > 0 && o.active && c >= l, d = {
695
+ consecutiveFailures: c,
696
+ updatedAt: i
697
+ };
698
+ if (u && (d.active = !1, d.autoDisabledAt = i, d.autoDisableReason = r.slice(0, 500)), await e.updateMany("_voltro_webhook_targets", d, { where: S(C("id", t), C("consecutiveFailures", s)) }) === 1) return {
699
+ consecutiveFailures: c,
700
+ autoDisabled: u
701
+ };
702
+ }
703
+ throw Error(`webhook auto-disable: streak CAS contention on target ${t} exceeded ${Se} attempts`);
704
+ }, Te = x.Struct({
705
+ status: x.Number,
706
+ body: x.String,
707
+ latencyMs: x.Number,
708
+ retryAfterSec: x.Union(x.Number, x.Undefined),
709
+ transportError: x.Union(x.String, x.Undefined),
710
+ encodeError: x.Union(x.String, x.Undefined)
711
+ }), Ee = x.Struct({
712
+ outcome: x.Literal("succeeded", "failed", "retry"),
713
+ delayMs: x.Union(x.Number, x.Undefined),
714
+ reason: x.Union(x.String, x.Undefined),
715
+ autoDisabled: x.Union(x.Boolean, x.Undefined),
716
+ consecutiveFailures: x.Union(x.Number, x.Undefined)
717
+ }), De = x.Struct({
718
+ acquired: x.Boolean,
719
+ retryInMs: x.Number
720
+ }), Oe = te.make({
721
+ name: "voltro.deliverWebhook",
722
+ payload: {
723
+ deliveryId: x.String,
724
+ targetId: x.String,
725
+ event: x.String,
726
+ eventId: x.String,
727
+ payloadJson: x.String,
728
+ attemptEpoch: x.optionalWith(x.Number, { default: () => 0 })
729
+ },
730
+ success: x.Struct({
731
+ finalStatus: x.Literal("succeeded", "failed", "deferred"),
732
+ attempts: x.Number
733
+ }),
734
+ idempotencyKey: ({ deliveryId: e, attemptEpoch: t }) => `voltro.deliverWebhook:${e}:${t ?? 0}`
735
+ }), ke = (e, t = {}) => (n, r) => {
736
+ let { deliveryId: i, targetId: a, event: s, eventId: c, payloadJson: l } = n, u = T({ scope: `webhook:${i}` });
737
+ return y.gen(function* () {
738
+ let n = yield* E.make({
739
+ name: "fetch-target",
740
+ success: x.Union(x.Null, x.Struct({
741
+ id: x.String,
742
+ event: x.String,
743
+ url: x.String,
744
+ secret: x.String,
745
+ signing: x.Any,
746
+ retry: x.Any,
747
+ headers: x.Union(x.Record({
748
+ key: x.String,
749
+ value: x.String
750
+ }), x.Null),
751
+ rateLimitPerMinute: x.Union(x.Number, x.Null),
752
+ active: x.Boolean,
753
+ format: x.Literal("json", "form", "xml"),
754
+ tenantId: x.Union(x.String, x.Null)
755
+ })),
756
+ execute: y.tryPromise({
757
+ try: async () => (await e.store.query({
758
+ table: "_voltro_webhook_targets",
759
+ predicate: C("id", a),
760
+ order: [{
761
+ column: "createdAt",
762
+ direction: "asc"
763
+ }],
764
+ take: 1,
765
+ skip: void 0,
766
+ projection: void 0
767
+ }))[0] ?? null,
768
+ catch: (e) => (u.debug("fetch-target raw error", {
769
+ targetId: a,
770
+ event: s,
771
+ cause: String(e)
772
+ }), /* @__PURE__ */ Error(`fetch-target failed: ${e.message}`))
773
+ }).pipe(y.orDie)
774
+ });
775
+ if (n === null) return {
776
+ finalStatus: "failed",
777
+ attempts: 0
778
+ };
779
+ let r = async (t, r, o) => {
780
+ await e.store.update("_voltro_webhook_deliveries", t, {
781
+ ...o,
782
+ updatedAt: /* @__PURE__ */ new Date()
783
+ }) === null && await e.store.insert("_voltro_webhook_deliveries", {
784
+ id: t,
785
+ deliveryId: i,
786
+ targetId: a,
787
+ tenantId: n.tenantId ?? null,
788
+ event: s,
789
+ eventId: c,
790
+ attempt: r,
791
+ payload: l,
792
+ scheduledAt: /* @__PURE__ */ new Date(),
793
+ createdAt: /* @__PURE__ */ new Date(),
794
+ updatedAt: /* @__PURE__ */ new Date(),
795
+ ...o
796
+ });
797
+ };
798
+ if (n.active !== !0) return yield* E.make({
799
+ name: "record-paused-pending",
800
+ success: x.Void,
801
+ execute: y.tryPromise({
802
+ try: () => r(`${i}:1`, 1, { status: "pending" }),
803
+ catch: (e) => /* @__PURE__ */ Error(`record-paused-pending failed: ${e.message}`)
804
+ }).pipe(y.orDie)
805
+ }), {
806
+ finalStatus: "deferred",
807
+ attempts: 0
808
+ };
809
+ let d = (e) => typeof e == "string" ? JSON.parse(e) : e, p = d(n.retry), m = d(n.signing), h = n.headers === null ? null : d(n.headers), g = t.rateWindowMs ?? 6e4, _ = t.events?.find((e) => e.id === s)?.globalRateLimit, v = [...typeof n.rateLimitPerMinute == "number" && n.rateLimitPerMinute > 0 ? [{
810
+ key: `target:${n.id}`,
811
+ limit: n.rateLimitPerMinute
812
+ }] : [], ..._ !== void 0 && _.perMinute > 0 ? [{
813
+ key: `event:${s}`,
814
+ limit: _.perMinute
815
+ }] : []];
816
+ for (let t = 1; t <= p.maxAttempts; t++) {
817
+ let d = `${i}:${t}`;
818
+ if (v.length > 0) for (let n = 0;; n++) {
819
+ let i = yield* E.make({
820
+ name: `rate-acquire-${t}-${n}`,
821
+ success: De,
822
+ execute: y.tryPromise({
823
+ try: () => X(e.store, v, Date.now(), g),
824
+ catch: (e) => (u.debug("rate-acquire raw error", {
825
+ targetId: a,
826
+ event: s,
827
+ attempt: t,
828
+ cause: String(e)
829
+ }), /* @__PURE__ */ Error(`rate-acquire failed: ${e.message}`))
830
+ }).pipe(y.orDie)
831
+ });
832
+ if (i.acquired) break;
833
+ yield* E.make({
834
+ name: `record-rate-deferral-${t}-${n}`,
835
+ success: x.Void,
836
+ execute: y.tryPromise({
837
+ try: () => r(d, t, {
838
+ status: "pending",
839
+ nextAttemptAt: new Date(Date.now() + i.retryInMs)
840
+ }),
841
+ catch: (e) => /* @__PURE__ */ Error(`record-rate-deferral failed: ${e.message}`)
842
+ }).pipe(y.orDie)
843
+ }), yield* D.sleep({
844
+ name: `rate-window-sleep-${t}-${n}`,
845
+ duration: `${i.retryInMs} millis`
846
+ });
847
+ }
848
+ yield* E.make({
849
+ name: `record-attempt-start-${t}`,
850
+ success: x.Void,
851
+ execute: y.tryPromise({
852
+ try: () => r(d, t, {
853
+ status: "inFlight",
854
+ nextAttemptAt: null,
855
+ scheduledAt: /* @__PURE__ */ new Date()
856
+ }),
857
+ catch: (e) => /* @__PURE__ */ Error(`record-attempt-start failed: ${e.message}`)
858
+ }).pipe(y.orDie)
859
+ });
860
+ let _ = yield* E.make({
861
+ name: `sign-and-post-${t}`,
862
+ success: Te,
863
+ execute: y.promise(async () => {
864
+ let e = Date.now(), r;
865
+ try {
866
+ r = Z(n.format, l);
867
+ } catch (t) {
868
+ if (t instanceof f) return {
869
+ status: 0,
870
+ body: "",
871
+ latencyMs: Date.now() - e,
872
+ retryAfterSec: void 0,
873
+ transportError: void 0,
874
+ encodeError: t.reason
875
+ };
876
+ throw t;
877
+ }
878
+ let a = Buffer.from(r.bytes), { headerName: u, headerValue: d } = o(m, a, n.secret);
879
+ process.env.NODE_ENV === "production" && w(n.url);
880
+ try {
881
+ let o = await fetch(n.url, {
882
+ method: "POST",
883
+ headers: {
884
+ "content-type": r.contentType,
885
+ [u]: d,
886
+ "x-voltro-event": s,
887
+ "x-voltro-event-id": c,
888
+ "x-voltro-delivery-id": i,
889
+ "x-voltro-attempt": String(t),
890
+ "idempotency-key": i,
891
+ ...h ?? {}
892
+ },
893
+ body: a
894
+ }), l = (await o.text()).slice(0, 8192), f = o.headers.get("retry-after");
895
+ return {
896
+ status: o.status,
897
+ body: l,
898
+ latencyMs: Date.now() - e,
899
+ retryAfterSec: f !== null && /^\d+$/.test(f) ? Number(f) : void 0,
900
+ transportError: void 0,
901
+ encodeError: void 0
902
+ };
903
+ } catch (t) {
904
+ return {
905
+ status: 0,
906
+ body: "",
907
+ latencyMs: Date.now() - e,
908
+ retryAfterSec: void 0,
909
+ transportError: t.message,
910
+ encodeError: void 0
911
+ };
912
+ }
913
+ })
914
+ }), b = yield* E.make({
915
+ name: `classify-and-record-${t}`,
916
+ success: Ee,
917
+ execute: y.tryPromise({
918
+ try: async () => {
919
+ let r = _.status >= 200 && _.status < 300, i = _.transportError !== void 0, o = _.encodeError !== void 0, s = (t, n) => we(e.store, a, t, n);
920
+ if (r) {
921
+ await e.store.update("_voltro_webhook_deliveries", d, {
922
+ status: "succeeded",
923
+ responseStatus: _.status,
924
+ responseBody: _.body,
925
+ latencyMs: _.latencyMs,
926
+ updatedAt: /* @__PURE__ */ new Date()
927
+ });
928
+ let t = await s(!0, "");
929
+ return {
930
+ outcome: "succeeded",
931
+ delayMs: void 0,
932
+ reason: void 0,
933
+ autoDisabled: t.autoDisabled,
934
+ consecutiveFailures: t.consecutiveFailures
935
+ };
936
+ }
937
+ let c = o ? null : A(p, t, {
938
+ ...i ? {} : { status: _.status },
939
+ ..._.retryAfterSec === void 0 ? {} : { retryAfterSeconds: _.retryAfterSec }
940
+ });
941
+ if (c === null) {
942
+ let t = o ? `encode (${n.format}): ${_.encodeError}` : i ? `transport: ${_.transportError}` : `http ${_.status}: ${_.body.slice(0, 200)}`;
943
+ await e.store.update("_voltro_webhook_deliveries", d, {
944
+ status: "failed",
945
+ responseStatus: i || o ? null : _.status,
946
+ responseBody: i || o ? null : _.body,
947
+ errorMessage: o ? t : _.transportError ?? null,
948
+ latencyMs: _.latencyMs,
949
+ updatedAt: /* @__PURE__ */ new Date()
950
+ });
951
+ let r = await s(!1, t);
952
+ return {
953
+ outcome: "failed",
954
+ delayMs: void 0,
955
+ reason: t,
956
+ autoDisabled: r.autoDisabled,
957
+ consecutiveFailures: r.consecutiveFailures
958
+ };
959
+ }
960
+ return await e.store.update("_voltro_webhook_deliveries", d, {
961
+ status: "retryScheduled",
962
+ responseStatus: i ? null : _.status,
963
+ responseBody: i ? null : _.body,
964
+ errorMessage: _.transportError ?? null,
965
+ latencyMs: _.latencyMs,
966
+ nextAttemptAt: new Date(Date.now() + c.delayMs),
967
+ updatedAt: /* @__PURE__ */ new Date()
968
+ }), {
969
+ outcome: "retry",
970
+ delayMs: c.delayMs,
971
+ reason: void 0,
972
+ autoDisabled: void 0,
973
+ consecutiveFailures: void 0
974
+ };
975
+ },
976
+ catch: (e) => (u.debug("classify-and-record raw error", {
977
+ targetId: a,
978
+ event: s,
979
+ attempt: t,
980
+ cause: String(e)
981
+ }), /* @__PURE__ */ Error(`classify-and-record failed: ${e.message}`))
982
+ }).pipe(y.orDie)
983
+ });
984
+ if (b.autoDisabled === !0 && u.warn("target auto-disabled after consecutive failures", {
985
+ targetId: a,
986
+ event: s,
987
+ consecutiveFailures: b.consecutiveFailures ?? null,
988
+ reason: b.reason ?? null
989
+ }), b.outcome === "succeeded") return {
990
+ finalStatus: "succeeded",
991
+ attempts: t
992
+ };
993
+ if (b.outcome === "failed") return {
994
+ finalStatus: "failed",
995
+ attempts: t
996
+ };
997
+ b.delayMs !== void 0 && (yield* D.sleep({
998
+ name: `retry-sleep-${t}`,
999
+ duration: `${b.delayMs} millis`
1000
+ }));
1001
+ }
1002
+ return {
1003
+ finalStatus: "failed",
1004
+ attempts: p.maxAttempts
1005
+ };
1006
+ });
1007
+ };
1008
+ //#endregion
1009
+ export { R as IdempotencyCache, G as RATE_WINDOW_MS, W as RATE_WINDOW_TABLE, xe as TARGETS_TABLE, u as WebhookDeliveryNotFound, d as WebhookPayloadInvalid, f as WebhookPayloadUnrepresentable, p as WebhookPayloadVersionInvalid, m as WebhookSubscribeInvalid, M as WebhooksService, X as acquireRateSlots, ke as buildDeliverWebhookExecute, ce as buildWebhooksService, ae as compareVersions, J as consumeRateSlot, pe as defaultIncomingPath, c as defaultOutgoingSignature, j as defaultRetryPolicy, s as defineIncomingWebhook, t as defineOutgoingEvent, r as defineWebhookProvider, Oe as deliverWebhookWorkflow, Z as encodePayload, re as fastRetryPolicy, P as generateSecret, B as getIdempotencyCache, i as githubSignature, l as isWebhookDescriptor, L as matchesFilter, fe as mountIncomingWebhook, A as nextRetry, O as parseDuration, H as parseTtl, we as recordDeliveryOutcome, Y as releaseRateSlot, I as resolveSubscribe, o as signPayload, n as slackSignature, e as stripeSignature, ue as useWebhooks, le as useWebhooksEffect, F as validateSubscribe, a as verifySignature, h as webhookTables };