@webhooks-cc/sdk 0.6.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18,15 +18,20 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
20
  // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
23
  ApiError: () => ApiError,
24
24
  NotFoundError: () => NotFoundError,
25
25
  RateLimitError: () => RateLimitError,
26
+ TEMPLATE_METADATA: () => TEMPLATE_METADATA,
26
27
  TimeoutError: () => TimeoutError,
27
28
  UnauthorizedError: () => UnauthorizedError,
29
+ WebhookFlowBuilder: () => WebhookFlowBuilder,
28
30
  WebhooksCC: () => WebhooksCC,
29
31
  WebhooksCCError: () => WebhooksCCError,
32
+ diffRequests: () => diffRequests,
33
+ extractJsonField: () => extractJsonField,
34
+ isDiscordWebhook: () => isDiscordWebhook,
30
35
  isGitHubWebhook: () => isGitHubWebhook,
31
36
  isLinearWebhook: () => isLinearWebhook,
32
37
  isPaddleWebhook: () => isPaddleWebhook,
@@ -38,14 +43,30 @@ __export(index_exports, {
38
43
  matchAll: () => matchAll,
39
44
  matchAny: () => matchAny,
40
45
  matchBodyPath: () => matchBodyPath,
46
+ matchBodySubset: () => matchBodySubset,
47
+ matchContentType: () => matchContentType,
41
48
  matchHeader: () => matchHeader,
42
49
  matchJsonField: () => matchJsonField,
43
50
  matchMethod: () => matchMethod,
51
+ matchPath: () => matchPath,
52
+ matchQueryParam: () => matchQueryParam,
53
+ parseBody: () => parseBody,
44
54
  parseDuration: () => parseDuration,
55
+ parseFormBody: () => parseFormBody,
45
56
  parseJsonBody: () => parseJsonBody,
46
- parseSSE: () => parseSSE
57
+ parseSSE: () => parseSSE,
58
+ verifyDiscordSignature: () => verifyDiscordSignature,
59
+ verifyGitHubSignature: () => verifyGitHubSignature,
60
+ verifyLinearSignature: () => verifyLinearSignature,
61
+ verifyPaddleSignature: () => verifyPaddleSignature,
62
+ verifyShopifySignature: () => verifyShopifySignature,
63
+ verifySignature: () => verifySignature,
64
+ verifySlackSignature: () => verifySlackSignature,
65
+ verifyStandardWebhookSignature: () => verifyStandardWebhookSignature,
66
+ verifyStripeSignature: () => verifyStripeSignature,
67
+ verifyTwilioSignature: () => verifyTwilioSignature
47
68
  });
48
- module.exports = __toCommonJS(index_exports);
69
+ module.exports = __toCommonJS(src_exports);
49
70
 
50
71
  // src/errors.ts
51
72
  var WebhooksCCError = class extends Error {
@@ -84,7 +105,7 @@ var RateLimitError = class extends WebhooksCCError {
84
105
  };
85
106
 
86
107
  // src/utils.ts
87
- var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)$/;
108
+ var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
88
109
  function parseDuration(input) {
89
110
  if (typeof input === "number") {
90
111
  if (!Number.isFinite(input) || input < 0) {
@@ -114,6 +135,8 @@ function parseDuration(input) {
114
135
  return value * 6e4;
115
136
  case "h":
116
137
  return value * 36e5;
138
+ case "d":
139
+ return value * 864e5;
117
140
  default:
118
141
  throw new Error(`Invalid duration: "${input}"`);
119
142
  }
@@ -199,14 +222,95 @@ var DEFAULT_TEMPLATE_BY_PROVIDER = {
199
222
  stripe: "payment_intent.succeeded",
200
223
  github: "push",
201
224
  shopify: "orders/create",
202
- twilio: "messaging.inbound"
225
+ twilio: "messaging.inbound",
226
+ slack: "event_callback",
227
+ paddle: "transaction.completed",
228
+ linear: "issue.create"
203
229
  };
204
230
  var PROVIDER_TEMPLATES = {
205
231
  stripe: ["payment_intent.succeeded", "checkout.session.completed", "invoice.paid"],
206
232
  github: ["push", "pull_request.opened", "ping"],
207
233
  shopify: ["orders/create", "orders/paid", "products/update", "app/uninstalled"],
208
- twilio: ["messaging.inbound", "messaging.status_callback", "voice.incoming_call"]
234
+ twilio: ["messaging.inbound", "messaging.status_callback", "voice.incoming_call"],
235
+ slack: ["event_callback", "slash_command", "url_verification"],
236
+ paddle: ["transaction.completed", "subscription.created", "subscription.updated"],
237
+ linear: ["issue.create", "issue.update", "comment.create"]
209
238
  };
239
+ var TEMPLATE_PROVIDERS = [
240
+ "stripe",
241
+ "github",
242
+ "shopify",
243
+ "twilio",
244
+ "slack",
245
+ "paddle",
246
+ "linear",
247
+ "standard-webhooks"
248
+ ];
249
+ var TEMPLATE_METADATA = Object.freeze({
250
+ stripe: Object.freeze({
251
+ provider: "stripe",
252
+ templates: Object.freeze([...PROVIDER_TEMPLATES.stripe]),
253
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.stripe,
254
+ secretRequired: true,
255
+ signatureHeader: "stripe-signature",
256
+ signatureAlgorithm: "hmac-sha256"
257
+ }),
258
+ github: Object.freeze({
259
+ provider: "github",
260
+ templates: Object.freeze([...PROVIDER_TEMPLATES.github]),
261
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.github,
262
+ secretRequired: true,
263
+ signatureHeader: "x-hub-signature-256",
264
+ signatureAlgorithm: "hmac-sha256"
265
+ }),
266
+ shopify: Object.freeze({
267
+ provider: "shopify",
268
+ templates: Object.freeze([...PROVIDER_TEMPLATES.shopify]),
269
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.shopify,
270
+ secretRequired: true,
271
+ signatureHeader: "x-shopify-hmac-sha256",
272
+ signatureAlgorithm: "hmac-sha256"
273
+ }),
274
+ twilio: Object.freeze({
275
+ provider: "twilio",
276
+ templates: Object.freeze([...PROVIDER_TEMPLATES.twilio]),
277
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.twilio,
278
+ secretRequired: true,
279
+ signatureHeader: "x-twilio-signature",
280
+ signatureAlgorithm: "hmac-sha1"
281
+ }),
282
+ slack: Object.freeze({
283
+ provider: "slack",
284
+ templates: Object.freeze([...PROVIDER_TEMPLATES.slack]),
285
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.slack,
286
+ secretRequired: true,
287
+ signatureHeader: "x-slack-signature",
288
+ signatureAlgorithm: "hmac-sha256"
289
+ }),
290
+ paddle: Object.freeze({
291
+ provider: "paddle",
292
+ templates: Object.freeze([...PROVIDER_TEMPLATES.paddle]),
293
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.paddle,
294
+ secretRequired: true,
295
+ signatureHeader: "paddle-signature",
296
+ signatureAlgorithm: "hmac-sha256"
297
+ }),
298
+ linear: Object.freeze({
299
+ provider: "linear",
300
+ templates: Object.freeze([...PROVIDER_TEMPLATES.linear]),
301
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.linear,
302
+ secretRequired: true,
303
+ signatureHeader: "linear-signature",
304
+ signatureAlgorithm: "hmac-sha256"
305
+ }),
306
+ "standard-webhooks": Object.freeze({
307
+ provider: "standard-webhooks",
308
+ templates: Object.freeze([]),
309
+ secretRequired: true,
310
+ signatureHeader: "webhook-signature",
311
+ signatureAlgorithm: "hmac-sha256"
312
+ })
313
+ });
210
314
  function randomHex(length) {
211
315
  const bytes = new Uint8Array(Math.ceil(length / 2));
212
316
  globalThis.crypto.getRandomValues(bytes);
@@ -609,6 +713,181 @@ function buildTemplatePayload(provider, template, event, now, bodyOverride) {
609
713
  };
610
714
  }
611
715
  if (provider !== "twilio") {
716
+ if (provider === "slack") {
717
+ const eventCallbackPayload = {
718
+ token: randomHex(24),
719
+ team_id: `T${randomHex(8).toUpperCase()}`,
720
+ api_app_id: `A${randomHex(8).toUpperCase()}`,
721
+ type: "event_callback",
722
+ event: {
723
+ type: "app_mention",
724
+ user: `U${randomHex(8).toUpperCase()}`,
725
+ text: "hello from webhooks.cc",
726
+ ts: `${nowSec}.000100`,
727
+ channel: `C${randomHex(8).toUpperCase()}`,
728
+ event_ts: `${nowSec}.000100`
729
+ },
730
+ event_id: `Ev${randomHex(12)}`,
731
+ event_time: nowSec,
732
+ authed_users: [`U${randomHex(8).toUpperCase()}`]
733
+ };
734
+ const verificationPayload = {
735
+ token: randomHex(24),
736
+ challenge: randomHex(16),
737
+ type: "url_verification"
738
+ };
739
+ const defaultSlashCommand = {
740
+ token: randomHex(24),
741
+ team_id: `T${randomHex(8).toUpperCase()}`,
742
+ team_domain: "webhooks-cc",
743
+ channel_id: `C${randomHex(8).toUpperCase()}`,
744
+ channel_name: "general",
745
+ user_id: `U${randomHex(8).toUpperCase()}`,
746
+ user_name: "webhooks-bot",
747
+ command: "/webhook-test",
748
+ text: "hello world",
749
+ response_url: "https://hooks.slack.com/commands/demo",
750
+ trigger_id: randomHex(12)
751
+ };
752
+ if (template === "slash_command") {
753
+ let body2;
754
+ if (bodyOverride === void 0) {
755
+ body2 = formEncode(defaultSlashCommand);
756
+ } else if (typeof bodyOverride === "string") {
757
+ body2 = bodyOverride;
758
+ } else {
759
+ const params = asStringRecord(bodyOverride);
760
+ if (!params) {
761
+ throw new Error("Slack slash_command body override must be a string or an object");
762
+ }
763
+ body2 = formEncode(params);
764
+ }
765
+ return {
766
+ body: body2,
767
+ contentType: "application/x-www-form-urlencoded",
768
+ headers: {
769
+ "user-agent": "Slackbot 1.0 (+https://api.slack.com/robots)"
770
+ }
771
+ };
772
+ }
773
+ const payload = bodyOverride ?? (template === "url_verification" ? verificationPayload : eventCallbackPayload);
774
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
775
+ return {
776
+ body,
777
+ contentType: "application/json",
778
+ headers: {
779
+ "user-agent": "Slackbot 1.0 (+https://api.slack.com/robots)"
780
+ }
781
+ };
782
+ }
783
+ if (provider === "paddle") {
784
+ const payloadByTemplate = {
785
+ "transaction.completed": {
786
+ event_id: randomUuid(),
787
+ event_type: "transaction.completed",
788
+ occurred_at: nowIso,
789
+ notification_id: randomUuid(),
790
+ data: {
791
+ id: `txn_${randomHex(12)}`,
792
+ status: "completed",
793
+ customer_id: `ctm_${randomHex(12)}`,
794
+ currency_code: "USD",
795
+ total: "49.00"
796
+ }
797
+ },
798
+ "subscription.created": {
799
+ event_id: randomUuid(),
800
+ event_type: "subscription.created",
801
+ occurred_at: nowIso,
802
+ notification_id: randomUuid(),
803
+ data: {
804
+ id: `sub_${randomHex(12)}`,
805
+ status: "active",
806
+ customer_id: `ctm_${randomHex(12)}`,
807
+ next_billed_at: nowIso
808
+ }
809
+ },
810
+ "subscription.updated": {
811
+ event_id: randomUuid(),
812
+ event_type: "subscription.updated",
813
+ occurred_at: nowIso,
814
+ notification_id: randomUuid(),
815
+ data: {
816
+ id: `sub_${randomHex(12)}`,
817
+ status: "past_due",
818
+ customer_id: `ctm_${randomHex(12)}`,
819
+ next_billed_at: nowIso
820
+ }
821
+ }
822
+ };
823
+ const payload = bodyOverride ?? payloadByTemplate[template];
824
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
825
+ return {
826
+ body,
827
+ contentType: "application/json",
828
+ headers: {
829
+ "user-agent": "Paddle/1.0"
830
+ }
831
+ };
832
+ }
833
+ if (provider === "linear") {
834
+ const issueId = randomUuid();
835
+ const payloadByTemplate = {
836
+ "issue.create": {
837
+ action: "create",
838
+ type: "Issue",
839
+ webhookTimestamp: nowIso,
840
+ data: {
841
+ id: issueId,
842
+ identifier: "ENG-42",
843
+ title: "Investigate webhook retry regression",
844
+ description: "Created from the webhooks.cc Linear template",
845
+ url: `https://linear.app/webhooks-cc/issue/ENG-42/${issueId}`
846
+ }
847
+ },
848
+ "issue.update": {
849
+ action: "update",
850
+ type: "Issue",
851
+ webhookTimestamp: nowIso,
852
+ data: {
853
+ id: issueId,
854
+ identifier: "ENG-42",
855
+ title: "Investigate webhook retry regression",
856
+ state: {
857
+ name: "In Progress"
858
+ },
859
+ url: `https://linear.app/webhooks-cc/issue/ENG-42/${issueId}`
860
+ }
861
+ },
862
+ "comment.create": {
863
+ action: "create",
864
+ type: "Comment",
865
+ webhookTimestamp: nowIso,
866
+ data: {
867
+ id: randomUuid(),
868
+ body: "Looks good from the webhook sandbox.",
869
+ issue: {
870
+ id: issueId,
871
+ identifier: "ENG-42",
872
+ title: "Investigate webhook retry regression"
873
+ },
874
+ user: {
875
+ id: randomUuid(),
876
+ name: "webhooks.cc bot"
877
+ }
878
+ }
879
+ }
880
+ };
881
+ const payload = bodyOverride ?? payloadByTemplate[template];
882
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
883
+ return {
884
+ body,
885
+ contentType: "application/json",
886
+ headers: {
887
+ "user-agent": "Linear/1.0"
888
+ }
889
+ };
890
+ }
612
891
  throw new Error(`Unsupported provider: ${provider}`);
613
892
  }
614
893
  const defaultTwilioParamsByTemplate = {
@@ -759,6 +1038,19 @@ function buildTwilioSignaturePayload(endpointUrl, params) {
759
1038
  }
760
1039
  return payload;
761
1040
  }
1041
+ function decodeStandardWebhookSecret(secret) {
1042
+ let rawSecret = secret;
1043
+ const hadPrefix = rawSecret.startsWith("whsec_");
1044
+ if (hadPrefix) {
1045
+ rawSecret = rawSecret.slice(6);
1046
+ }
1047
+ try {
1048
+ return fromBase64(rawSecret);
1049
+ } catch {
1050
+ const raw = hadPrefix ? secret : rawSecret;
1051
+ return new TextEncoder().encode(raw);
1052
+ }
1053
+ }
762
1054
  async function buildTemplateSendOptions(endpointUrl, options) {
763
1055
  if (options.provider === "standard-webhooks") {
764
1056
  const method2 = (options.method ?? "POST").toUpperCase();
@@ -767,18 +1059,7 @@ async function buildTemplateSendOptions(endpointUrl, options) {
767
1059
  const msgId = options.event ? `msg_${options.event}_${randomHex(8)}` : `msg_${randomHex(16)}`;
768
1060
  const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
769
1061
  const signingInput = `${msgId}.${timestamp}.${body}`;
770
- let rawSecret = options.secret;
771
- const hadPrefix = rawSecret.startsWith("whsec_");
772
- if (hadPrefix) {
773
- rawSecret = rawSecret.slice(6);
774
- }
775
- let secretBytes;
776
- try {
777
- secretBytes = fromBase64(rawSecret);
778
- } catch {
779
- const raw = hadPrefix ? options.secret : rawSecret;
780
- secretBytes = new TextEncoder().encode(raw);
781
- }
1062
+ const secretBytes = decodeStandardWebhookSecret(options.secret);
782
1063
  const signature = await hmacSignRaw("SHA-256", secretBytes, signingInput);
783
1064
  return {
784
1065
  method: method2,
@@ -826,6 +1107,21 @@ async function buildTemplateSendOptions(endpointUrl, options) {
826
1107
  const signature = await hmacSign("SHA-1", options.secret, signaturePayload);
827
1108
  headers["x-twilio-signature"] = toBase64(signature);
828
1109
  }
1110
+ if (provider === "slack") {
1111
+ const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
1112
+ const signature = await hmacSign("SHA-256", options.secret, `v0:${timestamp}:${built.body}`);
1113
+ headers["x-slack-request-timestamp"] = String(timestamp);
1114
+ headers["x-slack-signature"] = `v0=${toHex(signature)}`;
1115
+ }
1116
+ if (provider === "paddle") {
1117
+ const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
1118
+ const signature = await hmacSign("SHA-256", options.secret, `${timestamp}:${built.body}`);
1119
+ headers["paddle-signature"] = `ts=${timestamp};h1=${toHex(signature)}`;
1120
+ }
1121
+ if (provider === "linear") {
1122
+ const signature = await hmacSign("SHA-256", options.secret, built.body);
1123
+ headers["linear-signature"] = `sha256=${toHex(signature)}`;
1124
+ }
829
1125
  return {
830
1126
  method,
831
1127
  headers: {
@@ -836,11 +1132,579 @@ async function buildTemplateSendOptions(endpointUrl, options) {
836
1132
  };
837
1133
  }
838
1134
 
1135
+ // src/request-export.ts
1136
+ var OMITTED_EXPORT_HEADERS = /* @__PURE__ */ new Set([
1137
+ "host",
1138
+ "connection",
1139
+ "content-length",
1140
+ "transfer-encoding",
1141
+ "keep-alive",
1142
+ "te",
1143
+ "trailer",
1144
+ "upgrade",
1145
+ "authorization",
1146
+ "cookie",
1147
+ "proxy-authorization",
1148
+ "set-cookie",
1149
+ "accept-encoding",
1150
+ "cdn-loop",
1151
+ "cf-connecting-ip",
1152
+ "cf-ipcountry",
1153
+ "cf-ray",
1154
+ "cf-visitor",
1155
+ "via",
1156
+ "x-forwarded-for",
1157
+ "x-forwarded-host",
1158
+ "x-forwarded-proto",
1159
+ "x-real-ip",
1160
+ "true-client-ip"
1161
+ ]);
1162
+ var VALID_HTTP_METHOD = /^[A-Z]+$/;
1163
+ function normalizePath(path) {
1164
+ return path.startsWith("/") ? path : `/${path}`;
1165
+ }
1166
+ function buildRequestUrl(endpointUrl, request) {
1167
+ const url = new URL(`${endpointUrl}${normalizePath(request.path)}`);
1168
+ for (const [key, value] of Object.entries(request.queryParams)) {
1169
+ url.searchParams.set(key, value);
1170
+ }
1171
+ return url.toString();
1172
+ }
1173
+ function shouldIncludeHeader(name) {
1174
+ return !OMITTED_EXPORT_HEADERS.has(name.toLowerCase());
1175
+ }
1176
+ function escapeForShellDoubleQuotes(value) {
1177
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
1178
+ }
1179
+ function escapeForShellSingleQuotes(value) {
1180
+ return value.replace(/'/g, "'\\''");
1181
+ }
1182
+ function buildCurlExport(endpointUrl, requests) {
1183
+ return requests.map((request) => {
1184
+ const method = VALID_HTTP_METHOD.test(request.method) ? request.method : "GET";
1185
+ const parts = [`curl -X ${method}`];
1186
+ for (const [key, value] of Object.entries(request.headers)) {
1187
+ if (!shouldIncludeHeader(key)) {
1188
+ continue;
1189
+ }
1190
+ parts.push(`-H "${escapeForShellDoubleQuotes(key)}: ${escapeForShellDoubleQuotes(value)}"`);
1191
+ }
1192
+ if (request.body) {
1193
+ parts.push(`-d '${escapeForShellSingleQuotes(request.body)}'`);
1194
+ }
1195
+ parts.push(`"${escapeForShellDoubleQuotes(buildRequestUrl(endpointUrl, request))}"`);
1196
+ return parts.join(" \\\n ");
1197
+ });
1198
+ }
1199
+ function buildHarExport(endpointUrl, requests, creatorVersion) {
1200
+ return {
1201
+ log: {
1202
+ version: "1.2",
1203
+ creator: {
1204
+ name: "@webhooks-cc/sdk",
1205
+ version: creatorVersion
1206
+ },
1207
+ entries: requests.map((request) => {
1208
+ const contentType = request.contentType ?? "application/octet-stream";
1209
+ return {
1210
+ startedDateTime: new Date(request.receivedAt).toISOString(),
1211
+ time: 0,
1212
+ request: {
1213
+ method: request.method,
1214
+ url: buildRequestUrl(endpointUrl, request),
1215
+ httpVersion: "HTTP/1.1",
1216
+ headers: Object.entries(request.headers).filter(([key]) => shouldIncludeHeader(key)).map(([name, value]) => ({ name, value })),
1217
+ queryString: Object.entries(request.queryParams).map(([name, value]) => ({
1218
+ name,
1219
+ value
1220
+ })),
1221
+ headersSize: -1,
1222
+ bodySize: request.body ? new TextEncoder().encode(request.body).length : 0,
1223
+ ...request.body ? {
1224
+ postData: {
1225
+ mimeType: contentType,
1226
+ text: request.body
1227
+ }
1228
+ } : {}
1229
+ },
1230
+ response: {
1231
+ status: 0,
1232
+ statusText: "",
1233
+ httpVersion: "HTTP/1.1",
1234
+ headers: [],
1235
+ cookies: [],
1236
+ content: {
1237
+ size: 0,
1238
+ mimeType: "x-unknown"
1239
+ },
1240
+ redirectURL: "",
1241
+ headersSize: -1,
1242
+ bodySize: -1
1243
+ },
1244
+ cache: {},
1245
+ timings: {
1246
+ send: 0,
1247
+ wait: 0,
1248
+ receive: 0
1249
+ }
1250
+ };
1251
+ })
1252
+ }
1253
+ };
1254
+ }
1255
+
1256
+ // src/verify.ts
1257
+ function requireSecret(secret, functionName) {
1258
+ if (!secret || typeof secret !== "string") {
1259
+ throw new Error(`${functionName} requires a non-empty secret`);
1260
+ }
1261
+ }
1262
+ function getHeader(headers, name) {
1263
+ const target = name.toLowerCase();
1264
+ for (const [key, value] of Object.entries(headers)) {
1265
+ if (key.toLowerCase() === target) {
1266
+ return value;
1267
+ }
1268
+ }
1269
+ return void 0;
1270
+ }
1271
+ function normalizeBody(body) {
1272
+ return body ?? "";
1273
+ }
1274
+ function hexToBytes(hex) {
1275
+ const normalized = hex.trim().toLowerCase();
1276
+ if (!/^[a-f0-9]+$/.test(normalized) || normalized.length % 2 !== 0) {
1277
+ throw new Error("Expected a hex-encoded value");
1278
+ }
1279
+ const bytes = new Uint8Array(normalized.length / 2);
1280
+ for (let index = 0; index < normalized.length; index += 2) {
1281
+ bytes[index / 2] = parseInt(normalized.slice(index, index + 2), 16);
1282
+ }
1283
+ return bytes;
1284
+ }
1285
+ function timingSafeEqual(left, right) {
1286
+ if (left.length !== right.length) {
1287
+ return false;
1288
+ }
1289
+ let mismatch = 0;
1290
+ for (let i = 0; i < left.length; i++) {
1291
+ mismatch |= left.charCodeAt(i) ^ right.charCodeAt(i);
1292
+ }
1293
+ return mismatch === 0;
1294
+ }
1295
+ function parseStripeHeader(signatureHeader) {
1296
+ if (!signatureHeader) {
1297
+ return null;
1298
+ }
1299
+ let timestamp;
1300
+ const signatures = [];
1301
+ for (const part of signatureHeader.split(",")) {
1302
+ const trimmed = part.trim();
1303
+ if (!trimmed) {
1304
+ continue;
1305
+ }
1306
+ const separatorIndex = trimmed.indexOf("=");
1307
+ if (separatorIndex === -1) {
1308
+ continue;
1309
+ }
1310
+ const key = trimmed.slice(0, separatorIndex).trim();
1311
+ const value = trimmed.slice(separatorIndex + 1).trim();
1312
+ if (!value) {
1313
+ continue;
1314
+ }
1315
+ if (key === "t") {
1316
+ timestamp = value;
1317
+ continue;
1318
+ }
1319
+ if (key === "v1") {
1320
+ signatures.push(value.toLowerCase());
1321
+ }
1322
+ }
1323
+ if (!timestamp || signatures.length === 0) {
1324
+ return null;
1325
+ }
1326
+ return { timestamp, signatures };
1327
+ }
1328
+ function parseStandardSignatures(signatureHeader) {
1329
+ if (!signatureHeader) {
1330
+ return [];
1331
+ }
1332
+ const matches = Array.from(
1333
+ signatureHeader.matchAll(/v1,([A-Za-z0-9+/=]+)/g),
1334
+ (match) => match[1]
1335
+ );
1336
+ if (matches.length > 0) {
1337
+ return matches;
1338
+ }
1339
+ const [version, signature] = signatureHeader.split(",", 2);
1340
+ if (version?.trim() === "v1" && signature?.trim()) {
1341
+ return [signature.trim()];
1342
+ }
1343
+ return [];
1344
+ }
1345
+ function parsePaddleSignature(signatureHeader) {
1346
+ if (!signatureHeader) {
1347
+ return null;
1348
+ }
1349
+ let timestamp;
1350
+ const signatures = [];
1351
+ for (const part of signatureHeader.split(/[;,]/)) {
1352
+ const trimmed = part.trim();
1353
+ if (!trimmed) {
1354
+ continue;
1355
+ }
1356
+ const separatorIndex = trimmed.indexOf("=");
1357
+ if (separatorIndex === -1) {
1358
+ continue;
1359
+ }
1360
+ const key = trimmed.slice(0, separatorIndex).trim().toLowerCase();
1361
+ const value = trimmed.slice(separatorIndex + 1).trim();
1362
+ if (!value) {
1363
+ continue;
1364
+ }
1365
+ if (key === "ts") {
1366
+ timestamp = value;
1367
+ continue;
1368
+ }
1369
+ if (key === "h1") {
1370
+ signatures.push(value.toLowerCase());
1371
+ }
1372
+ }
1373
+ if (!timestamp || signatures.length === 0) {
1374
+ return null;
1375
+ }
1376
+ return { timestamp, signatures };
1377
+ }
1378
+ function toTwilioParams(body) {
1379
+ if (body === void 0) {
1380
+ return [];
1381
+ }
1382
+ if (typeof body === "string") {
1383
+ return Array.from(new URLSearchParams(body).entries());
1384
+ }
1385
+ return Object.entries(body).map(([key, value]) => [key, String(value)]);
1386
+ }
1387
+ async function verifyStripeSignature(body, signatureHeader, secret) {
1388
+ requireSecret(secret, "verifyStripeSignature");
1389
+ const parsed = parseStripeHeader(signatureHeader);
1390
+ if (!parsed) {
1391
+ return false;
1392
+ }
1393
+ const expected = toHex(
1394
+ await hmacSign("SHA-256", secret, `${parsed.timestamp}.${normalizeBody(body)}`)
1395
+ ).toLowerCase();
1396
+ return parsed.signatures.some((signature) => timingSafeEqual(signature, expected));
1397
+ }
1398
+ async function verifyGitHubSignature(body, signatureHeader, secret) {
1399
+ requireSecret(secret, "verifyGitHubSignature");
1400
+ if (!signatureHeader) {
1401
+ return false;
1402
+ }
1403
+ const match = signatureHeader.trim().match(/^sha256=(.+)$/i);
1404
+ if (!match) {
1405
+ return false;
1406
+ }
1407
+ const expected = toHex(await hmacSign("SHA-256", secret, normalizeBody(body))).toLowerCase();
1408
+ return timingSafeEqual(match[1].toLowerCase(), expected);
1409
+ }
1410
+ async function verifyShopifySignature(body, signatureHeader, secret) {
1411
+ requireSecret(secret, "verifyShopifySignature");
1412
+ if (!signatureHeader) {
1413
+ return false;
1414
+ }
1415
+ const expected = toBase64(await hmacSign("SHA-256", secret, normalizeBody(body)));
1416
+ return timingSafeEqual(signatureHeader.trim(), expected);
1417
+ }
1418
+ async function verifyTwilioSignature(url, body, signatureHeader, secret) {
1419
+ requireSecret(secret, "verifyTwilioSignature");
1420
+ if (!url) {
1421
+ throw new Error("verifyTwilioSignature requires the signed URL");
1422
+ }
1423
+ if (!signatureHeader) {
1424
+ return false;
1425
+ }
1426
+ const expected = toBase64(
1427
+ await hmacSign("SHA-1", secret, buildTwilioSignaturePayload(url, toTwilioParams(body)))
1428
+ );
1429
+ return timingSafeEqual(signatureHeader.trim(), expected);
1430
+ }
1431
+ async function verifySlackSignature(body, headers, secret) {
1432
+ requireSecret(secret, "verifySlackSignature");
1433
+ const signatureHeader = getHeader(headers, "x-slack-signature");
1434
+ const timestamp = getHeader(headers, "x-slack-request-timestamp");
1435
+ if (!signatureHeader || !timestamp) {
1436
+ return false;
1437
+ }
1438
+ const match = signatureHeader.trim().match(/^v0=(.+)$/i);
1439
+ if (!match) {
1440
+ return false;
1441
+ }
1442
+ const expected = toHex(
1443
+ await hmacSign("SHA-256", secret, `v0:${timestamp}:${normalizeBody(body)}`)
1444
+ ).toLowerCase();
1445
+ return timingSafeEqual(match[1].toLowerCase(), expected);
1446
+ }
1447
+ async function verifyPaddleSignature(body, signatureHeader, secret) {
1448
+ requireSecret(secret, "verifyPaddleSignature");
1449
+ const parsed = parsePaddleSignature(signatureHeader);
1450
+ if (!parsed) {
1451
+ return false;
1452
+ }
1453
+ const expected = toHex(
1454
+ await hmacSign("SHA-256", secret, `${parsed.timestamp}:${normalizeBody(body)}`)
1455
+ ).toLowerCase();
1456
+ return parsed.signatures.some((signature) => timingSafeEqual(signature, expected));
1457
+ }
1458
+ async function verifyLinearSignature(body, signatureHeader, secret) {
1459
+ requireSecret(secret, "verifyLinearSignature");
1460
+ if (!signatureHeader) {
1461
+ return false;
1462
+ }
1463
+ const match = signatureHeader.trim().match(/^(?:sha256=)?(.+)$/i);
1464
+ if (!match) {
1465
+ return false;
1466
+ }
1467
+ const expected = toHex(await hmacSign("SHA-256", secret, normalizeBody(body))).toLowerCase();
1468
+ return timingSafeEqual(match[1].toLowerCase(), expected);
1469
+ }
1470
+ async function verifyDiscordSignature(body, headers, publicKey) {
1471
+ if (!publicKey || typeof publicKey !== "string") {
1472
+ throw new Error("verifyDiscordSignature requires a non-empty public key");
1473
+ }
1474
+ const signatureHeader = getHeader(headers, "x-signature-ed25519");
1475
+ const timestamp = getHeader(headers, "x-signature-timestamp");
1476
+ if (!signatureHeader || !timestamp) {
1477
+ return false;
1478
+ }
1479
+ if (!globalThis.crypto?.subtle) {
1480
+ throw new Error("crypto.subtle is required for Discord signature verification");
1481
+ }
1482
+ try {
1483
+ const publicKeyBytes = hexToBytes(publicKey);
1484
+ const signatureBytes = hexToBytes(signatureHeader);
1485
+ const publicKeyData = new Uint8Array(publicKeyBytes.byteLength);
1486
+ publicKeyData.set(publicKeyBytes);
1487
+ const signatureData = new Uint8Array(signatureBytes.byteLength);
1488
+ signatureData.set(signatureBytes);
1489
+ const key = await globalThis.crypto.subtle.importKey(
1490
+ "raw",
1491
+ publicKeyData,
1492
+ { name: "Ed25519" },
1493
+ false,
1494
+ ["verify"]
1495
+ );
1496
+ return await globalThis.crypto.subtle.verify(
1497
+ "Ed25519",
1498
+ key,
1499
+ signatureData,
1500
+ new TextEncoder().encode(`${timestamp}${normalizeBody(body)}`)
1501
+ );
1502
+ } catch {
1503
+ return false;
1504
+ }
1505
+ }
1506
+ async function verifyStandardWebhookSignature(body, headers, secret) {
1507
+ requireSecret(secret, "verifyStandardWebhookSignature");
1508
+ const messageId = getHeader(headers, "webhook-id");
1509
+ const timestamp = getHeader(headers, "webhook-timestamp");
1510
+ const signatureHeader = getHeader(headers, "webhook-signature");
1511
+ if (!messageId || !timestamp || !signatureHeader) {
1512
+ return false;
1513
+ }
1514
+ const expected = toBase64(
1515
+ await hmacSignRaw(
1516
+ "SHA-256",
1517
+ decodeStandardWebhookSecret(secret),
1518
+ `${messageId}.${timestamp}.${normalizeBody(body)}`
1519
+ )
1520
+ );
1521
+ return parseStandardSignatures(signatureHeader).some(
1522
+ (signature) => timingSafeEqual(signature, expected)
1523
+ );
1524
+ }
1525
+ async function verifySignature(request, options) {
1526
+ let valid = false;
1527
+ if (options.provider === "stripe") {
1528
+ valid = await verifyStripeSignature(
1529
+ request.body,
1530
+ getHeader(request.headers, "stripe-signature"),
1531
+ options.secret
1532
+ );
1533
+ }
1534
+ if (options.provider === "github") {
1535
+ valid = await verifyGitHubSignature(
1536
+ request.body,
1537
+ getHeader(request.headers, "x-hub-signature-256"),
1538
+ options.secret
1539
+ );
1540
+ }
1541
+ if (options.provider === "shopify") {
1542
+ valid = await verifyShopifySignature(
1543
+ request.body,
1544
+ getHeader(request.headers, "x-shopify-hmac-sha256"),
1545
+ options.secret
1546
+ );
1547
+ }
1548
+ if (options.provider === "twilio") {
1549
+ if (!options.url) {
1550
+ throw new Error('verifySignature for provider "twilio" requires options.url');
1551
+ }
1552
+ valid = await verifyTwilioSignature(
1553
+ options.url,
1554
+ request.body,
1555
+ getHeader(request.headers, "x-twilio-signature"),
1556
+ options.secret
1557
+ );
1558
+ }
1559
+ if (options.provider === "slack") {
1560
+ valid = await verifySlackSignature(request.body, request.headers, options.secret);
1561
+ }
1562
+ if (options.provider === "paddle") {
1563
+ valid = await verifyPaddleSignature(
1564
+ request.body,
1565
+ getHeader(request.headers, "paddle-signature"),
1566
+ options.secret
1567
+ );
1568
+ }
1569
+ if (options.provider === "linear") {
1570
+ valid = await verifyLinearSignature(
1571
+ request.body,
1572
+ getHeader(request.headers, "linear-signature"),
1573
+ options.secret
1574
+ );
1575
+ }
1576
+ if (options.provider === "discord") {
1577
+ valid = await verifyDiscordSignature(request.body, request.headers, options.publicKey);
1578
+ }
1579
+ if (options.provider === "standard-webhooks") {
1580
+ valid = await verifyStandardWebhookSignature(request.body, request.headers, options.secret);
1581
+ }
1582
+ return { valid };
1583
+ }
1584
+
1585
+ // src/flow.ts
1586
+ var WebhookFlowBuilder = class {
1587
+ constructor(client) {
1588
+ this.client = client;
1589
+ this.createOptions = {};
1590
+ this.deleteAfterRun = false;
1591
+ }
1592
+ createEndpoint(options = {}) {
1593
+ this.createOptions = { ...this.createOptions, ...options };
1594
+ return this;
1595
+ }
1596
+ setMock(mockResponse) {
1597
+ this.mockResponse = mockResponse;
1598
+ return this;
1599
+ }
1600
+ send(options = {}) {
1601
+ this.sendStep = { kind: "send", options };
1602
+ return this;
1603
+ }
1604
+ sendTemplate(options) {
1605
+ this.sendStep = { kind: "sendTemplate", options };
1606
+ return this;
1607
+ }
1608
+ waitForCapture(options = {}) {
1609
+ this.waitOptions = options;
1610
+ return this;
1611
+ }
1612
+ verifySignature(options) {
1613
+ this.verificationOptions = options;
1614
+ return this;
1615
+ }
1616
+ replayTo(targetUrl) {
1617
+ this.replayTargetUrl = targetUrl;
1618
+ return this;
1619
+ }
1620
+ cleanup() {
1621
+ this.deleteAfterRun = true;
1622
+ return this;
1623
+ }
1624
+ async run() {
1625
+ let endpoint;
1626
+ let result;
1627
+ let request;
1628
+ try {
1629
+ endpoint = await this.client.endpoints.create(this.createOptions);
1630
+ if (this.mockResponse !== void 0) {
1631
+ await this.client.endpoints.update(endpoint.slug, {
1632
+ mockResponse: this.mockResponse
1633
+ });
1634
+ }
1635
+ if (this.sendStep?.kind === "send") {
1636
+ await this.client.endpoints.send(endpoint.slug, this.sendStep.options);
1637
+ }
1638
+ if (this.sendStep?.kind === "sendTemplate") {
1639
+ await this.client.endpoints.sendTemplate(endpoint.slug, this.sendStep.options);
1640
+ }
1641
+ if (this.waitOptions) {
1642
+ request = await this.client.requests.waitFor(endpoint.slug, this.waitOptions);
1643
+ }
1644
+ let verification;
1645
+ if (this.verificationOptions) {
1646
+ if (!request) {
1647
+ throw new Error("Flow verification requires waitForCapture() to run first");
1648
+ }
1649
+ const resolvedUrl = typeof this.verificationOptions.url === "function" ? this.verificationOptions.url(endpoint, request) : this.verificationOptions.url ?? (this.verificationOptions.provider === "twilio" ? endpoint.url : void 0);
1650
+ if (this.verificationOptions.provider === "discord") {
1651
+ verification = await verifySignature(request, {
1652
+ provider: "discord",
1653
+ publicKey: this.verificationOptions.publicKey
1654
+ });
1655
+ } else {
1656
+ verification = await verifySignature(request, {
1657
+ provider: this.verificationOptions.provider,
1658
+ secret: this.verificationOptions.secret,
1659
+ ...resolvedUrl ? { url: resolvedUrl } : {}
1660
+ });
1661
+ }
1662
+ }
1663
+ let replayResponse;
1664
+ if (this.replayTargetUrl) {
1665
+ if (!request) {
1666
+ throw new Error("Flow replay requires waitForCapture() to run first");
1667
+ }
1668
+ replayResponse = await this.client.requests.replay(request.id, this.replayTargetUrl);
1669
+ }
1670
+ result = {
1671
+ endpoint,
1672
+ request,
1673
+ verification,
1674
+ replayResponse,
1675
+ cleanedUp: false
1676
+ };
1677
+ return result;
1678
+ } finally {
1679
+ if (this.deleteAfterRun && endpoint) {
1680
+ try {
1681
+ await this.client.endpoints.delete(endpoint.slug);
1682
+ if (result) {
1683
+ result.cleanedUp = true;
1684
+ }
1685
+ } catch (error) {
1686
+ if (error instanceof NotFoundError) {
1687
+ if (result) {
1688
+ result.cleanedUp = true;
1689
+ }
1690
+ }
1691
+ }
1692
+ }
1693
+ }
1694
+ }
1695
+ };
1696
+
839
1697
  // src/client.ts
840
1698
  var DEFAULT_BASE_URL = "https://webhooks.cc";
841
1699
  var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
842
1700
  var DEFAULT_TIMEOUT = 3e4;
1701
+ var DEFAULT_RETRY_ATTEMPTS = 1;
1702
+ var DEFAULT_RETRY_BACKOFF_MS = 1e3;
1703
+ var DEFAULT_RETRY_STATUSES = [429, 500, 502, 503, 504];
843
1704
  var SDK_VERSION = "0.6.0";
1705
+ var WAIT_FOR_LOOKBACK_MS = 5 * 60 * 1e3;
1706
+ var DEFAULT_EXPORT_PAGE_SIZE = 100;
1707
+ var PROVIDER_PARAM_DESCRIPTION = TEMPLATE_PROVIDERS.map((provider) => `"${provider}"`).join("|");
844
1708
  var MIN_POLL_INTERVAL = 10;
845
1709
  var MAX_POLL_INTERVAL = 6e4;
846
1710
  var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
@@ -901,26 +1765,233 @@ function validatePathSegment(segment, name) {
901
1765
  );
902
1766
  }
903
1767
  }
904
- var WebhooksCC = class {
905
- constructor(options) {
906
- this.endpoints = {
907
- create: async (options = {}) => {
908
- return this.request("POST", "/endpoints", options);
909
- },
910
- list: async () => {
911
- return this.request("GET", "/endpoints");
912
- },
913
- get: async (slug) => {
914
- validatePathSegment(slug, "slug");
915
- return this.request("GET", `/endpoints/${slug}`);
916
- },
917
- update: async (slug, options) => {
918
- validatePathSegment(slug, "slug");
919
- if (options.mockResponse && options.mockResponse !== null) {
920
- const { status } = options.mockResponse;
921
- if (!Number.isInteger(status) || status < 100 || status > 599) {
922
- throw new Error(`Invalid mock response status: ${status}. Must be an integer 100-599.`);
923
- }
1768
+ function resolveTimestampFilter(value, now) {
1769
+ if (typeof value === "number") {
1770
+ return value;
1771
+ }
1772
+ const trimmed = value.trim();
1773
+ if (trimmed.length === 0) {
1774
+ throw new Error("Invalid timestamp filter: value cannot be empty");
1775
+ }
1776
+ const asNumber = Number(trimmed);
1777
+ if (!Number.isNaN(asNumber)) {
1778
+ return asNumber;
1779
+ }
1780
+ return now - parseDuration(trimmed);
1781
+ }
1782
+ function buildSearchQuery(filters, includePagination) {
1783
+ const params = new URLSearchParams();
1784
+ const now = Date.now();
1785
+ if (filters.slug !== void 0) {
1786
+ validatePathSegment(filters.slug, "slug");
1787
+ params.set("slug", filters.slug);
1788
+ }
1789
+ if (filters.method !== void 0) {
1790
+ params.set("method", filters.method);
1791
+ }
1792
+ if (filters.q !== void 0) {
1793
+ params.set("q", filters.q);
1794
+ }
1795
+ if (filters.from !== void 0) {
1796
+ params.set("from", String(resolveTimestampFilter(filters.from, now)));
1797
+ }
1798
+ if (filters.to !== void 0) {
1799
+ params.set("to", String(resolveTimestampFilter(filters.to, now)));
1800
+ }
1801
+ if (includePagination && filters.limit !== void 0) {
1802
+ params.set("limit", String(filters.limit));
1803
+ }
1804
+ if (includePagination && filters.offset !== void 0) {
1805
+ params.set("offset", String(filters.offset));
1806
+ }
1807
+ if (includePagination && filters.order !== void 0) {
1808
+ params.set("order", filters.order);
1809
+ }
1810
+ const query = params.toString();
1811
+ return query ? `?${query}` : "";
1812
+ }
1813
+ function buildClearQuery(options = {}) {
1814
+ const params = new URLSearchParams();
1815
+ if (options.before !== void 0) {
1816
+ params.set("before", String(resolveTimestampFilter(options.before, Date.now())));
1817
+ }
1818
+ const query = params.toString();
1819
+ return query ? `?${query}` : "";
1820
+ }
1821
+ function buildPaginatedListQuery(options = {}) {
1822
+ const params = new URLSearchParams();
1823
+ if (options.limit !== void 0) {
1824
+ params.set("limit", String(options.limit));
1825
+ }
1826
+ if (options.cursor !== void 0) {
1827
+ params.set("cursor", options.cursor);
1828
+ }
1829
+ const query = params.toString();
1830
+ return query ? `?${query}` : "";
1831
+ }
1832
+ function parseRetryAfterHeader(response) {
1833
+ const retryAfterHeader = response.headers.get("retry-after");
1834
+ if (!retryAfterHeader) {
1835
+ return void 0;
1836
+ }
1837
+ const parsedSeconds = parseInt(retryAfterHeader, 10);
1838
+ if (!Number.isNaN(parsedSeconds) && parsedSeconds >= 0) {
1839
+ return parsedSeconds;
1840
+ }
1841
+ return void 0;
1842
+ }
1843
+ function normalizeRetryOptions(retry) {
1844
+ const maxAttempts = Math.max(1, Math.floor(retry?.maxAttempts ?? DEFAULT_RETRY_ATTEMPTS));
1845
+ const backoffMs = Math.max(0, Math.floor(retry?.backoffMs ?? DEFAULT_RETRY_BACKOFF_MS));
1846
+ return {
1847
+ maxAttempts,
1848
+ backoffMs,
1849
+ retryOn: new Set(retry?.retryOn ?? DEFAULT_RETRY_STATUSES)
1850
+ };
1851
+ }
1852
+ function normalizeExportPageSize(limit) {
1853
+ if (limit === void 0) {
1854
+ return DEFAULT_EXPORT_PAGE_SIZE;
1855
+ }
1856
+ return Math.max(1, Math.min(DEFAULT_EXPORT_PAGE_SIZE, Math.floor(limit)));
1857
+ }
1858
+ function buildStreamPath(slug, since) {
1859
+ const params = new URLSearchParams();
1860
+ if (since !== void 0) {
1861
+ params.set("since", String(Math.max(0, Math.floor(since))));
1862
+ }
1863
+ const query = params.toString();
1864
+ return `/api/stream/${slug}${query ? `?${query}` : ""}`;
1865
+ }
1866
+ function normalizeReconnectBackoff(value) {
1867
+ if (value === void 0) {
1868
+ return DEFAULT_RETRY_BACKOFF_MS;
1869
+ }
1870
+ return Math.max(0, parseDuration(value));
1871
+ }
1872
+ function shouldReconnectStreamError(error) {
1873
+ if (error instanceof UnauthorizedError || error instanceof NotFoundError) {
1874
+ return false;
1875
+ }
1876
+ if (error instanceof WebhooksCCError) {
1877
+ return error.statusCode === 429 || error.statusCode >= 500;
1878
+ }
1879
+ return error instanceof Error;
1880
+ }
1881
+ function parseStreamRequest(data) {
1882
+ try {
1883
+ const parsed = JSON.parse(data);
1884
+ if (typeof parsed.endpointId !== "string" || typeof parsed.method !== "string" || typeof parsed.receivedAt !== "number" || typeof parsed.headers !== "object" || parsed.headers === null) {
1885
+ return null;
1886
+ }
1887
+ return {
1888
+ id: typeof parsed._id === "string" ? parsed._id : typeof parsed.id === "string" ? parsed.id : "",
1889
+ endpointId: parsed.endpointId,
1890
+ method: parsed.method,
1891
+ path: typeof parsed.path === "string" ? parsed.path : "/",
1892
+ headers: parsed.headers,
1893
+ body: typeof parsed.body === "string" ? parsed.body : void 0,
1894
+ queryParams: typeof parsed.queryParams === "object" && parsed.queryParams !== null ? parsed.queryParams : {},
1895
+ contentType: typeof parsed.contentType === "string" ? parsed.contentType : void 0,
1896
+ ip: typeof parsed.ip === "string" ? parsed.ip : "unknown",
1897
+ size: typeof parsed.size === "number" ? parsed.size : 0,
1898
+ receivedAt: parsed.receivedAt
1899
+ };
1900
+ } catch {
1901
+ return null;
1902
+ }
1903
+ }
1904
+ async function collectMatchingRequests(fetchRequests, options) {
1905
+ const timeout = parseDuration(options.timeout ?? 3e4);
1906
+ const rawPollInterval = parseDuration(options.pollInterval ?? 500);
1907
+ const safePollInterval = Math.max(
1908
+ MIN_POLL_INTERVAL,
1909
+ Math.min(MAX_POLL_INTERVAL, rawPollInterval)
1910
+ );
1911
+ const desiredCount = Math.max(1, Math.floor(options.count));
1912
+ const start = Date.now();
1913
+ let lastChecked = start - WAIT_FOR_LOOKBACK_MS;
1914
+ let iterations = 0;
1915
+ const MAX_ITERATIONS = 1e4;
1916
+ const collected = [];
1917
+ const seenRequestIds = /* @__PURE__ */ new Set();
1918
+ while (Date.now() - start < timeout && iterations < MAX_ITERATIONS) {
1919
+ iterations++;
1920
+ const checkTime = Date.now();
1921
+ try {
1922
+ const requests = (await fetchRequests(lastChecked)).slice().sort((left, right) => left.receivedAt - right.receivedAt);
1923
+ lastChecked = checkTime;
1924
+ for (const request of requests) {
1925
+ if (seenRequestIds.has(request.id)) {
1926
+ continue;
1927
+ }
1928
+ seenRequestIds.add(request.id);
1929
+ if (options.match && !options.match(request)) {
1930
+ continue;
1931
+ }
1932
+ collected.push(request);
1933
+ if (collected.length >= desiredCount) {
1934
+ return collected.slice(0, desiredCount);
1935
+ }
1936
+ }
1937
+ } catch (error) {
1938
+ if (error instanceof WebhooksCCError) {
1939
+ if (error instanceof UnauthorizedError || error instanceof NotFoundError) {
1940
+ throw error;
1941
+ }
1942
+ if (error.statusCode < 500 && !(error instanceof RateLimitError)) {
1943
+ throw error;
1944
+ }
1945
+ }
1946
+ }
1947
+ await sleep(safePollInterval);
1948
+ }
1949
+ throw new TimeoutError(timeout);
1950
+ }
1951
+ function validateMockResponse(mockResponse, fieldName) {
1952
+ const { status } = mockResponse;
1953
+ if (!Number.isInteger(status) || status < 100 || status > 599) {
1954
+ throw new Error(`Invalid ${fieldName} status: ${status}. Must be an integer 100-599.`);
1955
+ }
1956
+ }
1957
+ var WebhooksCC = class {
1958
+ constructor(options) {
1959
+ this.endpoints = {
1960
+ create: async (options = {}) => {
1961
+ if (options.mockResponse) {
1962
+ validateMockResponse(options.mockResponse, "mock response");
1963
+ }
1964
+ const body = {};
1965
+ if (options.name !== void 0) {
1966
+ body.name = options.name;
1967
+ }
1968
+ if (options.mockResponse !== void 0) {
1969
+ body.mockResponse = options.mockResponse;
1970
+ }
1971
+ const isEphemeral = options.ephemeral === true || options.expiresIn !== void 0;
1972
+ if (isEphemeral) {
1973
+ body.isEphemeral = true;
1974
+ }
1975
+ if (options.expiresIn !== void 0) {
1976
+ const durationMs = parseDuration(options.expiresIn);
1977
+ if (durationMs <= 0) {
1978
+ throw new Error("expiresIn must be greater than 0");
1979
+ }
1980
+ body.expiresAt = Date.now() + durationMs;
1981
+ }
1982
+ return this.request("POST", "/endpoints", body);
1983
+ },
1984
+ list: async () => {
1985
+ return this.request("GET", "/endpoints");
1986
+ },
1987
+ get: async (slug) => {
1988
+ validatePathSegment(slug, "slug");
1989
+ return this.request("GET", `/endpoints/${slug}`);
1990
+ },
1991
+ update: async (slug, options) => {
1992
+ validatePathSegment(slug, "slug");
1993
+ if (options.mockResponse && options.mockResponse !== null) {
1994
+ validateMockResponse(options.mockResponse, "mock response");
924
1995
  }
925
1996
  return this.request("PATCH", `/endpoints/${slug}`, options);
926
1997
  },
@@ -963,6 +2034,20 @@ var WebhooksCC = class {
963
2034
  return this.endpoints.send(slug, sendOptions);
964
2035
  }
965
2036
  };
2037
+ this.templates = {
2038
+ listProviders: () => {
2039
+ return [...TEMPLATE_PROVIDERS];
2040
+ },
2041
+ get: (provider) => {
2042
+ return TEMPLATE_METADATA[provider];
2043
+ }
2044
+ };
2045
+ this.usage = async () => {
2046
+ return this.request("GET", "/usage");
2047
+ };
2048
+ this.flow = () => {
2049
+ return new WebhookFlowBuilder(this);
2050
+ };
966
2051
  /**
967
2052
  * Build a request without sending it. Returns the computed method, URL,
968
2053
  * headers, and body — including any provider signatures. Useful for
@@ -1092,10 +2177,86 @@ var WebhooksCC = class {
1092
2177
  `/endpoints/${endpointSlug}/requests${query ? `?${query}` : ""}`
1093
2178
  );
1094
2179
  },
2180
+ listPaginated: async (endpointSlug, options = {}) => {
2181
+ validatePathSegment(endpointSlug, "endpointSlug");
2182
+ return this.request(
2183
+ "GET",
2184
+ `/endpoints/${endpointSlug}/requests/paginated${buildPaginatedListQuery(options)}`
2185
+ );
2186
+ },
1095
2187
  get: async (requestId) => {
1096
2188
  validatePathSegment(requestId, "requestId");
1097
2189
  return this.request("GET", `/requests/${requestId}`);
1098
2190
  },
2191
+ waitForAll: async (endpointSlug, options) => {
2192
+ validatePathSegment(endpointSlug, "endpointSlug");
2193
+ const listLimit = Math.min(1e3, Math.max(100, Math.floor(options.count) * 2));
2194
+ return collectMatchingRequests(
2195
+ (since) => this.requests.list(endpointSlug, {
2196
+ since,
2197
+ limit: listLimit
2198
+ }),
2199
+ options
2200
+ );
2201
+ },
2202
+ search: async (filters = {}) => {
2203
+ return this.request(
2204
+ "GET",
2205
+ `/search/requests${buildSearchQuery(filters, true)}`
2206
+ );
2207
+ },
2208
+ count: async (filters = {}) => {
2209
+ const response = await this.request(
2210
+ "GET",
2211
+ `/search/requests/count${buildSearchQuery(filters, false)}`
2212
+ );
2213
+ return response.count;
2214
+ },
2215
+ clear: async (endpointSlug, options = {}) => {
2216
+ validatePathSegment(endpointSlug, "endpointSlug");
2217
+ await this.request(
2218
+ "DELETE",
2219
+ `/endpoints/${endpointSlug}/requests${buildClearQuery(options)}`
2220
+ );
2221
+ },
2222
+ export: async (endpointSlug, options) => {
2223
+ validatePathSegment(endpointSlug, "endpointSlug");
2224
+ const endpoint = await this.endpoints.get(endpointSlug);
2225
+ const endpointUrl = endpoint.url ?? `${this.webhookUrl}/w/${endpoint.slug}`;
2226
+ const requests = [];
2227
+ const pageSize = normalizeExportPageSize(options.limit);
2228
+ let cursor;
2229
+ while (true) {
2230
+ const remaining = options.limit !== void 0 ? Math.max(0, options.limit - requests.length) : pageSize;
2231
+ if (options.limit !== void 0 && remaining === 0) {
2232
+ break;
2233
+ }
2234
+ const page = await this.requests.listPaginated(endpointSlug, {
2235
+ limit: options.limit !== void 0 ? Math.min(pageSize, remaining) : pageSize,
2236
+ cursor
2237
+ });
2238
+ for (const request of page.items) {
2239
+ if (options.since !== void 0 && request.receivedAt <= options.since) {
2240
+ continue;
2241
+ }
2242
+ requests.push(request);
2243
+ if (options.limit !== void 0 && requests.length >= options.limit) {
2244
+ break;
2245
+ }
2246
+ }
2247
+ if (!page.hasMore || !page.cursor) {
2248
+ break;
2249
+ }
2250
+ if (options.limit !== void 0 && requests.length >= options.limit) {
2251
+ break;
2252
+ }
2253
+ cursor = page.cursor;
2254
+ }
2255
+ if (options.format === "curl") {
2256
+ return buildCurlExport(endpointUrl, requests);
2257
+ }
2258
+ return buildHarExport(endpointUrl, requests, SDK_VERSION);
2259
+ },
1099
2260
  /**
1100
2261
  * Polls for incoming requests until one matches or timeout expires.
1101
2262
  *
@@ -1109,47 +2270,11 @@ var WebhooksCC = class {
1109
2270
  * @throws Error if timeout expires or max iterations (10000) reached
1110
2271
  */
1111
2272
  waitFor: async (endpointSlug, options = {}) => {
1112
- validatePathSegment(endpointSlug, "endpointSlug");
1113
- const timeout = parseDuration(options.timeout ?? 3e4);
1114
- const rawPollInterval = parseDuration(options.pollInterval ?? 500);
1115
- const { match } = options;
1116
- const safePollInterval = Math.max(
1117
- MIN_POLL_INTERVAL,
1118
- Math.min(MAX_POLL_INTERVAL, rawPollInterval)
1119
- );
1120
- const start = Date.now();
1121
- let lastChecked = start - 5 * 60 * 1e3;
1122
- const MAX_ITERATIONS = 1e4;
1123
- let iterations = 0;
1124
- while (Date.now() - start < timeout && iterations < MAX_ITERATIONS) {
1125
- iterations++;
1126
- const checkTime = Date.now();
1127
- try {
1128
- const requests = await this.requests.list(endpointSlug, {
1129
- since: lastChecked,
1130
- limit: 100
1131
- });
1132
- lastChecked = checkTime;
1133
- const matched = match ? requests.find(match) : requests[0];
1134
- if (matched) {
1135
- return matched;
1136
- }
1137
- } catch (error) {
1138
- if (error instanceof WebhooksCCError) {
1139
- if (error instanceof UnauthorizedError) {
1140
- throw error;
1141
- }
1142
- if (error instanceof NotFoundError) {
1143
- throw error;
1144
- }
1145
- if (error.statusCode < 500 && !(error instanceof RateLimitError)) {
1146
- throw error;
1147
- }
1148
- }
1149
- }
1150
- await sleep(safePollInterval);
1151
- }
1152
- throw new TimeoutError(timeout);
2273
+ const [request] = await this.requests.waitForAll(endpointSlug, {
2274
+ ...options,
2275
+ count: 1
2276
+ });
2277
+ return request;
1153
2278
  },
1154
2279
  /**
1155
2280
  * Replay a captured request to a target URL.
@@ -1192,20 +2317,25 @@ var WebhooksCC = class {
1192
2317
  * The connection is closed when the iterator is broken, the signal is aborted,
1193
2318
  * or the timeout expires.
1194
2319
  *
1195
- * No automatic reconnection if the connection drops, the iterator ends.
2320
+ * Reconnection is opt-in and resumes from the last yielded request timestamp.
1196
2321
  */
1197
2322
  subscribe: (slug, options = {}) => {
1198
2323
  validatePathSegment(slug, "slug");
1199
- const { signal, timeout } = options;
2324
+ const { signal, timeout, reconnect = false, onReconnect } = options;
1200
2325
  const baseUrl = this.baseUrl;
1201
2326
  const apiKey = this.apiKey;
1202
2327
  const timeoutMs = timeout !== void 0 ? parseDuration(timeout) : void 0;
2328
+ const maxReconnectAttempts = Math.max(0, Math.floor(options.maxReconnectAttempts ?? 5));
2329
+ const reconnectBackoffMs = normalizeReconnectBackoff(options.reconnectBackoffMs);
1203
2330
  return {
1204
2331
  [Symbol.asyncIterator]() {
1205
2332
  const controller = new AbortController();
1206
2333
  let timeoutId;
1207
2334
  let iterator = null;
1208
2335
  let started = false;
2336
+ let reconnectAttempts = 0;
2337
+ let lastReceivedAt;
2338
+ const seenRequestIds = /* @__PURE__ */ new Set();
1209
2339
  const onAbort = () => controller.abort();
1210
2340
  if (signal) {
1211
2341
  if (signal.aborted) {
@@ -1222,7 +2352,10 @@ var WebhooksCC = class {
1222
2352
  if (signal) signal.removeEventListener("abort", onAbort);
1223
2353
  };
1224
2354
  const start = async () => {
1225
- const url = `${baseUrl}/api/stream/${slug}`;
2355
+ const url = `${baseUrl}${buildStreamPath(
2356
+ slug,
2357
+ lastReceivedAt !== void 0 ? lastReceivedAt - 1 : void 0
2358
+ )}`;
1226
2359
  const connectController = new AbortController();
1227
2360
  const connectTimeout = setTimeout(() => connectController.abort(), 3e4);
1228
2361
  controller.signal.addEventListener("abort", () => connectController.abort(), {
@@ -1238,12 +2371,10 @@ var WebhooksCC = class {
1238
2371
  clearTimeout(connectTimeout);
1239
2372
  }
1240
2373
  if (!response.ok) {
1241
- cleanup();
1242
2374
  const text = await response.text();
1243
2375
  throw mapStatusToError(response.status, text, response);
1244
2376
  }
1245
2377
  if (!response.body) {
1246
- cleanup();
1247
2378
  throw new Error("SSE response has no body");
1248
2379
  }
1249
2380
  controller.signal.addEventListener(
@@ -1256,6 +2387,24 @@ var WebhooksCC = class {
1256
2387
  );
1257
2388
  return parseSSE(response.body);
1258
2389
  };
2390
+ const reconnectStream = async () => {
2391
+ if (!reconnect || reconnectAttempts >= maxReconnectAttempts || controller.signal.aborted) {
2392
+ cleanup();
2393
+ return false;
2394
+ }
2395
+ reconnectAttempts++;
2396
+ try {
2397
+ onReconnect?.(reconnectAttempts);
2398
+ } catch {
2399
+ }
2400
+ await sleep(reconnectBackoffMs * 2 ** (reconnectAttempts - 1));
2401
+ if (controller.signal.aborted) {
2402
+ cleanup();
2403
+ return false;
2404
+ }
2405
+ iterator = await start();
2406
+ return true;
2407
+ };
1259
2408
  return {
1260
2409
  [Symbol.asyncIterator]() {
1261
2410
  return this;
@@ -1269,32 +2418,26 @@ var WebhooksCC = class {
1269
2418
  while (iterator) {
1270
2419
  const { done, value } = await iterator.next();
1271
2420
  if (done) {
1272
- cleanup();
2421
+ iterator = null;
2422
+ if (await reconnectStream()) {
2423
+ continue;
2424
+ }
1273
2425
  return { done: true, value: void 0 };
1274
2426
  }
2427
+ reconnectAttempts = 0;
1275
2428
  if (value.event === "request") {
1276
- try {
1277
- const data = JSON.parse(value.data);
1278
- if (!data.endpointId || !data.method || !data.headers || !data.receivedAt) {
1279
- continue;
1280
- }
1281
- const req = {
1282
- id: data._id ?? data.id,
1283
- endpointId: data.endpointId,
1284
- method: data.method,
1285
- path: data.path ?? "/",
1286
- headers: data.headers,
1287
- body: data.body ?? void 0,
1288
- queryParams: data.queryParams ?? {},
1289
- contentType: data.contentType ?? void 0,
1290
- ip: data.ip ?? "unknown",
1291
- size: data.size ?? 0,
1292
- receivedAt: data.receivedAt
1293
- };
1294
- return { done: false, value: req };
1295
- } catch {
2429
+ const req = parseStreamRequest(value.data);
2430
+ if (!req) {
2431
+ continue;
2432
+ }
2433
+ if (req.id && seenRequestIds.has(req.id)) {
1296
2434
  continue;
1297
2435
  }
2436
+ if (req.id) {
2437
+ seenRequestIds.add(req.id);
2438
+ }
2439
+ lastReceivedAt = req.receivedAt;
2440
+ return { done: false, value: req };
1298
2441
  }
1299
2442
  if (value.event === "timeout" || value.event === "endpoint_deleted") {
1300
2443
  cleanup();
@@ -1304,11 +2447,17 @@ var WebhooksCC = class {
1304
2447
  cleanup();
1305
2448
  return { done: true, value: void 0 };
1306
2449
  } catch (error) {
1307
- cleanup();
1308
- controller.abort();
1309
2450
  if (error instanceof Error && error.name === "AbortError") {
2451
+ cleanup();
2452
+ controller.abort();
1310
2453
  return { done: true, value: void 0 };
1311
2454
  }
2455
+ iterator = null;
2456
+ if (shouldReconnectStreamError(error) && await reconnectStream()) {
2457
+ return this.next();
2458
+ }
2459
+ cleanup();
2460
+ controller.abort();
1312
2461
  throw error;
1313
2462
  }
1314
2463
  },
@@ -1332,67 +2481,100 @@ var WebhooksCC = class {
1332
2481
  this.baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL);
1333
2482
  this.webhookUrl = stripTrailingSlashes(options.webhookUrl ?? DEFAULT_WEBHOOK_URL);
1334
2483
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
2484
+ this.retry = normalizeRetryOptions(options.retry);
1335
2485
  this.hooks = options.hooks ?? {};
1336
2486
  }
1337
2487
  async request(method, path, body) {
1338
2488
  const url = `${this.baseUrl}/api${path}`;
1339
- const controller = new AbortController();
1340
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1341
- const start = Date.now();
1342
- try {
1343
- this.hooks.onRequest?.({ method, url });
1344
- } catch {
1345
- }
1346
- try {
1347
- const response = await fetch(url, {
1348
- method,
1349
- headers: {
1350
- Authorization: `Bearer ${this.apiKey}`,
1351
- "Content-Type": "application/json"
1352
- },
1353
- body: body ? JSON.stringify(body) : void 0,
1354
- signal: controller.signal
1355
- });
1356
- const durationMs = Date.now() - start;
1357
- if (!response.ok) {
1358
- const errorText = await response.text();
1359
- const sanitizedError = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
1360
- const error = mapStatusToError(response.status, sanitizedError, response);
1361
- try {
1362
- this.hooks.onError?.({ method, url, error, durationMs });
1363
- } catch {
1364
- }
1365
- throw error;
1366
- }
2489
+ let attempt = 0;
2490
+ while (true) {
2491
+ attempt++;
2492
+ const controller = new AbortController();
2493
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2494
+ const start = Date.now();
1367
2495
  try {
1368
- this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
2496
+ this.hooks.onRequest?.({ method, url });
1369
2497
  } catch {
1370
2498
  }
1371
- if (response.status === 204 || response.headers.get("content-length") === "0") {
1372
- return void 0;
1373
- }
1374
- const contentType = response.headers.get("content-type");
1375
- if (contentType && !contentType.includes("application/json")) {
1376
- throw new Error(`Unexpected content type: ${contentType}`);
1377
- }
1378
- return response.json();
1379
- } catch (error) {
1380
- if (error instanceof Error && error.name === "AbortError") {
1381
- const timeoutError = new TimeoutError(this.timeout);
2499
+ try {
2500
+ const response = await fetch(url, {
2501
+ method,
2502
+ headers: {
2503
+ Authorization: `Bearer ${this.apiKey}`,
2504
+ "Content-Type": "application/json"
2505
+ },
2506
+ body: body ? JSON.stringify(body) : void 0,
2507
+ signal: controller.signal
2508
+ });
2509
+ const durationMs = Date.now() - start;
2510
+ if (!response.ok) {
2511
+ const errorText = await response.text();
2512
+ const sanitizedError = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
2513
+ const error = mapStatusToError(response.status, sanitizedError, response);
2514
+ try {
2515
+ this.hooks.onError?.({ method, url, error, durationMs });
2516
+ } catch {
2517
+ }
2518
+ if (attempt < this.retry.maxAttempts && this.retry.retryOn.has(response.status)) {
2519
+ const retryDelayMs = response.status === 429 && parseRetryAfterHeader(response) !== void 0 ? (parseRetryAfterHeader(response) ?? 0) * 1e3 : this.retry.backoffMs * 2 ** (attempt - 1);
2520
+ await sleep(retryDelayMs);
2521
+ continue;
2522
+ }
2523
+ throw error;
2524
+ }
1382
2525
  try {
1383
- this.hooks.onError?.({
1384
- method,
1385
- url,
1386
- error: timeoutError,
1387
- durationMs: Date.now() - start
1388
- });
2526
+ this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
1389
2527
  } catch {
1390
2528
  }
1391
- throw timeoutError;
2529
+ if (response.status === 204 || response.headers.get("content-length") === "0") {
2530
+ return void 0;
2531
+ }
2532
+ const contentType = response.headers.get("content-type");
2533
+ if (contentType && !contentType.includes("application/json")) {
2534
+ throw new Error(`Unexpected content type: ${contentType}`);
2535
+ }
2536
+ return response.json();
2537
+ } catch (error) {
2538
+ if (error instanceof WebhooksCCError) {
2539
+ throw error;
2540
+ }
2541
+ if (error instanceof Error && error.name === "AbortError") {
2542
+ const timeoutError = new TimeoutError(this.timeout);
2543
+ try {
2544
+ this.hooks.onError?.({
2545
+ method,
2546
+ url,
2547
+ error: timeoutError,
2548
+ durationMs: Date.now() - start
2549
+ });
2550
+ } catch {
2551
+ }
2552
+ if (attempt < this.retry.maxAttempts) {
2553
+ await sleep(this.retry.backoffMs * 2 ** (attempt - 1));
2554
+ continue;
2555
+ }
2556
+ throw timeoutError;
2557
+ }
2558
+ const isNetworkError = error instanceof Error;
2559
+ if (isNetworkError) {
2560
+ try {
2561
+ this.hooks.onError?.({
2562
+ method,
2563
+ url,
2564
+ error,
2565
+ durationMs: Date.now() - start
2566
+ });
2567
+ } catch {
2568
+ }
2569
+ }
2570
+ if (attempt < this.retry.maxAttempts && isNetworkError) {
2571
+ await sleep(this.retry.backoffMs * 2 ** (attempt - 1));
2572
+ continue;
2573
+ }
2574
+ throw error;
2575
+ } finally {
2576
+ clearTimeout(timeoutId);
1392
2577
  }
1393
- throw error;
1394
- } finally {
1395
- clearTimeout(timeoutId);
1396
2578
  }
1397
2579
  }
1398
2580
  /** Returns a static description of all SDK operations (no API call). */
@@ -1402,7 +2584,12 @@ var WebhooksCC = class {
1402
2584
  endpoints: {
1403
2585
  create: {
1404
2586
  description: "Create a webhook endpoint",
1405
- params: { name: "string?" }
2587
+ params: {
2588
+ name: "string?",
2589
+ ephemeral: "boolean?",
2590
+ expiresIn: "number|string?",
2591
+ mockResponse: "object?"
2592
+ }
1406
2593
  },
1407
2594
  list: {
1408
2595
  description: "List all endpoints",
@@ -1428,18 +2615,38 @@ var WebhooksCC = class {
1428
2615
  description: "Send a provider template webhook with signed headers",
1429
2616
  params: {
1430
2617
  slug: "string",
1431
- provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"',
2618
+ provider: PROVIDER_PARAM_DESCRIPTION,
1432
2619
  template: "string?",
1433
2620
  secret: "string",
1434
2621
  event: "string?"
1435
2622
  }
1436
2623
  }
1437
2624
  },
2625
+ templates: {
2626
+ listProviders: {
2627
+ description: "List supported template providers",
2628
+ params: {}
2629
+ },
2630
+ get: {
2631
+ description: "Get static metadata for a template provider",
2632
+ params: {
2633
+ provider: PROVIDER_PARAM_DESCRIPTION
2634
+ }
2635
+ }
2636
+ },
2637
+ usage: {
2638
+ description: "Get current request usage and remaining quota",
2639
+ params: {}
2640
+ },
2641
+ flow: {
2642
+ description: "Create a fluent webhook flow builder for common capture/verify/replay flows",
2643
+ params: {}
2644
+ },
1438
2645
  sendTo: {
1439
2646
  description: "Send a webhook directly to any URL with optional provider signing",
1440
2647
  params: {
1441
2648
  url: "string",
1442
- provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
2649
+ provider: `${PROVIDER_PARAM_DESCRIPTION}?`,
1443
2650
  secret: "string?",
1444
2651
  body: "unknown?",
1445
2652
  headers: "Record<string, string>?"
@@ -1449,7 +2656,7 @@ var WebhooksCC = class {
1449
2656
  description: "Build a request without sending it \u2014 returns computed method, URL, headers, and body including provider signatures",
1450
2657
  params: {
1451
2658
  url: "string",
1452
- provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
2659
+ provider: `${PROVIDER_PARAM_DESCRIPTION}?`,
1453
2660
  secret: "string?",
1454
2661
  body: "unknown?",
1455
2662
  headers: "Record<string, string>?"
@@ -1460,10 +2667,24 @@ var WebhooksCC = class {
1460
2667
  description: "List captured requests",
1461
2668
  params: { endpointSlug: "string", limit: "number?", since: "number?" }
1462
2669
  },
2670
+ listPaginated: {
2671
+ description: "List captured requests with cursor-based pagination",
2672
+ params: { endpointSlug: "string", limit: "number?", cursor: "string?" }
2673
+ },
1463
2674
  get: {
1464
2675
  description: "Get request by ID",
1465
2676
  params: { requestId: "string" }
1466
2677
  },
2678
+ waitForAll: {
2679
+ description: "Poll until multiple matching requests arrive",
2680
+ params: {
2681
+ endpointSlug: "string",
2682
+ count: "number",
2683
+ timeout: "number|string?",
2684
+ pollInterval: "number|string?",
2685
+ match: "function?"
2686
+ }
2687
+ },
1467
2688
  waitFor: {
1468
2689
  description: "Poll until a matching request arrives",
1469
2690
  params: {
@@ -1474,11 +2695,58 @@ var WebhooksCC = class {
1474
2695
  },
1475
2696
  subscribe: {
1476
2697
  description: "Stream requests via SSE",
1477
- params: { slug: "string", signal: "AbortSignal?", timeout: "number|string?" }
2698
+ params: {
2699
+ slug: "string",
2700
+ signal: "AbortSignal?",
2701
+ timeout: "number|string?",
2702
+ reconnect: "boolean?",
2703
+ maxReconnectAttempts: "number?",
2704
+ reconnectBackoffMs: "number|string?",
2705
+ onReconnect: "function?"
2706
+ }
1478
2707
  },
1479
2708
  replay: {
1480
2709
  description: "Replay a captured request to a URL",
1481
2710
  params: { requestId: "string", targetUrl: "string" }
2711
+ },
2712
+ export: {
2713
+ description: "Export captured requests as HAR or cURL commands",
2714
+ params: {
2715
+ endpointSlug: "string",
2716
+ format: '"har"|"curl"',
2717
+ limit: "number?",
2718
+ since: "number?"
2719
+ }
2720
+ },
2721
+ search: {
2722
+ description: "Search retained requests across path, body, and headers",
2723
+ params: {
2724
+ slug: "string?",
2725
+ method: "string?",
2726
+ q: "string?",
2727
+ from: "number|string?",
2728
+ to: "number|string?",
2729
+ limit: "number?",
2730
+ offset: "number?",
2731
+ order: '"asc"|"desc"?'
2732
+ }
2733
+ },
2734
+ count: {
2735
+ description: "Count retained requests matching search filters",
2736
+ params: {
2737
+ slug: "string?",
2738
+ method: "string?",
2739
+ q: "string?",
2740
+ from: "number|string?",
2741
+ to: "number|string?"
2742
+ }
2743
+ },
2744
+ clear: {
2745
+ description: "Delete captured requests for an endpoint",
2746
+ params: {
2747
+ endpointSlug: "string",
2748
+ before: "number|string?"
2749
+ }
1482
2750
  }
1483
2751
  }
1484
2752
  };
@@ -1494,6 +2762,49 @@ function stripTrailingSlashes(url) {
1494
2762
  }
1495
2763
 
1496
2764
  // src/helpers.ts
2765
+ function getHeaderValue(headers, name) {
2766
+ const target = name.toLowerCase();
2767
+ for (const [key, value] of Object.entries(headers)) {
2768
+ if (key.toLowerCase() === target) {
2769
+ return value;
2770
+ }
2771
+ }
2772
+ return void 0;
2773
+ }
2774
+ function getContentType(request) {
2775
+ return request.contentType ?? getHeaderValue(request.headers, "content-type");
2776
+ }
2777
+ function normalizeContentType(value) {
2778
+ if (!value) {
2779
+ return void 0;
2780
+ }
2781
+ return value.split(";", 1)[0]?.trim().toLowerCase();
2782
+ }
2783
+ function getJsonPathValue(body, path) {
2784
+ const parts = path.split(".");
2785
+ let current = body;
2786
+ for (const part of parts) {
2787
+ if (current === null || current === void 0) {
2788
+ return void 0;
2789
+ }
2790
+ if (Array.isArray(current)) {
2791
+ const index = Number(part);
2792
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) {
2793
+ return void 0;
2794
+ }
2795
+ current = current[index];
2796
+ continue;
2797
+ }
2798
+ if (typeof current !== "object") {
2799
+ return void 0;
2800
+ }
2801
+ if (!Object.prototype.hasOwnProperty.call(current, part)) {
2802
+ return void 0;
2803
+ }
2804
+ current = current[part];
2805
+ }
2806
+ return current;
2807
+ }
1497
2808
  function parseJsonBody(request) {
1498
2809
  if (!request.body) return void 0;
1499
2810
  try {
@@ -1516,6 +2827,50 @@ function matchJsonField(field, value) {
1516
2827
  return body[field] === value;
1517
2828
  };
1518
2829
  }
2830
+ function parseFormBody(request) {
2831
+ if (!request.body) {
2832
+ return void 0;
2833
+ }
2834
+ const contentType = normalizeContentType(getContentType(request));
2835
+ if (contentType !== "application/x-www-form-urlencoded") {
2836
+ return void 0;
2837
+ }
2838
+ const parsed = {};
2839
+ for (const [key, value] of new URLSearchParams(request.body).entries()) {
2840
+ const existing = parsed[key];
2841
+ if (existing === void 0) {
2842
+ parsed[key] = value;
2843
+ continue;
2844
+ }
2845
+ if (Array.isArray(existing)) {
2846
+ existing.push(value);
2847
+ continue;
2848
+ }
2849
+ parsed[key] = [existing, value];
2850
+ }
2851
+ return parsed;
2852
+ }
2853
+ function parseBody(request) {
2854
+ if (!request.body) {
2855
+ return void 0;
2856
+ }
2857
+ const contentType = normalizeContentType(getContentType(request));
2858
+ if (contentType === "application/json" || contentType?.endsWith("+json")) {
2859
+ const parsed = parseJsonBody(request);
2860
+ return parsed === void 0 ? request.body : parsed;
2861
+ }
2862
+ if (contentType === "application/x-www-form-urlencoded") {
2863
+ return parseFormBody(request);
2864
+ }
2865
+ return request.body;
2866
+ }
2867
+ function extractJsonField(request, path) {
2868
+ const body = parseJsonBody(request);
2869
+ if (body === void 0) {
2870
+ return void 0;
2871
+ }
2872
+ return getJsonPathValue(body, path);
2873
+ }
1519
2874
  function isShopifyWebhook(request) {
1520
2875
  return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-shopify-hmac-sha256");
1521
2876
  }
@@ -1531,23 +2886,85 @@ function isPaddleWebhook(request) {
1531
2886
  function isLinearWebhook(request) {
1532
2887
  return Object.keys(request.headers).some((k) => k.toLowerCase() === "linear-signature");
1533
2888
  }
2889
+ function isDiscordWebhook(request) {
2890
+ const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
2891
+ return keys.includes("x-signature-ed25519") && keys.includes("x-signature-timestamp");
2892
+ }
1534
2893
  function isStandardWebhook(request) {
1535
2894
  const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
1536
2895
  return keys.includes("webhook-id") && keys.includes("webhook-timestamp") && keys.includes("webhook-signature");
1537
2896
  }
1538
2897
 
1539
2898
  // src/matchers.ts
2899
+ function getHeaderValue2(headers, name) {
2900
+ const lowerName = name.toLowerCase();
2901
+ for (const [key, value] of Object.entries(headers)) {
2902
+ if (key.toLowerCase() === lowerName) {
2903
+ return value;
2904
+ }
2905
+ }
2906
+ return void 0;
2907
+ }
2908
+ function globToRegExp(pattern) {
2909
+ let source = "^";
2910
+ for (let index = 0; index < pattern.length; index++) {
2911
+ const char = pattern[index];
2912
+ if (char === "*") {
2913
+ if (pattern[index + 1] === "*") {
2914
+ source += ".*";
2915
+ index++;
2916
+ } else {
2917
+ source += "[^/]*";
2918
+ }
2919
+ continue;
2920
+ }
2921
+ source += /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
2922
+ }
2923
+ source += "$";
2924
+ return new RegExp(source);
2925
+ }
2926
+ function isDeepSubset(expected, actual) {
2927
+ if (Array.isArray(expected)) {
2928
+ return Array.isArray(actual) && expected.every((value, index) => isDeepSubset(value, actual[index]));
2929
+ }
2930
+ if (expected && typeof expected === "object") {
2931
+ if (!actual || typeof actual !== "object" || Array.isArray(actual)) {
2932
+ return false;
2933
+ }
2934
+ return Object.entries(expected).every(([key, value]) => {
2935
+ if (!Object.prototype.hasOwnProperty.call(actual, key)) {
2936
+ return false;
2937
+ }
2938
+ return isDeepSubset(value, actual[key]);
2939
+ });
2940
+ }
2941
+ return Object.is(expected, actual);
2942
+ }
1540
2943
  function matchMethod(method) {
1541
2944
  const upper = method.toUpperCase();
1542
2945
  return (request) => request.method.toUpperCase() === upper;
1543
2946
  }
1544
2947
  function matchHeader(name, value) {
1545
- const lowerName = name.toLowerCase();
1546
2948
  return (request) => {
1547
- const entry = Object.entries(request.headers).find(([k]) => k.toLowerCase() === lowerName);
1548
- if (!entry) return false;
2949
+ const headerValue = getHeaderValue2(request.headers, name);
2950
+ if (headerValue === void 0) return false;
1549
2951
  if (value === void 0) return true;
1550
- return entry[1] === value;
2952
+ return headerValue === value;
2953
+ };
2954
+ }
2955
+ function matchPath(pattern) {
2956
+ const regex = globToRegExp(pattern);
2957
+ return (request) => regex.test(request.path);
2958
+ }
2959
+ function matchQueryParam(key, value) {
2960
+ return (request) => {
2961
+ if (!Object.prototype.hasOwnProperty.call(request.queryParams, key)) {
2962
+ return false;
2963
+ }
2964
+ if (value === void 0) {
2965
+ return true;
2966
+ }
2967
+ return request.queryParams[key] === value;
1551
2968
  };
1552
2969
  }
1553
2970
  function matchBodyPath(path, value) {
@@ -1572,6 +2989,23 @@ function matchBodyPath(path, value) {
1572
2989
  return current === value;
1573
2990
  };
1574
2991
  }
2992
+ function matchBodySubset(subset) {
2993
+ return (request) => {
2994
+ const body = parseJsonBody(request);
2995
+ return isDeepSubset(subset, body);
2996
+ };
2997
+ }
2998
+ function matchContentType(type) {
2999
+ const expected = type.trim().toLowerCase();
3000
+ return (request) => {
3001
+ const raw = request.contentType ?? getHeaderValue2(request.headers, "content-type");
3002
+ if (!raw) {
3003
+ return false;
3004
+ }
3005
+ const normalized = raw.trim().toLowerCase();
3006
+ return normalized === expected || normalized.startsWith(`${expected};`);
3007
+ };
3008
+ }
1575
3009
  function matchAll(first, ...rest) {
1576
3010
  const matchers = [first, ...rest];
1577
3011
  return (request) => matchers.every((m) => m(request));
@@ -1580,15 +3014,173 @@ function matchAny(first, ...rest) {
1580
3014
  const matchers = [first, ...rest];
1581
3015
  return (request) => matchers.some((m) => m(request));
1582
3016
  }
3017
+
3018
+ // src/diff.ts
3019
+ function isPlainObject(value) {
3020
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3021
+ }
3022
+ function isJsonBody(body) {
3023
+ if (body.length === 0) {
3024
+ return { valid: false };
3025
+ }
3026
+ try {
3027
+ return { valid: true, value: JSON.parse(body) };
3028
+ } catch {
3029
+ return { valid: false };
3030
+ }
3031
+ }
3032
+ function areEqual(left, right) {
3033
+ if (left === right) {
3034
+ return true;
3035
+ }
3036
+ if (Array.isArray(left) && Array.isArray(right)) {
3037
+ return left.length === right.length && left.every((value, index) => areEqual(value, right[index]));
3038
+ }
3039
+ if (isPlainObject(left) && isPlainObject(right)) {
3040
+ const leftKeys = Object.keys(left);
3041
+ const rightKeys = Object.keys(right);
3042
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => areEqual(left[key], right[key]));
3043
+ }
3044
+ return Number.isNaN(left) && Number.isNaN(right);
3045
+ }
3046
+ function compareJsonValues(left, right, path, changes) {
3047
+ if (areEqual(left, right)) {
3048
+ return;
3049
+ }
3050
+ if (Array.isArray(left) && Array.isArray(right)) {
3051
+ const maxLength = Math.max(left.length, right.length);
3052
+ for (let index = 0; index < maxLength; index++) {
3053
+ const nextPath = path ? `${path}.${index}` : String(index);
3054
+ compareJsonValues(left[index], right[index], nextPath, changes);
3055
+ }
3056
+ return;
3057
+ }
3058
+ if (isPlainObject(left) && isPlainObject(right)) {
3059
+ const keys = [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].sort();
3060
+ for (const key of keys) {
3061
+ const nextPath = path ? `${path}.${key}` : key;
3062
+ compareJsonValues(left[key], right[key], nextPath, changes);
3063
+ }
3064
+ return;
3065
+ }
3066
+ changes[path || "$"] = { left, right };
3067
+ }
3068
+ function formatJsonDiff(changes) {
3069
+ return Object.entries(changes).map(
3070
+ ([path, difference]) => `${path}: ${JSON.stringify(difference.left)} -> ${JSON.stringify(difference.right)}`
3071
+ ).join("\n");
3072
+ }
3073
+ function formatTextDiff(left, right) {
3074
+ const leftLines = left.split("\n");
3075
+ const rightLines = right.split("\n");
3076
+ const maxLength = Math.max(leftLines.length, rightLines.length);
3077
+ const lines = [];
3078
+ for (let index = 0; index < maxLength; index++) {
3079
+ const leftLine = leftLines[index];
3080
+ const rightLine = rightLines[index];
3081
+ if (leftLine === rightLine) {
3082
+ continue;
3083
+ }
3084
+ if (leftLine !== void 0) {
3085
+ lines.push(`- ${leftLine}`);
3086
+ }
3087
+ if (rightLine !== void 0) {
3088
+ lines.push(`+ ${rightLine}`);
3089
+ }
3090
+ }
3091
+ return lines.join("\n");
3092
+ }
3093
+ function normalizeHeaders(headers, ignoredHeaders) {
3094
+ const normalized = {};
3095
+ for (const [key, value] of Object.entries(headers)) {
3096
+ const lowerKey = key.toLowerCase();
3097
+ if (!ignoredHeaders.has(lowerKey)) {
3098
+ normalized[lowerKey] = value;
3099
+ }
3100
+ }
3101
+ return normalized;
3102
+ }
3103
+ function diffHeaders(leftHeaders, rightHeaders, options) {
3104
+ const ignoredHeaders = new Set(
3105
+ (options.ignoreHeaders ?? []).map((header) => header.toLowerCase())
3106
+ );
3107
+ const left = normalizeHeaders(leftHeaders, ignoredHeaders);
3108
+ const right = normalizeHeaders(rightHeaders, ignoredHeaders);
3109
+ const leftKeys = new Set(Object.keys(left));
3110
+ const rightKeys = new Set(Object.keys(right));
3111
+ const added = [...rightKeys].filter((key) => !leftKeys.has(key)).sort();
3112
+ const removed = [...leftKeys].filter((key) => !rightKeys.has(key)).sort();
3113
+ const changed = {};
3114
+ for (const key of [...leftKeys].filter((header) => rightKeys.has(header)).sort()) {
3115
+ if (left[key] !== right[key]) {
3116
+ changed[key] = { left: left[key], right: right[key] };
3117
+ }
3118
+ }
3119
+ if (added.length === 0 && removed.length === 0 && Object.keys(changed).length === 0) {
3120
+ return void 0;
3121
+ }
3122
+ return { added, removed, changed };
3123
+ }
3124
+ function diffBodies(leftBody, rightBody) {
3125
+ const left = leftBody ?? "";
3126
+ const right = rightBody ?? "";
3127
+ if (left === right) {
3128
+ return void 0;
3129
+ }
3130
+ const leftJson = isJsonBody(left);
3131
+ const rightJson = isJsonBody(right);
3132
+ if (leftJson.valid && rightJson.valid) {
3133
+ const changed = {};
3134
+ compareJsonValues(leftJson.value, rightJson.value, "", changed);
3135
+ if (Object.keys(changed).length === 0) {
3136
+ return void 0;
3137
+ }
3138
+ return {
3139
+ type: "json",
3140
+ changed,
3141
+ diff: formatJsonDiff(changed)
3142
+ };
3143
+ }
3144
+ return {
3145
+ type: "text",
3146
+ diff: formatTextDiff(left, right)
3147
+ };
3148
+ }
3149
+ function diffRequests(left, right, options = {}) {
3150
+ const differences = {};
3151
+ if (left.method !== right.method) {
3152
+ differences.method = { left: left.method, right: right.method };
3153
+ }
3154
+ if (left.path !== right.path) {
3155
+ differences.path = { left: left.path, right: right.path };
3156
+ }
3157
+ const headerDiff = diffHeaders(left.headers, right.headers, options);
3158
+ if (headerDiff) {
3159
+ differences.headers = headerDiff;
3160
+ }
3161
+ const bodyDiff = diffBodies(left.body, right.body);
3162
+ if (bodyDiff) {
3163
+ differences.body = bodyDiff;
3164
+ }
3165
+ return {
3166
+ matches: Object.keys(differences).length === 0,
3167
+ differences
3168
+ };
3169
+ }
1583
3170
  // Annotate the CommonJS export names for ESM import in node:
1584
3171
  0 && (module.exports = {
1585
3172
  ApiError,
1586
3173
  NotFoundError,
1587
3174
  RateLimitError,
3175
+ TEMPLATE_METADATA,
1588
3176
  TimeoutError,
1589
3177
  UnauthorizedError,
3178
+ WebhookFlowBuilder,
1590
3179
  WebhooksCC,
1591
3180
  WebhooksCCError,
3181
+ diffRequests,
3182
+ extractJsonField,
3183
+ isDiscordWebhook,
1592
3184
  isGitHubWebhook,
1593
3185
  isLinearWebhook,
1594
3186
  isPaddleWebhook,
@@ -1600,10 +3192,26 @@ function matchAny(first, ...rest) {
1600
3192
  matchAll,
1601
3193
  matchAny,
1602
3194
  matchBodyPath,
3195
+ matchBodySubset,
3196
+ matchContentType,
1603
3197
  matchHeader,
1604
3198
  matchJsonField,
1605
3199
  matchMethod,
3200
+ matchPath,
3201
+ matchQueryParam,
3202
+ parseBody,
1606
3203
  parseDuration,
3204
+ parseFormBody,
1607
3205
  parseJsonBody,
1608
- parseSSE
3206
+ parseSSE,
3207
+ verifyDiscordSignature,
3208
+ verifyGitHubSignature,
3209
+ verifyLinearSignature,
3210
+ verifyPaddleSignature,
3211
+ verifyShopifySignature,
3212
+ verifySignature,
3213
+ verifySlackSignature,
3214
+ verifyStandardWebhookSignature,
3215
+ verifyStripeSignature,
3216
+ verifyTwilioSignature
1609
3217
  });