@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.
@@ -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 () => (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,
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), { headers: o } = e(O, {
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
- ...o,
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" | "pending" | "succeeded" | "inFlight" | "retryScheduled", "text", boolean>;
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
- readonly secret: string;
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" | "pending" | "succeeded" | "inFlight" | "retryScheduled", "text", boolean>;
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 { WebhooksService as V, buildWebhooksService as H, compareVersions as U, eventsOf as W, generateSecret as G, matchesFilter as K, resolveSubscribe as q, useWebhooks as J, useWebhooksEffect as Y, validateSubscribe as X } from "./service.js";
9
- import { IdempotencyCache as Z, getIdempotencyCache as Q, parseTtl as $ } from "./idempotency.js";
10
- import { UnverifiedIncomingWebhook as ee, WEBHOOK_VERIFICATION_PROPERTY as te, defaultIncomingPath as ne, incomingWebhookVerification as re, mountIncomingWebhook as ie } from "./incoming.js";
11
- import { RATE_WINDOW_MS as ae, RATE_WINDOW_TABLE as oe, acquireRateSlots as se, consumeRateSlot as ce, releaseRateSlot as le } from "./rateLimit.js";
12
- import { encodePayload as ue } from "./encode.js";
13
- import { TARGETS_TABLE as de, recordDeliveryOutcome as fe } from "./autoDisable.js";
14
- import { buildDeliverWebhookExecute as pe, deliverWebhookWorkflow as me } from "./deliverWorkflow.js";
15
- import { _voltroWebhookDeliveriesTable as he, _voltroWebhookEventStatsTable as ge, _voltroWebhookRateWindowsTable as _e, _voltroWebhookTargetsTable as ve, webhookTables as ye } from "./mixin.js";
16
- import { WEBHOOK_EMIT_EFFECT as be, makeDeferredEmitHandler as xe } from "./deferredEmit.js";
17
- export { Z as IdempotencyCache, ae as RATE_WINDOW_MS, oe 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, de as TARGETS_TABLE, ee as UnverifiedIncomingWebhook, e as WEBHOOKS_EGRESS, t as WEBHOOKS_PLUGIN_PERMISSIONS, be as WEBHOOK_EMIT_EFFECT, te as WEBHOOK_VERIFICATION_PROPERTY, F as WebhookDeliveryNotFound, I as WebhookPayloadInvalid, L as WebhookPayloadUnrepresentable, R as WebhookPayloadVersionInvalid, z as WebhookSubscribeInvalid, B as WebhookTargetNotFound, V as WebhooksService, he as _voltroWebhookDeliveriesTable, ge as _voltroWebhookEventStatsTable, _e as _voltroWebhookRateWindowsTable, ve as _voltroWebhookTargetsTable, se as acquireRateSlots, o as asOutgoingEvent, pe as buildDeliverWebhookExecute, H as buildWebhooksService, U as compareVersions, ce as consumeRateSlot, ne as defaultIncomingPath, C as defaultOutgoingSignature, j as defaultRetryPolicy, r as defineIncomingWebhook, i as defineWebhookProvider, me as deliverWebhookWorkflow, ue as encodePayload, W as eventsOf, M as fastRetryPolicy, G as generateSecret, g as generateStandardWebhooksSecret, w as genericHmacSignature, Q as getIdempotencyCache, T as githubSignature, s as hasWebhookAudience, re as incomingWebhookVerification, _ as isStandardWebhooksSecret, a as isWebhookDescriptor, xe as makeDeferredEmitHandler, K as matchesFilter, ie as mountIncomingWebhook, N as nextRetry, P as parseDuration, $ as parseTtl, fe as recordDeliveryOutcome, le as releaseRateSlot, q 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, J as useWebhooks, Y as useWebhooksEffect, X as validateSubscribe, A as verifyRequest, S as verifyStandardWebhooks, ye as webhookTables, n as webhooksPlugin };
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" | "pending" | "succeeded" | "inFlight" | "retryScheduled", "text", boolean>;
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" | "pending" | "succeeded" | "inFlight" | "retryScheduled", "text", boolean>;
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
@@ -6,6 +6,8 @@ var u = o("_voltro_webhook_targets", {
6
6
  event: s(),
7
7
  url: s(),
8
8
  secret: s().encrypted().serverOnly(),
9
+ previousSecret: s().encrypted().serverOnly().nullable(),
10
+ previousSecretUntil: c().nullable(),
9
11
  signing: r(),
10
12
  retry: r(),
11
13
  scope: r().nullable(),
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") {}, b = 32, x = 32, S = () => m(b).toString("hex"), C = (e) => e.events ?? (e.event === void 0 ? [] : [e.event]), w = (e, t) => {
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
- }, T = (e) => {
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 = C(e);
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 < x) throw new c({
55
+ if (e.secret !== void 0 && e.secret.length < S) throw new c({
56
56
  field: "secret",
57
- reason: `secret must be at least ${x} chars`
57
+ reason: `secret must be at least ${S} chars`
58
58
  });
59
- if (w(e.secret, e.signing), e.retry !== void 0) {
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
- }, E = (e, n, a, o) => {
78
- let s = e.signing ?? n?.defaultSigning ?? r(), c = s._tag === "standardWebhooks" ? t : S, l = o ?? e.secret ?? c();
79
- return w(l, s), {
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
- }, D = (e) => e instanceof Date ? e.getTime() : typeof e == "string" ? Date.parse(e) : 0, O = (e) => e instanceof Date ? e.toISOString() : typeof e == "string" ? e : null, k = (e) => {
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
- }, A = (e) => ({
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: O(e.scheduledAt),
119
- nextAttemptAt: O(e.nextAttemptAt),
120
- createdAt: O(e.createdAt)
121
- }), j = (e, t) => e === t ? "current" : e < t ? "behind" : "ahead", M = (e, t) => {
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 (!F(N(n, t), r)) return !1;
124
+ for (let [t, r] of Object.entries(e)) if (!I(P(n, t), r)) return !1;
125
125
  return !0;
126
- }, N = (e, t) => {
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
- }, P = (e, t) => {
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
- }, F = (e, t) => {
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 && !((P(e, n.gt) ?? 0) > 0) || "gte" in n && !((P(e, n.gte) ?? -1) >= 0) || "lt" in n && !((P(e, n.lt) ?? 0) < 0) || "lte" in n && !((P(e, n.lte) ?? 1) <= 0) ? !1 : [
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
- }, I = (e, t) => e !== null && Object.entries(t).every(([t, n]) => e[t] === n), L = 500, R = 200, z = async (e, t) => {
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 < R; r += 1) {
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: L,
160
- skip: r === 0 ? void 0 : r * L,
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 < L) return n;
163
+ if (n.push(...i), i.length < R) return n;
164
164
  }
165
- throw Error(`webhooks: more than ${R * L} rows in \`_voltro_webhook_targets\` — refusing to answer a scope lookup from a partial scan. A truncated scan mints a fresh secret for a live endpoint (breaking its signatures) or reports that a real scope matches nothing.`);
166
- }, B = async (e, t) => {
167
- let n = (await z(e)).filter((e) => I(e.scope, t));
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
- }, V = async (e, t) => {
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
- }, H = (n, r, i = {}) => {
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
- T(e);
202
- let r = C(e), i = e.secret === void 0 && e.scope !== void 0 ? await B(n, e.scope) : void 0, a = e.secret ?? i ?? t(), o = r.map((t) => E(e, c.get(t), t, a));
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 z(n, m == null ? _("event", u) : g(_("event", u), _("tenantId", m)));
232
- await V(n, u);
233
- let y = v.filter((e) => M(e.filter, i)), b = [], x = JSON.stringify(i), S = async (e, t) => {
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 z(n)).filter((t) => I(t.scope, e.scope)).map((e) => e.id);
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 r = t();
358
- for (let t of await u.resolveTargets(e)) await n.store.update("_voltro_webhook_targets", t, {
359
- secret: r,
360
- updatedAt: /* @__PURE__ */ new Date()
361
- });
362
- return { secret: r };
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 && T({
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 || D(e.createdAt) >= i).slice(0, e?.limit ?? 200).map(A);
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
- ...A(t),
443
- payload: k(t.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
- }, U = d.gen(function* () {
485
- return yield* y;
486
- }), W = (e) => {
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, H as buildWebhooksService, j as compareVersions, C as eventsOf, S as generateSecret, M as matchesFilter, E as resolveSubscribe, W as useWebhooks, U as useWebhooksEffect, T as validateSubscribe };
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.86.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.86.0",
52
- "@voltro/integration-http": "0.86.0",
53
- "@voltro/logger": "0.86.0",
54
- "@voltro/plugin-multitenancy": "0.86.0",
55
- "@voltro/protocol": "0.86.0",
56
- "@voltro/runtime": "0.86.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"