@voltro/plugin-webhooks 0.86.0 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/service.js CHANGED
@@ -7,13 +7,13 @@ import { Context as u, Effect as d, Result as f, Schema as p } from "effect";
7
7
  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
- //#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) => {
10
+ // src/service.ts
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,10 @@ 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
- //#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 };
526
+ 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/dist/signing.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { standardWebhooksHeaders as e, verifyStandardWebhooks as t } from "./standardWebhooks.js";
2
2
  import { createHmac as n, timingSafeEqual as r } from "node:crypto";
3
- //#region src/signing.ts
3
+ // src/signing.ts
4
4
  var i = (e) => e === "hmacSha256" ? "sha256" : "sha1", a = (e, t, r, a = "hex") => {
5
5
  let o = n(i(e), t);
6
6
  return o.update(r), o.digest(a);
@@ -141,5 +141,4 @@ var s = (t, n) => {
141
141
  encoding: "hex",
142
142
  versionPrefix: "v1="
143
143
  });
144
- //#endregion
145
144
  export { m as defaultOutgoingSignature, h as genericHmacSignature, d as githubSignature, s as signRequest, f as slackSignature, p as standardWebhooksSignature, u as stripeSignature, c as verifyRequest };
@@ -1,5 +1,5 @@
1
1
  import { createHmac as e, randomBytes as t, timingSafeEqual as n } from "node:crypto";
2
- //#region src/standardWebhooks.ts
2
+ // src/standardWebhooks.ts
3
3
  var r = "webhook-id", i = "webhook-timestamp", a = "webhook-signature", o = "v1", s = "whsec_", c = 24, l = 64, u = 300, d = () => `${s}${t(32).toString("base64")}`, f = (e) => {
4
4
  if (!e.startsWith("whsec_")) throw Error(`standard-webhooks: the signing secret must be the spec's serialization — "${s}" + base64(24..64 random bytes). Mint one with generateStandardWebhooksSecret(). (A hex secret is also valid base64, so accepting it would sign with the wrong key bytes and every conformant consumer would reject the delivery.)`);
5
5
  let t = Buffer.from(e.slice(6), "base64");
@@ -84,5 +84,4 @@ var r = "webhook-id", i = "webhook-timestamp", a = "webhook-signature", o = "v1"
84
84
  reason: "signature mismatch"
85
85
  };
86
86
  };
87
- //#endregion
88
87
  export { u as STANDARD_WEBHOOKS_DEFAULT_TOLERANCE_SECONDS, r as STANDARD_WEBHOOKS_ID_HEADER, l as STANDARD_WEBHOOKS_MAX_KEY_BYTES, c as STANDARD_WEBHOOKS_MIN_KEY_BYTES, s as STANDARD_WEBHOOKS_SECRET_PREFIX, a as STANDARD_WEBHOOKS_SIGNATURE_HEADER, i as STANDARD_WEBHOOKS_TIMESTAMP_HEADER, o as STANDARD_WEBHOOKS_VERSION, d as generateStandardWebhooksSecret, p as isStandardWebhooksSecret, _ as standardWebhooksHeaders, f as standardWebhooksKey, g as standardWebhooksSignatureToken, h as standardWebhooksSignedContent, y as verifyStandardWebhooks };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-webhooks",
3
- "version": "0.86.0",
3
+ "version": "0.88.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,15 +48,15 @@
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.88.0",
52
+ "@voltro/integration-http": "0.88.0",
53
+ "@voltro/logger": "0.88.0",
54
+ "@voltro/plugin-multitenancy": "0.88.0",
55
+ "@voltro/protocol": "0.88.0",
56
+ "@voltro/runtime": "0.88.0"
57
57
  },
58
58
  "peerDependencies": {
59
- "effect": "4.0.1"
59
+ "effect": "4.1.0"
60
60
  },
61
61
  "publishConfig": {
62
62
  "access": "public"