@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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { and as e, eq as t } from "@voltro/database";
|
|
2
|
+
//#region src/autoDisable.ts
|
|
3
|
+
var n = "_voltro_webhook_targets", r = 32, i = async (e, n) => (await e.query({
|
|
4
|
+
table: "_voltro_webhook_targets",
|
|
5
|
+
predicate: t("id", n),
|
|
6
|
+
order: [],
|
|
7
|
+
take: 1,
|
|
8
|
+
skip: void 0,
|
|
9
|
+
projection: void 0
|
|
10
|
+
}))[0] ?? null, a = async (n, a, o, s, c = /* @__PURE__ */ new Date(), l = !1) => {
|
|
11
|
+
for (let u = 0; u < r; u++) {
|
|
12
|
+
u > 0 && await new Promise((e) => setTimeout(e, u));
|
|
13
|
+
let r = await i(n, a);
|
|
14
|
+
if (r === null) return {
|
|
15
|
+
consecutiveFailures: 0,
|
|
16
|
+
autoDisabled: !1
|
|
17
|
+
};
|
|
18
|
+
let d = r.consecutiveFailures ?? 0;
|
|
19
|
+
if (o) {
|
|
20
|
+
if (d === 0 || await n.updateMany("_voltro_webhook_targets", {
|
|
21
|
+
consecutiveFailures: 0,
|
|
22
|
+
updatedAt: c
|
|
23
|
+
}, { where: e(t("id", a), t("consecutiveFailures", d)) }) === 1) return {
|
|
24
|
+
consecutiveFailures: 0,
|
|
25
|
+
autoDisabled: !1
|
|
26
|
+
};
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let f = d + 1, p = r.autoDisableAfter, m = r.active && (l || typeof p == "number" && p > 0 && f >= p), h = {
|
|
30
|
+
consecutiveFailures: f,
|
|
31
|
+
updatedAt: c
|
|
32
|
+
};
|
|
33
|
+
if (m && (h.active = !1, h.autoDisabledAt = c, h.autoDisableReason = s.slice(0, 500)), await n.updateMany("_voltro_webhook_targets", h, { where: e(t("id", a), t("consecutiveFailures", d)) }) === 1) return {
|
|
34
|
+
consecutiveFailures: f,
|
|
35
|
+
autoDisabled: m
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
throw Error(`webhook auto-disable: streak CAS contention on target ${a} exceeded ${r} attempts`);
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { n as TARGETS_TABLE, a as recordDeliveryOutcome };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineOutboxHandler as e } from "@voltro/runtime";
|
|
2
|
+
//#region src/deferredEmit.ts
|
|
3
|
+
var t = "voltro.webhook.emit", n = (e) => typeof e.event == "string", r = (r) => e({
|
|
4
|
+
effect: t,
|
|
5
|
+
maxAttempts: 1,
|
|
6
|
+
handler: async ({ payload: e, outboxId: i }) => {
|
|
7
|
+
if (!n(e)) throw Error(`${t}: row payload has no \`event\` name — cannot resolve a descriptor. Got keys: ${Object.keys(e).join(", ") || "(none)"}`);
|
|
8
|
+
let a = r();
|
|
9
|
+
if (a === void 0) throw Error(`${t}: the outgoing-webhooks service is not available in this process. An emit was enqueued by a build that had the plugin registered; this one does not.`);
|
|
10
|
+
await a.emit(e.event, e.payload, {
|
|
11
|
+
tenantId: e.tenantId,
|
|
12
|
+
eventId: i
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
//#endregion
|
|
17
|
+
export { t as WEBHOOK_EMIT_EFFECT, r as makeDeferredEmitHandler };
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { signRequest as e } from "./signing.js";
|
|
2
|
+
import { nextRetry as t } from "./retry.js";
|
|
3
|
+
import { WebhookPayloadUnrepresentable as n } from "./errors.js";
|
|
4
|
+
import { acquireRateSlots as r } from "./rateLimit.js";
|
|
5
|
+
import { encodePayload as i } from "./encode.js";
|
|
6
|
+
import { recordDeliveryOutcome as a } from "./autoDisable.js";
|
|
7
|
+
import { Effect as o, Schema as s } from "effect";
|
|
8
|
+
import { eq as c } from "@voltro/database";
|
|
9
|
+
import { assertPublicUrl as l } from "@voltro/integration-http";
|
|
10
|
+
import { createLogger as u } from "@voltro/logger";
|
|
11
|
+
import { Activity as d, DurableClock as f, Workflow as p } from "effect/workflow";
|
|
12
|
+
//#region src/deliverWorkflow.ts
|
|
13
|
+
var m = s.Struct({
|
|
14
|
+
status: s.Number,
|
|
15
|
+
body: s.String,
|
|
16
|
+
latencyMs: s.Number,
|
|
17
|
+
retryAfterSec: s.Union([s.Number, s.Undefined]),
|
|
18
|
+
transportError: s.Union([s.String, s.Undefined]),
|
|
19
|
+
encodeError: s.Union([s.String, s.Undefined])
|
|
20
|
+
}), h = s.Struct({
|
|
21
|
+
outcome: s.Literals([
|
|
22
|
+
"succeeded",
|
|
23
|
+
"failed",
|
|
24
|
+
"retry"
|
|
25
|
+
]),
|
|
26
|
+
delayMs: s.Union([s.Number, s.Undefined]),
|
|
27
|
+
reason: s.Union([s.String, s.Undefined]),
|
|
28
|
+
autoDisabled: s.Union([s.Boolean, s.Undefined]),
|
|
29
|
+
consecutiveFailures: s.Union([s.Number, s.Undefined])
|
|
30
|
+
}), g = s.Struct({
|
|
31
|
+
acquired: s.Boolean,
|
|
32
|
+
retryInMs: s.Number
|
|
33
|
+
}), _ = {
|
|
34
|
+
deliveryId: s.String,
|
|
35
|
+
targetId: s.String,
|
|
36
|
+
event: s.String,
|
|
37
|
+
eventId: s.String,
|
|
38
|
+
payloadJson: s.String,
|
|
39
|
+
attemptEpoch: s.optional(s.Number)
|
|
40
|
+
}, v = p.make("voltro.deliverWebhook", {
|
|
41
|
+
payload: _,
|
|
42
|
+
success: s.Struct({
|
|
43
|
+
finalStatus: s.Literals([
|
|
44
|
+
"succeeded",
|
|
45
|
+
"failed",
|
|
46
|
+
"deferred"
|
|
47
|
+
]),
|
|
48
|
+
attempts: s.Number
|
|
49
|
+
}),
|
|
50
|
+
idempotencyKey: ({ deliveryId: e, attemptEpoch: t }) => `voltro.deliverWebhook:${e}:${t ?? 0}`
|
|
51
|
+
}), y = 3e4, b = (e, t) => {
|
|
52
|
+
if (e !== void 0) return e;
|
|
53
|
+
let n = Number(t.VOLTRO_WEBHOOK_TIMEOUT_MS);
|
|
54
|
+
return Number.isFinite(n) && n > 0 ? n : y;
|
|
55
|
+
}, x = (p, _ = {}) => (v, y) => {
|
|
56
|
+
let { deliveryId: x, targetId: S, event: C, eventId: w, payloadJson: T } = v, E = u({ scope: `webhook:${x}` });
|
|
57
|
+
return o.gen(function* () {
|
|
58
|
+
let u = yield* d.make({
|
|
59
|
+
name: "fetch-target",
|
|
60
|
+
success: s.Union([s.Null, s.Struct({
|
|
61
|
+
id: s.String,
|
|
62
|
+
event: s.String,
|
|
63
|
+
url: s.String,
|
|
64
|
+
secret: s.String,
|
|
65
|
+
signing: s.Any,
|
|
66
|
+
retry: s.Any,
|
|
67
|
+
headers: s.Union([s.Record(s.String, s.String), s.Null]),
|
|
68
|
+
rateLimitPerMinute: s.Union([s.Number, s.Null]),
|
|
69
|
+
active: s.Boolean,
|
|
70
|
+
format: s.Literals([
|
|
71
|
+
"json",
|
|
72
|
+
"form",
|
|
73
|
+
"xml"
|
|
74
|
+
]),
|
|
75
|
+
tenantId: s.Union([s.String, s.Null])
|
|
76
|
+
})]),
|
|
77
|
+
execute: o.tryPromise({
|
|
78
|
+
try: async () => (await p.store.query({
|
|
79
|
+
table: "_voltro_webhook_targets",
|
|
80
|
+
predicate: c("id", S),
|
|
81
|
+
order: [{
|
|
82
|
+
column: "createdAt",
|
|
83
|
+
direction: "asc"
|
|
84
|
+
}],
|
|
85
|
+
take: 1,
|
|
86
|
+
skip: void 0,
|
|
87
|
+
projection: void 0
|
|
88
|
+
}))[0] ?? null,
|
|
89
|
+
catch: (e) => (E.debug("fetch-target raw error", {
|
|
90
|
+
targetId: S,
|
|
91
|
+
event: C,
|
|
92
|
+
cause: String(e)
|
|
93
|
+
}), /* @__PURE__ */ Error(`fetch-target failed: ${e.message}`))
|
|
94
|
+
}).pipe(o.orDie)
|
|
95
|
+
});
|
|
96
|
+
if (u === null) return {
|
|
97
|
+
finalStatus: "failed",
|
|
98
|
+
attempts: 0
|
|
99
|
+
};
|
|
100
|
+
let v = async (e, t, n) => {
|
|
101
|
+
await p.store.update("_voltro_webhook_deliveries", e, {
|
|
102
|
+
...n,
|
|
103
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
104
|
+
}) === null && await p.store.insert("_voltro_webhook_deliveries", {
|
|
105
|
+
id: e,
|
|
106
|
+
deliveryId: x,
|
|
107
|
+
targetId: S,
|
|
108
|
+
tenantId: u.tenantId ?? null,
|
|
109
|
+
event: C,
|
|
110
|
+
eventId: w,
|
|
111
|
+
attempt: t,
|
|
112
|
+
payload: T,
|
|
113
|
+
scheduledAt: /* @__PURE__ */ new Date(),
|
|
114
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
115
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
116
|
+
...n
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
if (u.active !== !0) return yield* d.make({
|
|
120
|
+
name: "record-paused-pending",
|
|
121
|
+
success: s.Void,
|
|
122
|
+
execute: o.tryPromise({
|
|
123
|
+
try: () => v(`${x}:1`, 1, { status: "pending" }),
|
|
124
|
+
catch: (e) => /* @__PURE__ */ Error(`record-paused-pending failed: ${e.message}`)
|
|
125
|
+
}).pipe(o.orDie)
|
|
126
|
+
}), {
|
|
127
|
+
finalStatus: "deferred",
|
|
128
|
+
attempts: 0
|
|
129
|
+
};
|
|
130
|
+
let y = (e) => typeof e == "string" ? JSON.parse(e) : e, D = y(u.retry), O = y(u.signing), k = u.headers === null ? null : y(u.headers), A = _.rateWindowMs ?? 6e4, j = b(_.timeoutMs, process.env), M = _.events?.find((e) => e.id === C)?.globalRateLimit, N = [...typeof u.rateLimitPerMinute == "number" && u.rateLimitPerMinute > 0 ? [{
|
|
131
|
+
key: `target:${u.id}`,
|
|
132
|
+
limit: u.rateLimitPerMinute
|
|
133
|
+
}] : [], ...M !== void 0 && M.perMinute > 0 ? [{
|
|
134
|
+
key: `event:${C}`,
|
|
135
|
+
limit: M.perMinute
|
|
136
|
+
}] : []];
|
|
137
|
+
for (let c = 1; c <= D.maxAttempts; c++) {
|
|
138
|
+
let _ = `${x}:${c}`;
|
|
139
|
+
if (N.length > 0) for (let e = 0;; e++) {
|
|
140
|
+
let t = yield* d.make({
|
|
141
|
+
name: `rate-acquire-${c}-${e}`,
|
|
142
|
+
success: g,
|
|
143
|
+
execute: o.tryPromise({
|
|
144
|
+
try: () => r(p.store, N, Date.now(), A),
|
|
145
|
+
catch: (e) => (E.debug("rate-acquire raw error", {
|
|
146
|
+
targetId: S,
|
|
147
|
+
event: C,
|
|
148
|
+
attempt: c,
|
|
149
|
+
cause: String(e)
|
|
150
|
+
}), /* @__PURE__ */ Error(`rate-acquire failed: ${e.message}`))
|
|
151
|
+
}).pipe(o.orDie)
|
|
152
|
+
});
|
|
153
|
+
if (t.acquired) break;
|
|
154
|
+
yield* d.make({
|
|
155
|
+
name: `record-rate-deferral-${c}-${e}`,
|
|
156
|
+
success: s.Void,
|
|
157
|
+
execute: o.tryPromise({
|
|
158
|
+
try: () => v(_, c, {
|
|
159
|
+
status: "pending",
|
|
160
|
+
nextAttemptAt: new Date(Date.now() + t.retryInMs)
|
|
161
|
+
}),
|
|
162
|
+
catch: (e) => /* @__PURE__ */ Error(`record-rate-deferral failed: ${e.message}`)
|
|
163
|
+
}).pipe(o.orDie)
|
|
164
|
+
}), yield* f.sleep({
|
|
165
|
+
name: `rate-window-sleep-${c}-${e}`,
|
|
166
|
+
duration: `${t.retryInMs} millis`
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
yield* d.make({
|
|
170
|
+
name: `record-attempt-start-${c}`,
|
|
171
|
+
success: s.Void,
|
|
172
|
+
execute: o.tryPromise({
|
|
173
|
+
try: () => v(_, c, {
|
|
174
|
+
status: "inFlight",
|
|
175
|
+
nextAttemptAt: null,
|
|
176
|
+
scheduledAt: /* @__PURE__ */ new Date()
|
|
177
|
+
}),
|
|
178
|
+
catch: (e) => /* @__PURE__ */ Error(`record-attempt-start failed: ${e.message}`)
|
|
179
|
+
}).pipe(o.orDie)
|
|
180
|
+
});
|
|
181
|
+
let y = yield* d.make({
|
|
182
|
+
name: `sign-and-post-${c}`,
|
|
183
|
+
success: m,
|
|
184
|
+
execute: o.promise(async () => {
|
|
185
|
+
let t = Date.now(), r;
|
|
186
|
+
try {
|
|
187
|
+
r = i(u.format, T);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
if (e instanceof n) return {
|
|
190
|
+
status: 0,
|
|
191
|
+
body: "",
|
|
192
|
+
latencyMs: Date.now() - t,
|
|
193
|
+
retryAfterSec: void 0,
|
|
194
|
+
transportError: void 0,
|
|
195
|
+
encodeError: e.reason
|
|
196
|
+
};
|
|
197
|
+
throw e;
|
|
198
|
+
}
|
|
199
|
+
let a = Buffer.from(r.bytes), { headers: o } = e(O, {
|
|
200
|
+
rawBody: a,
|
|
201
|
+
secret: u.secret,
|
|
202
|
+
messageId: x
|
|
203
|
+
});
|
|
204
|
+
process.env.NODE_ENV === "production" && l(u.url);
|
|
205
|
+
try {
|
|
206
|
+
let e = await fetch(u.url, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: {
|
|
209
|
+
"content-type": r.contentType,
|
|
210
|
+
...o,
|
|
211
|
+
"x-voltro-event": C,
|
|
212
|
+
"x-voltro-event-id": w,
|
|
213
|
+
"x-voltro-delivery-id": x,
|
|
214
|
+
"x-voltro-attempt": String(c),
|
|
215
|
+
"idempotency-key": x,
|
|
216
|
+
...k ?? {}
|
|
217
|
+
},
|
|
218
|
+
body: a,
|
|
219
|
+
redirect: "manual",
|
|
220
|
+
signal: AbortSignal.timeout(j)
|
|
221
|
+
}), n = (await e.text()).slice(0, 8192), i = e.headers.get("retry-after");
|
|
222
|
+
return {
|
|
223
|
+
status: e.status,
|
|
224
|
+
body: n,
|
|
225
|
+
latencyMs: Date.now() - t,
|
|
226
|
+
retryAfterSec: i !== null && /^\d+$/.test(i) ? Number(i) : void 0,
|
|
227
|
+
transportError: void 0,
|
|
228
|
+
encodeError: void 0
|
|
229
|
+
};
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return {
|
|
232
|
+
status: 0,
|
|
233
|
+
body: "",
|
|
234
|
+
latencyMs: Date.now() - t,
|
|
235
|
+
retryAfterSec: void 0,
|
|
236
|
+
transportError: e.message,
|
|
237
|
+
encodeError: void 0
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
}), b = yield* d.make({
|
|
242
|
+
name: `classify-and-record-${c}`,
|
|
243
|
+
success: h,
|
|
244
|
+
execute: o.tryPromise({
|
|
245
|
+
try: async () => {
|
|
246
|
+
let e = y.status >= 200 && y.status < 300, n = y.transportError !== void 0, r = y.encodeError !== void 0, i = y.status === 410, o = (e, t) => a(p.store, S, e, t, /* @__PURE__ */ new Date(), !e && i);
|
|
247
|
+
if (e) {
|
|
248
|
+
await p.store.update("_voltro_webhook_deliveries", _, {
|
|
249
|
+
status: "succeeded",
|
|
250
|
+
responseStatus: y.status,
|
|
251
|
+
responseBody: y.body,
|
|
252
|
+
latencyMs: y.latencyMs,
|
|
253
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
254
|
+
});
|
|
255
|
+
let e = await o(!0, "");
|
|
256
|
+
return {
|
|
257
|
+
outcome: "succeeded",
|
|
258
|
+
delayMs: void 0,
|
|
259
|
+
reason: void 0,
|
|
260
|
+
autoDisabled: e.autoDisabled,
|
|
261
|
+
consecutiveFailures: e.consecutiveFailures
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
let s = r ? null : t(D, c, {
|
|
265
|
+
...n ? {} : { status: y.status },
|
|
266
|
+
...y.retryAfterSec === void 0 ? {} : { retryAfterSeconds: y.retryAfterSec }
|
|
267
|
+
});
|
|
268
|
+
if (s === null) {
|
|
269
|
+
let e = r ? `encode (${u.format}): ${y.encodeError}` : n ? `transport: ${y.transportError}` : `http ${y.status}: ${y.body.slice(0, 200)}`;
|
|
270
|
+
await p.store.update("_voltro_webhook_deliveries", _, {
|
|
271
|
+
status: "failed",
|
|
272
|
+
responseStatus: n || r ? null : y.status,
|
|
273
|
+
responseBody: n || r ? null : y.body,
|
|
274
|
+
errorMessage: r ? e : y.transportError ?? null,
|
|
275
|
+
latencyMs: y.latencyMs,
|
|
276
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
277
|
+
});
|
|
278
|
+
let t = await o(!1, e);
|
|
279
|
+
return {
|
|
280
|
+
outcome: "failed",
|
|
281
|
+
delayMs: void 0,
|
|
282
|
+
reason: e,
|
|
283
|
+
autoDisabled: t.autoDisabled,
|
|
284
|
+
consecutiveFailures: t.consecutiveFailures
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return await p.store.update("_voltro_webhook_deliveries", _, {
|
|
288
|
+
status: "retryScheduled",
|
|
289
|
+
responseStatus: n ? null : y.status,
|
|
290
|
+
responseBody: n ? null : y.body,
|
|
291
|
+
errorMessage: y.transportError ?? null,
|
|
292
|
+
latencyMs: y.latencyMs,
|
|
293
|
+
nextAttemptAt: new Date(Date.now() + s.delayMs),
|
|
294
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
295
|
+
}), {
|
|
296
|
+
outcome: "retry",
|
|
297
|
+
delayMs: s.delayMs,
|
|
298
|
+
reason: void 0,
|
|
299
|
+
autoDisabled: void 0,
|
|
300
|
+
consecutiveFailures: void 0
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
catch: (e) => (E.debug("classify-and-record raw error", {
|
|
304
|
+
targetId: S,
|
|
305
|
+
event: C,
|
|
306
|
+
attempt: c,
|
|
307
|
+
cause: String(e)
|
|
308
|
+
}), /* @__PURE__ */ Error(`classify-and-record failed: ${e.message}`))
|
|
309
|
+
}).pipe(o.orDie)
|
|
310
|
+
});
|
|
311
|
+
if (b.autoDisabled === !0 && E.warn("target auto-disabled after consecutive failures", {
|
|
312
|
+
targetId: S,
|
|
313
|
+
event: C,
|
|
314
|
+
consecutiveFailures: b.consecutiveFailures ?? null,
|
|
315
|
+
reason: b.reason ?? null
|
|
316
|
+
}), b.outcome === "succeeded") return {
|
|
317
|
+
finalStatus: "succeeded",
|
|
318
|
+
attempts: c
|
|
319
|
+
};
|
|
320
|
+
if (b.outcome === "failed") return {
|
|
321
|
+
finalStatus: "failed",
|
|
322
|
+
attempts: c
|
|
323
|
+
};
|
|
324
|
+
b.delayMs !== void 0 && (yield* f.sleep({
|
|
325
|
+
name: `retry-sleep-${c}`,
|
|
326
|
+
duration: `${b.delayMs} millis`
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
finalStatus: "failed",
|
|
331
|
+
attempts: D.maxAttempts
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
//#endregion
|
|
336
|
+
export { y as DEFAULT_WEBHOOK_TIMEOUT_MS, x as buildDeliverWebhookExecute, v as deliverWebhookWorkflow };
|
package/dist/dsl.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/dsl.ts
|
|
2
|
+
var e = (e) => ({
|
|
3
|
+
_tag: "incomingWebhook",
|
|
4
|
+
...e
|
|
5
|
+
}), t = (e) => ({
|
|
6
|
+
_tag: "webhookProvider",
|
|
7
|
+
...e
|
|
8
|
+
}), n = (e) => {
|
|
9
|
+
if (typeof e != "object" || !e) return !1;
|
|
10
|
+
let t = e._tag;
|
|
11
|
+
return t === "outgoingEvent" || t === "incomingWebhook" || t === "webhookProvider";
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { e as defineIncomingWebhook, t as defineWebhookProvider, n as isWebhookDescriptor };
|
package/dist/encode.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { WebhookPayloadUnrepresentable as e } from "./errors.js";
|
|
2
|
+
//#region src/encode.ts
|
|
3
|
+
var t = (t, r) => {
|
|
4
|
+
if (t === "json") return {
|
|
5
|
+
contentType: "application/json",
|
|
6
|
+
bytes: Buffer.from(r, "utf8")
|
|
7
|
+
};
|
|
8
|
+
let i;
|
|
9
|
+
try {
|
|
10
|
+
i = JSON.parse(r);
|
|
11
|
+
} catch (n) {
|
|
12
|
+
throw new e({
|
|
13
|
+
format: t,
|
|
14
|
+
reason: `payload is not valid JSON: ${n.message}`
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return t === "form" ? {
|
|
18
|
+
contentType: "application/x-www-form-urlencoded",
|
|
19
|
+
bytes: Buffer.from(n(i), "utf8")
|
|
20
|
+
} : {
|
|
21
|
+
contentType: "application/xml",
|
|
22
|
+
bytes: Buffer.from(a(i), "utf8")
|
|
23
|
+
};
|
|
24
|
+
}, n = (t) => {
|
|
25
|
+
if (typeof t != "object" || !t || Array.isArray(t)) throw new e({
|
|
26
|
+
format: "form",
|
|
27
|
+
reason: `form encoding requires a JSON object at the top level, got ${l(t)}`
|
|
28
|
+
});
|
|
29
|
+
let n = new URLSearchParams();
|
|
30
|
+
for (let [e, i] of Object.entries(t)) r(n, e, i);
|
|
31
|
+
return n.toString();
|
|
32
|
+
}, r = (e, t, n) => {
|
|
33
|
+
if (n === null) e.append(t, "");
|
|
34
|
+
else if (Array.isArray(n)) n.forEach((n, i) => r(e, `${t}[${i}]`, n));
|
|
35
|
+
else if (typeof n == "object") for (let [i, a] of Object.entries(n)) r(e, `${t}[${i}]`, a);
|
|
36
|
+
else e.append(t, String(n));
|
|
37
|
+
}, i = /^[A-Za-z_][A-Za-z0-9_.-]*$/, a = (e) => `<?xml version="1.0" encoding="UTF-8"?>${o("webhook", e)}`, o = (t, n) => {
|
|
38
|
+
if (!i.test(t)) throw new e({
|
|
39
|
+
format: "xml",
|
|
40
|
+
reason: `object key ${JSON.stringify(t)} is not a valid XML element name`
|
|
41
|
+
});
|
|
42
|
+
return `<${t}>${s(t, n)}</${t}>`;
|
|
43
|
+
}, s = (e, t) => t == null ? "" : Array.isArray(t) ? t.map((e) => o("item", e)).join("") : typeof t == "object" ? Object.entries(t).map(([e, t]) => o(e, t)).join("") : c(String(t)), c = (e) => e.replace(/[&<>"']/g, (e) => {
|
|
44
|
+
switch (e) {
|
|
45
|
+
case "&": return "&";
|
|
46
|
+
case "<": return "<";
|
|
47
|
+
case ">": return ">";
|
|
48
|
+
case "\"": return """;
|
|
49
|
+
default: return "'";
|
|
50
|
+
}
|
|
51
|
+
}), l = (e) => e === null ? "null" : Array.isArray(e) ? "array" : typeof e;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { t as encodePayload };
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Schema } from 'effect';
|
|
2
|
+
import { YieldableError } from 'effect/Cause';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Thrown by `WebhooksService.replay` when no delivery row exists for the
|
|
@@ -8,12 +9,10 @@ import { Schema } from 'effect';
|
|
|
8
9
|
export declare class WebhookDeliveryNotFound extends WebhookDeliveryNotFound_base {
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
declare const WebhookDeliveryNotFound_base: Schema.
|
|
12
|
-
readonly _tag: Schema.tag<"WebhookDeliveryNotFound">;
|
|
13
|
-
} & {
|
|
12
|
+
declare const WebhookDeliveryNotFound_base: Schema.Class<WebhookDeliveryNotFound, Schema.TaggedStruct<"WebhookDeliveryNotFound", {
|
|
14
13
|
/** The delivery id that did not resolve to a row. */
|
|
15
|
-
deliveryId:
|
|
16
|
-
}>;
|
|
14
|
+
readonly deliveryId: Schema.String;
|
|
15
|
+
}>, YieldableError>;
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* Thrown by `WebhooksService.emit` when the payload does not decode
|
|
@@ -25,14 +24,12 @@ declare const WebhookDeliveryNotFound_base: Schema.TaggedErrorClass<WebhookDeliv
|
|
|
25
24
|
export declare class WebhookPayloadInvalid extends WebhookPayloadInvalid_base {
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
declare const WebhookPayloadInvalid_base: Schema.
|
|
29
|
-
readonly _tag: Schema.tag<"WebhookPayloadInvalid">;
|
|
30
|
-
} & {
|
|
27
|
+
declare const WebhookPayloadInvalid_base: Schema.Class<WebhookPayloadInvalid, Schema.TaggedStruct<"WebhookPayloadInvalid", {
|
|
31
28
|
/** The outgoing event id the payload was emitted for. */
|
|
32
|
-
event:
|
|
29
|
+
readonly event: Schema.String;
|
|
33
30
|
/** Tree-formatted schema decode issues (one line per violation). */
|
|
34
|
-
issues:
|
|
35
|
-
}>;
|
|
31
|
+
readonly issues: Schema.String;
|
|
32
|
+
}>, YieldableError>;
|
|
36
33
|
|
|
37
34
|
/**
|
|
38
35
|
* Raised by the delivery workflow's wire-encoder when the target's
|
|
@@ -47,14 +44,12 @@ declare const WebhookPayloadInvalid_base: Schema.TaggedErrorClass<WebhookPayload
|
|
|
47
44
|
export declare class WebhookPayloadUnrepresentable extends WebhookPayloadUnrepresentable_base {
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
declare const WebhookPayloadUnrepresentable_base: Schema.
|
|
51
|
-
readonly _tag: Schema.tag<"WebhookPayloadUnrepresentable">;
|
|
52
|
-
} & {
|
|
47
|
+
declare const WebhookPayloadUnrepresentable_base: Schema.Class<WebhookPayloadUnrepresentable, Schema.TaggedStruct<"WebhookPayloadUnrepresentable", {
|
|
53
48
|
/** The wire format that could not represent the payload. */
|
|
54
|
-
format: Schema.
|
|
49
|
+
readonly format: Schema.Literals<readonly ["json", "form", "xml"]>;
|
|
55
50
|
/** Human-readable detail — which shape/key defeated the encoder. */
|
|
56
|
-
reason:
|
|
57
|
-
}>;
|
|
51
|
+
readonly reason: Schema.String;
|
|
52
|
+
}>, YieldableError>;
|
|
58
53
|
|
|
59
54
|
/**
|
|
60
55
|
* Thrown by `WebhooksService.updateTargetPayloadVersion` when the
|
|
@@ -63,12 +58,10 @@ declare const WebhookPayloadUnrepresentable_base: Schema.TaggedErrorClass<Webhoo
|
|
|
63
58
|
export declare class WebhookPayloadVersionInvalid extends WebhookPayloadVersionInvalid_base {
|
|
64
59
|
}
|
|
65
60
|
|
|
66
|
-
declare const WebhookPayloadVersionInvalid_base: Schema.
|
|
67
|
-
readonly _tag: Schema.tag<"WebhookPayloadVersionInvalid">;
|
|
68
|
-
} & {
|
|
61
|
+
declare const WebhookPayloadVersionInvalid_base: Schema.Class<WebhookPayloadVersionInvalid, Schema.TaggedStruct<"WebhookPayloadVersionInvalid", {
|
|
69
62
|
/** The rejected version value (as supplied by the caller). */
|
|
70
|
-
version:
|
|
71
|
-
}>;
|
|
63
|
+
readonly version: Schema.Number;
|
|
64
|
+
}>, YieldableError>;
|
|
72
65
|
|
|
73
66
|
/**
|
|
74
67
|
* Thrown by `WebhooksService.subscribe` (via `validateSubscribe`) when
|
|
@@ -78,26 +71,23 @@ declare const WebhookPayloadVersionInvalid_base: Schema.TaggedErrorClass<Webhook
|
|
|
78
71
|
* `reason` is the human-readable detail (carries the offending value).
|
|
79
72
|
*/
|
|
80
73
|
export declare class WebhookSubscribeInvalid extends WebhookSubscribeInvalid_base {
|
|
74
|
+
get message(): string;
|
|
81
75
|
}
|
|
82
76
|
|
|
83
|
-
declare const WebhookSubscribeInvalid_base: Schema.
|
|
84
|
-
readonly _tag: Schema.tag<"WebhookSubscribeInvalid">;
|
|
85
|
-
} & {
|
|
77
|
+
declare const WebhookSubscribeInvalid_base: Schema.Class<WebhookSubscribeInvalid, Schema.TaggedStruct<"WebhookSubscribeInvalid", {
|
|
86
78
|
/** The offending subscribe-input field: `'url'` | `'event'` |
|
|
87
79
|
* `'secret'` | `'retry'` | `'rateLimitPerMinute'`. */
|
|
88
|
-
field:
|
|
80
|
+
readonly field: Schema.String;
|
|
89
81
|
/** Human-readable detail, including the offending value. */
|
|
90
|
-
reason:
|
|
91
|
-
}>;
|
|
82
|
+
readonly reason: Schema.String;
|
|
83
|
+
}>, YieldableError>;
|
|
92
84
|
|
|
93
85
|
/** A target selected for management no longer exists. */
|
|
94
86
|
export declare class WebhookTargetNotFound extends WebhookTargetNotFound_base {
|
|
95
87
|
}
|
|
96
88
|
|
|
97
|
-
declare const WebhookTargetNotFound_base: Schema.
|
|
98
|
-
readonly
|
|
99
|
-
}
|
|
100
|
-
targetId: typeof Schema.String;
|
|
101
|
-
}>;
|
|
89
|
+
declare const WebhookTargetNotFound_base: Schema.Class<WebhookTargetNotFound, Schema.TaggedStruct<"WebhookTargetNotFound", {
|
|
90
|
+
readonly targetId: Schema.String;
|
|
91
|
+
}>, YieldableError>;
|
|
102
92
|
|
|
103
93
|
export { }
|
package/dist/errors.js
CHANGED
|
@@ -3,11 +3,19 @@ import { Schema as e } from "effect";
|
|
|
3
3
|
var t = class extends e.TaggedError()("WebhookSubscribeInvalid", {
|
|
4
4
|
field: e.String,
|
|
5
5
|
reason: e.String
|
|
6
|
-
}) {
|
|
6
|
+
}) {
|
|
7
|
+
get message() {
|
|
8
|
+
return `${this.field}: ${this.reason}`;
|
|
9
|
+
}
|
|
10
|
+
}, n = class extends e.TaggedError()("WebhookDeliveryNotFound", { deliveryId: e.String }) {}, r = class extends e.TaggedError()("WebhookTargetNotFound", { targetId: e.String }) {}, i = class extends e.TaggedError()("WebhookPayloadVersionInvalid", { version: e.Number }) {}, a = class extends e.TaggedError()("WebhookPayloadInvalid", {
|
|
7
11
|
event: e.String,
|
|
8
12
|
issues: e.String
|
|
9
13
|
}) {}, o = class extends e.TaggedError()("WebhookPayloadUnrepresentable", {
|
|
10
|
-
format: e.
|
|
14
|
+
format: e.Literals([
|
|
15
|
+
"json",
|
|
16
|
+
"form",
|
|
17
|
+
"xml"
|
|
18
|
+
]),
|
|
11
19
|
reason: e.String
|
|
12
20
|
}) {};
|
|
13
21
|
//#endregion
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/eventAudience.ts
|
|
2
|
+
var e = (e) => e.webhook !== void 0, t = (e) => {
|
|
3
|
+
let t = e.webhook;
|
|
4
|
+
if (t !== void 0) return {
|
|
5
|
+
_tag: "outgoingEvent",
|
|
6
|
+
id: e.name,
|
|
7
|
+
payload: e.payload,
|
|
8
|
+
...t.description === void 0 ? {} : { description: t.description },
|
|
9
|
+
...t.version === void 0 ? {} : { version: t.version },
|
|
10
|
+
...t.rateLimit === void 0 ? {} : { globalRateLimit: t.rateLimit }
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { t as asOutgoingEvent, e as hasWebhookAudience };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/idempotency.ts
|
|
2
|
+
var e = class {
|
|
3
|
+
capacity;
|
|
4
|
+
cache = /* @__PURE__ */ new Map();
|
|
5
|
+
constructor(e = 1e4) {
|
|
6
|
+
this.capacity = e;
|
|
7
|
+
}
|
|
8
|
+
evictExpired() {
|
|
9
|
+
let e = Date.now();
|
|
10
|
+
for (let [t, n] of this.cache) n.expiresAt < e && this.cache.delete(t);
|
|
11
|
+
}
|
|
12
|
+
claim(e, t) {
|
|
13
|
+
let n = this.cache.get(e);
|
|
14
|
+
if (n !== void 0 && n.expiresAt > Date.now()) return this.cache.delete(e), this.cache.set(e, n), n.value === "inflight" ? "inflight" : "duplicate";
|
|
15
|
+
if (this.cache.set(e, {
|
|
16
|
+
value: "inflight",
|
|
17
|
+
expiresAt: Date.now() + t
|
|
18
|
+
}), this.cache.size > this.capacity) {
|
|
19
|
+
let e = this.cache.keys().next().value;
|
|
20
|
+
e !== void 0 && this.cache.delete(e);
|
|
21
|
+
}
|
|
22
|
+
return "fresh";
|
|
23
|
+
}
|
|
24
|
+
commit(e) {
|
|
25
|
+
let t = this.cache.get(e);
|
|
26
|
+
t !== void 0 && this.cache.set(e, {
|
|
27
|
+
value: "processed",
|
|
28
|
+
expiresAt: t.expiresAt
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
release(e) {
|
|
32
|
+
this.cache.delete(e);
|
|
33
|
+
}
|
|
34
|
+
size() {
|
|
35
|
+
return this.evictExpired(), this.cache.size;
|
|
36
|
+
}
|
|
37
|
+
clear() {
|
|
38
|
+
this.cache.clear();
|
|
39
|
+
}
|
|
40
|
+
}, t = null, n = () => (t === null && (t = new e()), t), r = {
|
|
41
|
+
"5m": 3e5,
|
|
42
|
+
"1h": 36e5,
|
|
43
|
+
"6h": 216e5,
|
|
44
|
+
"1d": 864e5,
|
|
45
|
+
"7d": 6048e5,
|
|
46
|
+
"30d": 2592e6
|
|
47
|
+
}, i = (e) => e === void 0 ? r["7d"] : r[e] ?? r["7d"];
|
|
48
|
+
//#endregion
|
|
49
|
+
export { e as IdempotencyCache, n as getIdempotencyCache, i as parseTtl };
|