@voltro/plugin-webhooks 0.86.0 → 0.87.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 +5321 -0
- package/THIRD-PARTY-NOTICES.md +860 -221
- package/dist/deliverWorkflow.js +27 -13
- package/dist/index.d.ts +46 -4
- package/dist/index.js +10 -10
- package/dist/mixin.d.ts +16 -2
- package/dist/mixin.js +2 -0
- package/dist/service.js +87 -51
- package/package.json +7 -7
package/dist/deliverWorkflow.js
CHANGED
|
@@ -62,6 +62,8 @@ var m = s.Struct({
|
|
|
62
62
|
event: s.String,
|
|
63
63
|
url: s.String,
|
|
64
64
|
secret: s.String,
|
|
65
|
+
previousSecret: s.optional(s.Union([s.String, s.Null])),
|
|
66
|
+
previousSecretUntil: s.optional(s.Union([s.String, s.Null])),
|
|
65
67
|
signing: s.Any,
|
|
66
68
|
retry: s.Any,
|
|
67
69
|
headers: s.Union([s.Record(s.String, s.String), s.Null]),
|
|
@@ -75,17 +77,26 @@ var m = s.Struct({
|
|
|
75
77
|
tenantId: s.Union([s.String, s.Null])
|
|
76
78
|
})]),
|
|
77
79
|
execute: o.tryPromise({
|
|
78
|
-
try: async () =>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
try: async () => {
|
|
81
|
+
let e = (await p.store.query({
|
|
82
|
+
table: "_voltro_webhook_targets",
|
|
83
|
+
predicate: c("id", S),
|
|
84
|
+
order: [{
|
|
85
|
+
column: "createdAt",
|
|
86
|
+
direction: "asc"
|
|
87
|
+
}],
|
|
88
|
+
take: 1,
|
|
89
|
+
skip: void 0,
|
|
90
|
+
projection: void 0
|
|
91
|
+
}))[0];
|
|
92
|
+
if (e === void 0) return null;
|
|
93
|
+
let t = e.previousSecretUntil;
|
|
94
|
+
return {
|
|
95
|
+
...e,
|
|
96
|
+
previousSecret: e.previousSecret ?? null,
|
|
97
|
+
previousSecretUntil: t instanceof Date ? t.toISOString() : t ?? null
|
|
98
|
+
};
|
|
99
|
+
},
|
|
89
100
|
catch: (e) => (E.debug("fetch-target raw error", {
|
|
90
101
|
targetId: S,
|
|
91
102
|
event: C,
|
|
@@ -196,7 +207,10 @@ var m = s.Struct({
|
|
|
196
207
|
};
|
|
197
208
|
throw e;
|
|
198
209
|
}
|
|
199
|
-
let a = Buffer.from(r.bytes),
|
|
210
|
+
let a = Buffer.from(r.bytes), o = u.previousSecret != null && u.previousSecretUntil != null && Date.parse(u.previousSecretUntil) > Date.now() && O._tag === "standardWebhooks" && O.previousSecret === void 0 ? {
|
|
211
|
+
...O,
|
|
212
|
+
previousSecret: u.previousSecret
|
|
213
|
+
} : O, { headers: s } = e(o, {
|
|
200
214
|
rawBody: a,
|
|
201
215
|
secret: u.secret,
|
|
202
216
|
messageId: x
|
|
@@ -207,7 +221,7 @@ var m = s.Struct({
|
|
|
207
221
|
method: "POST",
|
|
208
222
|
headers: {
|
|
209
223
|
"content-type": r.contentType,
|
|
210
|
-
...
|
|
224
|
+
...s,
|
|
211
225
|
"x-voltro-event": C,
|
|
212
226
|
"x-voltro-event-id": w,
|
|
213
227
|
"x-voltro-delivery-id": x,
|
package/dist/index.d.ts
CHANGED
|
@@ -617,6 +617,9 @@ emitter: () => Pick<WebhooksServiceShape, "emit"> | undefined) => OutboxHandlerD
|
|
|
617
617
|
*/
|
|
618
618
|
export declare const matchesFilter: (filter: WebhookFilter | Readonly<Record<string, unknown>> | null, payload: unknown) => boolean;
|
|
619
619
|
|
|
620
|
+
/** The longest overlap `rotateSecret` accepts, and its default: 30 days, unless ended earlier. */
|
|
621
|
+
export declare const MAX_SECRET_OVERLAP_SECONDS: number;
|
|
622
|
+
|
|
620
623
|
/** A mounted handler, carrying its verification declaration. */
|
|
621
624
|
export declare type MountedIncomingWebhook = ((request: IncomingRequest) => Promise<IncomingResponse>) & {
|
|
622
625
|
readonly [WEBHOOK_VERIFICATION_PROPERTY]: IncomingWebhookVerification;
|
|
@@ -822,6 +825,28 @@ export declare interface RetryPolicy {
|
|
|
822
825
|
|
|
823
826
|
export declare type RetryStrategy = 'fixed' | 'linear' | 'exponential';
|
|
824
827
|
|
|
828
|
+
/** What `rotateSecret` returns. */
|
|
829
|
+
export declare interface RotatedSecret {
|
|
830
|
+
/** The new secret — returned once; hand it to the receiver. */
|
|
831
|
+
readonly secret: string;
|
|
832
|
+
/** Until when deliveries are signed under the previous secret too. `null`:
|
|
833
|
+
* no overlap — asked for none, or a scheme with one signature per message. */
|
|
834
|
+
readonly previousSignedUntil: Date | null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** How long a rotated webhook secret keeps signing beside the new one. */
|
|
838
|
+
export declare interface RotateSecretOptions {
|
|
839
|
+
/**
|
|
840
|
+
* Seconds every delivery still carries a signature under the previous
|
|
841
|
+
* secret, so the receiver can switch when it is ready. The length is the
|
|
842
|
+
* RECEIVER's switch-over time — nothing on the sending side can measure it —
|
|
843
|
+
* so by default the overlap lasts until {@link WebhooksServiceShape.endSecretOverlap}
|
|
844
|
+
* ends it (once the receiver has switched), and at most 30 days. Pass a
|
|
845
|
+
* number to bound it yourself; `0` switches at once (after a leak).
|
|
846
|
+
*/
|
|
847
|
+
readonly overlapSeconds?: number;
|
|
848
|
+
}
|
|
849
|
+
|
|
825
850
|
export declare type SignatureScheme = HmacSignatureScheme | CustomSignatureScheme | StandardWebhooksSignatureScheme;
|
|
826
851
|
|
|
827
852
|
export declare interface SignedRequest {
|
|
@@ -1250,7 +1275,7 @@ export declare const _voltroWebhookDeliveriesTable: Table<"_voltro_webhook_deliv
|
|
|
1250
1275
|
readonly eventId: ColumnBuilder<string | null, "text", boolean>;
|
|
1251
1276
|
/** Attempt counter (1-indexed). */
|
|
1252
1277
|
readonly attempt: ColumnBuilder<number, "integer", boolean>;
|
|
1253
|
-
readonly status: ColumnBuilder<"failed" | "
|
|
1278
|
+
readonly status: ColumnBuilder<"failed" | "succeeded" | "pending" | "inFlight" | "retryScheduled", "text", boolean>;
|
|
1254
1279
|
/** Payload as sent over the wire. Stored verbatim — re-rendering
|
|
1255
1280
|
* from a referenced event row would lose the snapshot if the
|
|
1256
1281
|
* source event was deleted. */
|
|
@@ -1389,6 +1414,13 @@ export declare const _voltroWebhookTargetsTable: Table<"_voltro_webhook_targets"
|
|
|
1389
1414
|
* (a non-ciphertext value passes through); `voltro db encrypt-column
|
|
1390
1415
|
* _voltro_webhook_targets.secret` converts them in place. */
|
|
1391
1416
|
readonly secret: ColumnBuilder<string, "text", boolean>;
|
|
1417
|
+
/** During a rotation (`rotateSecret`): the secret the receiver may still
|
|
1418
|
+
* verify with. Every delivery until {@link previousSecretUntil} carries a
|
|
1419
|
+
* signature under it beside the new one (Standard Webhooks' signature list),
|
|
1420
|
+
* so the receiver switches when it is ready. Encrypted like `secret`. */
|
|
1421
|
+
readonly previousSecret: ColumnBuilder<string | null, "text", boolean>;
|
|
1422
|
+
/** When deliveries stop signing under `previousSecret`. */
|
|
1423
|
+
readonly previousSecretUntil: ColumnBuilder<Date | null, "timestamp", boolean>;
|
|
1392
1424
|
/** Serialised `SignatureScheme` discriminated union — see `signing.ts`.
|
|
1393
1425
|
* Stored as JSON so future schemes don't require a schema migration. */
|
|
1394
1426
|
readonly signing: ColumnBuilder<unknown, "json", boolean>;
|
|
@@ -1702,8 +1734,11 @@ export declare interface WebhooksServiceShape {
|
|
|
1702
1734
|
* must sign identically, and the previous way to achieve it was delete +
|
|
1703
1735
|
* re-subscribe, minting new ids and orphaning the delivery history.
|
|
1704
1736
|
*/
|
|
1705
|
-
readonly rotateSecret: (target: TargetSelector) => Promise<
|
|
1706
|
-
|
|
1737
|
+
readonly rotateSecret: (target: TargetSelector, options?: RotateSecretOptions) => Promise<RotatedSecret>;
|
|
1738
|
+
/** End a rotation's overlap once the receiver verifies with the new secret: the
|
|
1739
|
+
* previous secret stops signing at once. Returns how many targets were overlapping. */
|
|
1740
|
+
readonly endSecretOverlap: (target: TargetSelector) => Promise<{
|
|
1741
|
+
readonly ended: number;
|
|
1707
1742
|
}>;
|
|
1708
1743
|
/** Re-pin a target's `payloadVersion` to the event's current
|
|
1709
1744
|
* version. Called from the dashboard's "Re-pin" action after
|
|
@@ -1805,6 +1840,13 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
|
|
|
1805
1840
|
* (a non-ciphertext value passes through); `voltro db encrypt-column
|
|
1806
1841
|
* _voltro_webhook_targets.secret` converts them in place. */
|
|
1807
1842
|
readonly secret: ColumnBuilder<string, "text", boolean>;
|
|
1843
|
+
/** During a rotation (`rotateSecret`): the secret the receiver may still
|
|
1844
|
+
* verify with. Every delivery until {@link previousSecretUntil} carries a
|
|
1845
|
+
* signature under it beside the new one (Standard Webhooks' signature list),
|
|
1846
|
+
* so the receiver switches when it is ready. Encrypted like `secret`. */
|
|
1847
|
+
readonly previousSecret: ColumnBuilder<string | null, "text", boolean>;
|
|
1848
|
+
/** When deliveries stop signing under `previousSecret`. */
|
|
1849
|
+
readonly previousSecretUntil: ColumnBuilder<Date | null, "timestamp", boolean>;
|
|
1808
1850
|
/** Serialised `SignatureScheme` discriminated union — see `signing.ts`.
|
|
1809
1851
|
* Stored as JSON so future schemes don't require a schema migration. */
|
|
1810
1852
|
readonly signing: ColumnBuilder<unknown, "json", boolean>;
|
|
@@ -1910,7 +1952,7 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
|
|
|
1910
1952
|
readonly eventId: ColumnBuilder<string | null, "text", boolean>;
|
|
1911
1953
|
/** Attempt counter (1-indexed). */
|
|
1912
1954
|
readonly attempt: ColumnBuilder<number, "integer", boolean>;
|
|
1913
|
-
readonly status: ColumnBuilder<"failed" | "
|
|
1955
|
+
readonly status: ColumnBuilder<"failed" | "succeeded" | "pending" | "inFlight" | "retryScheduled", "text", boolean>;
|
|
1914
1956
|
/** Payload as sent over the wire. Stored verbatim — re-rendering
|
|
1915
1957
|
* from a referenced event row would lose the snapshot if the
|
|
1916
1958
|
* source event was deleted. */
|
package/dist/index.js
CHANGED
|
@@ -5,13 +5,13 @@ import { STANDARD_WEBHOOKS_DEFAULT_TOLERANCE_SECONDS as c, STANDARD_WEBHOOKS_ID_
|
|
|
5
5
|
import { defaultOutgoingSignature as C, genericHmacSignature as w, githubSignature as T, signRequest as E, slackSignature as D, standardWebhooksSignature as O, stripeSignature as k, verifyRequest as A } from "./signing.js";
|
|
6
6
|
import { defaultRetryPolicy as j, fastRetryPolicy as M, nextRetry as N, parseDuration as P } from "./retry.js";
|
|
7
7
|
import { WebhookDeliveryNotFound as F, WebhookPayloadInvalid as I, WebhookPayloadUnrepresentable as L, WebhookPayloadVersionInvalid as R, WebhookSubscribeInvalid as z, WebhookTargetNotFound as B } from "./errors.js";
|
|
8
|
-
import {
|
|
9
|
-
import { IdempotencyCache as
|
|
10
|
-
import { UnverifiedIncomingWebhook as
|
|
11
|
-
import { RATE_WINDOW_MS as
|
|
12
|
-
import { encodePayload as
|
|
13
|
-
import { TARGETS_TABLE as
|
|
14
|
-
import { buildDeliverWebhookExecute as
|
|
15
|
-
import { _voltroWebhookDeliveriesTable as
|
|
16
|
-
import { WEBHOOK_EMIT_EFFECT as
|
|
17
|
-
export {
|
|
8
|
+
import { MAX_SECRET_OVERLAP_SECONDS as V, WebhooksService as H, buildWebhooksService as U, compareVersions as W, eventsOf as G, generateSecret as K, matchesFilter as q, resolveSubscribe as J, useWebhooks as Y, useWebhooksEffect as X, validateSubscribe as Z } from "./service.js";
|
|
9
|
+
import { IdempotencyCache as Q, getIdempotencyCache as $, parseTtl as ee } from "./idempotency.js";
|
|
10
|
+
import { UnverifiedIncomingWebhook as te, WEBHOOK_VERIFICATION_PROPERTY as ne, defaultIncomingPath as re, incomingWebhookVerification as ie, mountIncomingWebhook as ae } from "./incoming.js";
|
|
11
|
+
import { RATE_WINDOW_MS as oe, RATE_WINDOW_TABLE as se, acquireRateSlots as ce, consumeRateSlot as le, releaseRateSlot as ue } from "./rateLimit.js";
|
|
12
|
+
import { encodePayload as de } from "./encode.js";
|
|
13
|
+
import { TARGETS_TABLE as fe, recordDeliveryOutcome as pe } from "./autoDisable.js";
|
|
14
|
+
import { buildDeliverWebhookExecute as me, deliverWebhookWorkflow as he } from "./deliverWorkflow.js";
|
|
15
|
+
import { _voltroWebhookDeliveriesTable as ge, _voltroWebhookEventStatsTable as _e, _voltroWebhookRateWindowsTable as ve, _voltroWebhookTargetsTable as ye, webhookTables as be } from "./mixin.js";
|
|
16
|
+
import { WEBHOOK_EMIT_EFFECT as xe, makeDeferredEmitHandler as Se } from "./deferredEmit.js";
|
|
17
|
+
export { Q as IdempotencyCache, V as MAX_SECRET_OVERLAP_SECONDS, oe as RATE_WINDOW_MS, se as RATE_WINDOW_TABLE, c as STANDARD_WEBHOOKS_DEFAULT_TOLERANCE_SECONDS, l as STANDARD_WEBHOOKS_ID_HEADER, u as STANDARD_WEBHOOKS_MAX_KEY_BYTES, d as STANDARD_WEBHOOKS_MIN_KEY_BYTES, f as STANDARD_WEBHOOKS_SECRET_PREFIX, p as STANDARD_WEBHOOKS_SIGNATURE_HEADER, m as STANDARD_WEBHOOKS_TIMESTAMP_HEADER, h as STANDARD_WEBHOOKS_VERSION, fe as TARGETS_TABLE, te as UnverifiedIncomingWebhook, e as WEBHOOKS_EGRESS, t as WEBHOOKS_PLUGIN_PERMISSIONS, xe as WEBHOOK_EMIT_EFFECT, ne as WEBHOOK_VERIFICATION_PROPERTY, F as WebhookDeliveryNotFound, I as WebhookPayloadInvalid, L as WebhookPayloadUnrepresentable, R as WebhookPayloadVersionInvalid, z as WebhookSubscribeInvalid, B as WebhookTargetNotFound, H as WebhooksService, ge as _voltroWebhookDeliveriesTable, _e as _voltroWebhookEventStatsTable, ve as _voltroWebhookRateWindowsTable, ye as _voltroWebhookTargetsTable, ce as acquireRateSlots, o as asOutgoingEvent, me as buildDeliverWebhookExecute, U as buildWebhooksService, W as compareVersions, le as consumeRateSlot, re as defaultIncomingPath, C as defaultOutgoingSignature, j as defaultRetryPolicy, r as defineIncomingWebhook, i as defineWebhookProvider, he as deliverWebhookWorkflow, de as encodePayload, G as eventsOf, M as fastRetryPolicy, K as generateSecret, g as generateStandardWebhooksSecret, w as genericHmacSignature, $ as getIdempotencyCache, T as githubSignature, s as hasWebhookAudience, ie as incomingWebhookVerification, _ as isStandardWebhooksSecret, a as isWebhookDescriptor, Se as makeDeferredEmitHandler, q as matchesFilter, ae as mountIncomingWebhook, N as nextRetry, P as parseDuration, ee as parseTtl, pe as recordDeliveryOutcome, ue as releaseRateSlot, J as resolveSubscribe, E as signRequest, D as slackSignature, v as standardWebhooksHeaders, y as standardWebhooksKey, O as standardWebhooksSignature, b as standardWebhooksSignatureToken, x as standardWebhooksSignedContent, k as stripeSignature, Y as useWebhooks, X as useWebhooksEffect, Z as validateSubscribe, A as verifyRequest, S as verifyStandardWebhooks, be as webhookTables, n as webhooksPlugin };
|
package/dist/mixin.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export declare const _voltroWebhookDeliveriesTable: Table<"_voltro_webhook_deliv
|
|
|
32
32
|
readonly eventId: ColumnBuilder<string | null, "text", boolean>;
|
|
33
33
|
/** Attempt counter (1-indexed). */
|
|
34
34
|
readonly attempt: ColumnBuilder<number, "integer", boolean>;
|
|
35
|
-
readonly status: ColumnBuilder<"failed" | "
|
|
35
|
+
readonly status: ColumnBuilder<"failed" | "succeeded" | "pending" | "inFlight" | "retryScheduled", "text", boolean>;
|
|
36
36
|
/** Payload as sent over the wire. Stored verbatim — re-rendering
|
|
37
37
|
* from a referenced event row would lose the snapshot if the
|
|
38
38
|
* source event was deleted. */
|
|
@@ -171,6 +171,13 @@ export declare const _voltroWebhookTargetsTable: Table<"_voltro_webhook_targets"
|
|
|
171
171
|
* (a non-ciphertext value passes through); `voltro db encrypt-column
|
|
172
172
|
* _voltro_webhook_targets.secret` converts them in place. */
|
|
173
173
|
readonly secret: ColumnBuilder<string, "text", boolean>;
|
|
174
|
+
/** During a rotation (`rotateSecret`): the secret the receiver may still
|
|
175
|
+
* verify with. Every delivery until {@link previousSecretUntil} carries a
|
|
176
|
+
* signature under it beside the new one (Standard Webhooks' signature list),
|
|
177
|
+
* so the receiver switches when it is ready. Encrypted like `secret`. */
|
|
178
|
+
readonly previousSecret: ColumnBuilder<string | null, "text", boolean>;
|
|
179
|
+
/** When deliveries stop signing under `previousSecret`. */
|
|
180
|
+
readonly previousSecretUntil: ColumnBuilder<Date | null, "timestamp", boolean>;
|
|
174
181
|
/** Serialised `SignatureScheme` discriminated union — see `signing.ts`.
|
|
175
182
|
* Stored as JSON so future schemes don't require a schema migration. */
|
|
176
183
|
readonly signing: ColumnBuilder<unknown, "json", boolean>;
|
|
@@ -287,6 +294,13 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
|
|
|
287
294
|
* (a non-ciphertext value passes through); `voltro db encrypt-column
|
|
288
295
|
* _voltro_webhook_targets.secret` converts them in place. */
|
|
289
296
|
readonly secret: ColumnBuilder<string, "text", boolean>;
|
|
297
|
+
/** During a rotation (`rotateSecret`): the secret the receiver may still
|
|
298
|
+
* verify with. Every delivery until {@link previousSecretUntil} carries a
|
|
299
|
+
* signature under it beside the new one (Standard Webhooks' signature list),
|
|
300
|
+
* so the receiver switches when it is ready. Encrypted like `secret`. */
|
|
301
|
+
readonly previousSecret: ColumnBuilder<string | null, "text", boolean>;
|
|
302
|
+
/** When deliveries stop signing under `previousSecret`. */
|
|
303
|
+
readonly previousSecretUntil: ColumnBuilder<Date | null, "timestamp", boolean>;
|
|
290
304
|
/** Serialised `SignatureScheme` discriminated union — see `signing.ts`.
|
|
291
305
|
* Stored as JSON so future schemes don't require a schema migration. */
|
|
292
306
|
readonly signing: ColumnBuilder<unknown, "json", boolean>;
|
|
@@ -392,7 +406,7 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
|
|
|
392
406
|
readonly eventId: ColumnBuilder<string | null, "text", boolean>;
|
|
393
407
|
/** Attempt counter (1-indexed). */
|
|
394
408
|
readonly attempt: ColumnBuilder<number, "integer", boolean>;
|
|
395
|
-
readonly status: ColumnBuilder<"failed" | "
|
|
409
|
+
readonly status: ColumnBuilder<"failed" | "succeeded" | "pending" | "inFlight" | "retryScheduled", "text", boolean>;
|
|
396
410
|
/** Payload as sent over the wire. Stored verbatim — re-rendering
|
|
397
411
|
* from a referenced event row would lose the snapshot if the
|
|
398
412
|
* source event was deleted. */
|
package/dist/mixin.js
CHANGED
package/dist/service.js
CHANGED
|
@@ -8,12 +8,12 @@ import { randomBytes as m, randomUUID as h } from "node:crypto";
|
|
|
8
8
|
import { and as g, eq as _ } from "@voltro/database";
|
|
9
9
|
import { assertPublicUrl as v } from "@voltro/integration-http";
|
|
10
10
|
//#region src/service.ts
|
|
11
|
-
var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {},
|
|
11
|
+
var y = 2592e3, b = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, x = 32, S = 32, C = () => m(x).toString("hex"), w = (e) => e.events ?? (e.event === void 0 ? [] : [e.event]), T = (e, t) => {
|
|
12
12
|
if (e !== void 0 && t?._tag === "standardWebhooks" && !n(e)) throw new c({
|
|
13
13
|
field: "secret",
|
|
14
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
15
|
});
|
|
16
|
-
},
|
|
16
|
+
}, E = (e) => {
|
|
17
17
|
let t;
|
|
18
18
|
try {
|
|
19
19
|
t = new URL(e.url);
|
|
@@ -35,7 +35,7 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
35
35
|
reason: e.message
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
-
let n =
|
|
38
|
+
let n = w(e);
|
|
39
39
|
if (n.length === 0) throw new c({
|
|
40
40
|
field: "event",
|
|
41
41
|
reason: "event id required"
|
|
@@ -52,11 +52,11 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
52
52
|
field: "events",
|
|
53
53
|
reason: "duplicate event — one row per event, so a repeat would deliver twice"
|
|
54
54
|
});
|
|
55
|
-
if (e.secret !== void 0 && e.secret.length <
|
|
55
|
+
if (e.secret !== void 0 && e.secret.length < S) throw new c({
|
|
56
56
|
field: "secret",
|
|
57
|
-
reason: `secret must be at least ${
|
|
57
|
+
reason: `secret must be at least ${S} chars`
|
|
58
58
|
});
|
|
59
|
-
if (
|
|
59
|
+
if (T(e.secret, e.signing), e.retry !== void 0) {
|
|
60
60
|
if (e.retry.maxAttempts < 1) throw new c({
|
|
61
61
|
field: "retry",
|
|
62
62
|
reason: "retry.maxAttempts must be ≥ 1"
|
|
@@ -74,9 +74,9 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
74
74
|
field: "autoDisableAfter",
|
|
75
75
|
reason: `autoDisableAfter must be a positive integer, got ${e.autoDisableAfter}`
|
|
76
76
|
});
|
|
77
|
-
},
|
|
78
|
-
let s = e.signing ?? n?.defaultSigning ?? r(), c = s._tag === "standardWebhooks" ? t :
|
|
79
|
-
return
|
|
77
|
+
}, D = (e, n, a, o) => {
|
|
78
|
+
let s = e.signing ?? n?.defaultSigning ?? r(), c = s._tag === "standardWebhooks" ? t : C, l = o ?? e.secret ?? c();
|
|
79
|
+
return T(l, s), {
|
|
80
80
|
id: h(),
|
|
81
81
|
tenantId: e.tenantId ?? null,
|
|
82
82
|
event: a ?? e.event ?? "",
|
|
@@ -97,14 +97,14 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
97
97
|
payloadVersion: e.payloadVersion ?? n?.version ?? 1,
|
|
98
98
|
description: e.description ?? null
|
|
99
99
|
};
|
|
100
|
-
},
|
|
100
|
+
}, O = (e) => e instanceof Date ? e.getTime() : typeof e == "string" ? Date.parse(e) : 0, k = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : null, A = (e) => {
|
|
101
101
|
if (typeof e != "string") return e;
|
|
102
102
|
try {
|
|
103
103
|
return JSON.parse(e);
|
|
104
104
|
} catch {
|
|
105
105
|
return e;
|
|
106
106
|
}
|
|
107
|
-
},
|
|
107
|
+
}, j = (e) => ({
|
|
108
108
|
id: String(e.id),
|
|
109
109
|
deliveryId: String(e.deliveryId),
|
|
110
110
|
targetId: String(e.targetId),
|
|
@@ -115,28 +115,28 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
115
115
|
responseStatus: e.responseStatus ?? null,
|
|
116
116
|
errorMessage: e.errorMessage ?? null,
|
|
117
117
|
latencyMs: e.latencyMs ?? null,
|
|
118
|
-
scheduledAt:
|
|
119
|
-
nextAttemptAt:
|
|
120
|
-
createdAt:
|
|
121
|
-
}),
|
|
118
|
+
scheduledAt: k(e.scheduledAt),
|
|
119
|
+
nextAttemptAt: k(e.nextAttemptAt),
|
|
120
|
+
createdAt: k(e.createdAt)
|
|
121
|
+
}), M = (e, t) => e === t ? "current" : e < t ? "behind" : "ahead", N = (e, t) => {
|
|
122
122
|
if (e === null) return !0;
|
|
123
123
|
let n = { payload: t };
|
|
124
|
-
for (let [t, r] of Object.entries(e)) if (!
|
|
124
|
+
for (let [t, r] of Object.entries(e)) if (!I(P(n, t), r)) return !1;
|
|
125
125
|
return !0;
|
|
126
|
-
},
|
|
126
|
+
}, P = (e, t) => {
|
|
127
127
|
let n = t.split("."), r = e;
|
|
128
128
|
for (let e of n) {
|
|
129
129
|
if (typeof r != "object" || !r) return;
|
|
130
130
|
r = r[e];
|
|
131
131
|
}
|
|
132
132
|
return r;
|
|
133
|
-
},
|
|
133
|
+
}, F = (e, t) => {
|
|
134
134
|
if (typeof e == "number" && typeof t == "number") return e - t;
|
|
135
135
|
if (typeof e == "string" && typeof t == "string") return e < t ? -1 : +(e > t);
|
|
136
|
-
},
|
|
136
|
+
}, I = (e, t) => {
|
|
137
137
|
if (typeof t == "object" && t && !Array.isArray(t)) {
|
|
138
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 && !((
|
|
139
|
+
return "eq" in n && e !== n.eq || "in" in n && (!Array.isArray(n.in) || !n.in.includes(e)) || "gt" in n && !((F(e, n.gt) ?? 0) > 0) || "gte" in n && !((F(e, n.gte) ?? -1) >= 0) || "lt" in n && !((F(e, n.lt) ?? 0) < 0) || "lte" in n && !((F(e, n.lte) ?? 1) <= 0) ? !1 : [
|
|
140
140
|
"eq",
|
|
141
141
|
"in",
|
|
142
142
|
"gt",
|
|
@@ -146,9 +146,9 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
146
146
|
].some((e) => e in n);
|
|
147
147
|
}
|
|
148
148
|
return e === t;
|
|
149
|
-
},
|
|
149
|
+
}, L = (e, t) => e !== null && Object.entries(t).every(([t, n]) => e[t] === n), R = 500, z = 200, B = async (e, t) => {
|
|
150
150
|
let n = [];
|
|
151
|
-
for (let r = 0; r <
|
|
151
|
+
for (let r = 0; r < z; r += 1) {
|
|
152
152
|
let i = await e.store.query({
|
|
153
153
|
table: "_voltro_webhook_targets",
|
|
154
154
|
predicate: t,
|
|
@@ -156,20 +156,20 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
156
156
|
column: "id",
|
|
157
157
|
direction: "asc"
|
|
158
158
|
}],
|
|
159
|
-
take:
|
|
160
|
-
skip: r === 0 ? void 0 : r *
|
|
159
|
+
take: R,
|
|
160
|
+
skip: r === 0 ? void 0 : r * R,
|
|
161
161
|
projection: void 0
|
|
162
162
|
});
|
|
163
|
-
if (n.push(...i), i.length <
|
|
163
|
+
if (n.push(...i), i.length < R) return n;
|
|
164
164
|
}
|
|
165
|
-
throw Error(`webhooks: more than ${
|
|
166
|
-
},
|
|
167
|
-
let n = (await
|
|
165
|
+
throw Error(`webhooks: more than ${z * R} 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
|
+
}, V = async (e, t) => {
|
|
167
|
+
let n = (await B(e)).filter((e) => L(e.scope, t));
|
|
168
168
|
if (n.length === 0) return;
|
|
169
169
|
let r = new Set(n.map((e) => e.secret));
|
|
170
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
171
|
return [...r][0];
|
|
172
|
-
},
|
|
172
|
+
}, H = async (e, t) => {
|
|
173
173
|
try {
|
|
174
174
|
let n = await e.store.query({
|
|
175
175
|
table: "_voltro_webhook_event_stats",
|
|
@@ -195,11 +195,11 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
195
195
|
updatedAt: r
|
|
196
196
|
});
|
|
197
197
|
} catch {}
|
|
198
|
-
},
|
|
198
|
+
}, U = (n, r, i = {}) => {
|
|
199
199
|
let c = new Map((i.events ?? []).map((e) => [e.id, e])), u = {
|
|
200
200
|
subscribe: async (e) => {
|
|
201
|
-
|
|
202
|
-
let r =
|
|
201
|
+
E(e);
|
|
202
|
+
let r = w(e), i = e.secret === void 0 && e.scope !== void 0 ? await V(n, e.scope) : void 0, a = e.secret ?? i ?? t(), o = r.map((t) => D(e, c.get(t), t, a));
|
|
203
203
|
for (let e of o) await n.store.insert("_voltro_webhook_targets", {
|
|
204
204
|
...e,
|
|
205
205
|
createdAt: /* @__PURE__ */ new Date(),
|
|
@@ -228,9 +228,9 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
228
228
|
issues: e.failure.message
|
|
229
229
|
});
|
|
230
230
|
}
|
|
231
|
-
let d = a?.eventId ?? h(), m = a?.tenantId, v = await
|
|
232
|
-
await
|
|
233
|
-
let y = v.filter((e) =>
|
|
231
|
+
let d = a?.eventId ?? h(), m = a?.tenantId, v = await B(n, m == null ? _("event", u) : g(_("event", u), _("tenantId", m)));
|
|
232
|
+
await H(n, u);
|
|
233
|
+
let y = v.filter((e) => N(e.filter, i)), b = [], x = JSON.stringify(i), S = async (e, t) => {
|
|
234
234
|
let i = h();
|
|
235
235
|
e.active ? (await r({
|
|
236
236
|
deliveryId: i,
|
|
@@ -310,7 +310,7 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
310
310
|
})).length === 0) throw new l({ targetId: e });
|
|
311
311
|
return [e];
|
|
312
312
|
}
|
|
313
|
-
let t = (await
|
|
313
|
+
let t = (await B(n)).filter((t) => L(t.scope, e.scope)).map((e) => e.id);
|
|
314
314
|
if (t.length === 0) throw Error(`webhooks: no target matches scope ${JSON.stringify(e.scope)} — the operation would have silently done nothing`);
|
|
315
315
|
return t;
|
|
316
316
|
},
|
|
@@ -353,16 +353,52 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
353
353
|
deleteTarget: async (e) => {
|
|
354
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
355
|
},
|
|
356
|
-
rotateSecret: async (e) => {
|
|
357
|
-
let
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
356
|
+
rotateSecret: async (e, r = {}) => {
|
|
357
|
+
let i = r.overlapSeconds ?? 2592e3;
|
|
358
|
+
if (!Number.isInteger(i) || i < 0 || i > 2592e3) throw Error(`rotateSecret: overlapSeconds must be an integer between 0 and ${y} (30 days), got ${String(i)}`);
|
|
359
|
+
let a = t(), o = i > 0 ? /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3 + i * 1e3) : null, s = o !== null;
|
|
360
|
+
for (let t of await u.resolveTargets(e)) {
|
|
361
|
+
let [e] = await n.store.query({
|
|
362
|
+
table: "_voltro_webhook_targets",
|
|
363
|
+
predicate: _("id", t),
|
|
364
|
+
order: [],
|
|
365
|
+
take: 1,
|
|
366
|
+
skip: void 0,
|
|
367
|
+
projection: void 0
|
|
368
|
+
}), r = typeof e?.signing == "string" ? JSON.parse(e.signing) : e?.signing, i = o !== null && e !== void 0 && r?._tag === "standardWebhooks";
|
|
369
|
+
i || (s = !1), await n.store.update("_voltro_webhook_targets", t, {
|
|
370
|
+
secret: a,
|
|
371
|
+
previousSecret: i ? e.secret : null,
|
|
372
|
+
previousSecretUntil: i ? o : null,
|
|
373
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
secret: a,
|
|
378
|
+
previousSignedUntil: s ? o : null
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
endSecretOverlap: async (e) => {
|
|
382
|
+
let t = 0;
|
|
383
|
+
for (let r of await u.resolveTargets(e)) {
|
|
384
|
+
let [e] = await n.store.query({
|
|
385
|
+
table: "_voltro_webhook_targets",
|
|
386
|
+
predicate: _("id", r),
|
|
387
|
+
order: [],
|
|
388
|
+
take: 1,
|
|
389
|
+
skip: void 0,
|
|
390
|
+
projection: void 0
|
|
391
|
+
});
|
|
392
|
+
e?.previousSecret != null && (await n.store.update("_voltro_webhook_targets", r, {
|
|
393
|
+
previousSecret: null,
|
|
394
|
+
previousSecretUntil: null,
|
|
395
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
396
|
+
}), t += 1);
|
|
397
|
+
}
|
|
398
|
+
return { ended: t };
|
|
363
399
|
},
|
|
364
400
|
updateTarget: async (e, t) => {
|
|
365
|
-
if (t.url !== void 0 &&
|
|
401
|
+
if (t.url !== void 0 && E({
|
|
366
402
|
event: "x",
|
|
367
403
|
url: t.url
|
|
368
404
|
}), 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");
|
|
@@ -427,7 +463,7 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
427
463
|
skip: void 0,
|
|
428
464
|
projection: void 0
|
|
429
465
|
}), 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 ||
|
|
466
|
+
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 || O(e.createdAt) >= i).slice(0, e?.limit ?? 200).map(j);
|
|
431
467
|
},
|
|
432
468
|
getDelivery: async (e) => {
|
|
433
469
|
let t = (await n.store.query({
|
|
@@ -439,8 +475,8 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
439
475
|
projection: void 0
|
|
440
476
|
}))[0];
|
|
441
477
|
return t === void 0 ? null : {
|
|
442
|
-
...
|
|
443
|
-
payload:
|
|
478
|
+
...j(t),
|
|
479
|
+
payload: A(t.payload),
|
|
444
480
|
responseBody: t.responseBody ?? null
|
|
445
481
|
};
|
|
446
482
|
},
|
|
@@ -481,11 +517,11 @@ var y = class extends u.Service()("@voltro/webhooks/WebhooksService") {}, b = 32
|
|
|
481
517
|
}
|
|
482
518
|
};
|
|
483
519
|
return u;
|
|
484
|
-
},
|
|
485
|
-
return yield*
|
|
486
|
-
}),
|
|
520
|
+
}, W = d.gen(function* () {
|
|
521
|
+
return yield* b;
|
|
522
|
+
}), G = (e) => {
|
|
487
523
|
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
524
|
return e.webhooks;
|
|
489
525
|
};
|
|
490
526
|
//#endregion
|
|
491
|
-
export { y as WebhooksService,
|
|
527
|
+
export { y as MAX_SECRET_OVERLAP_SECONDS, b as WebhooksService, U as buildWebhooksService, M as compareVersions, w as eventsOf, C as generateSecret, N as matchesFilter, D as resolveSubscribe, G as useWebhooks, W as useWebhooksEffect, E as validateSubscribe };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-webhooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.0",
|
|
4
4
|
"description": "Webhooks plugin — first-class outgoing (multi-target, durable-workflow delivery, HMAC signing, retry policies, rate limits) + incoming (signature verification, idempotency, provider templates for Stripe / GitHub / Slack / generic).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"node": ">=24.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@voltro/database": "0.
|
|
52
|
-
"@voltro/integration-http": "0.
|
|
53
|
-
"@voltro/logger": "0.
|
|
54
|
-
"@voltro/plugin-multitenancy": "0.
|
|
55
|
-
"@voltro/protocol": "0.
|
|
56
|
-
"@voltro/runtime": "0.
|
|
51
|
+
"@voltro/database": "0.87.0",
|
|
52
|
+
"@voltro/integration-http": "0.87.0",
|
|
53
|
+
"@voltro/logger": "0.87.0",
|
|
54
|
+
"@voltro/plugin-multitenancy": "0.87.0",
|
|
55
|
+
"@voltro/protocol": "0.87.0",
|
|
56
|
+
"@voltro/runtime": "0.87.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"effect": "4.0.1"
|