@webhooks-cc/sdk 0.5.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.mjs CHANGED
@@ -1,74 +1,12 @@
1
- // src/errors.ts
2
- var WebhooksCCError = class extends Error {
3
- constructor(statusCode, message) {
4
- super(message);
5
- this.statusCode = statusCode;
6
- this.name = "WebhooksCCError";
7
- Object.setPrototypeOf(this, new.target.prototype);
8
- }
9
- };
10
- var UnauthorizedError = class extends WebhooksCCError {
11
- constructor(message = "Invalid or missing API key") {
12
- super(401, message);
13
- this.name = "UnauthorizedError";
14
- }
15
- };
16
- var NotFoundError = class extends WebhooksCCError {
17
- constructor(message = "Resource not found") {
18
- super(404, message);
19
- this.name = "NotFoundError";
20
- }
21
- };
22
- var TimeoutError = class extends WebhooksCCError {
23
- constructor(timeoutMs) {
24
- super(0, `Request timed out after ${timeoutMs}ms`);
25
- this.name = "TimeoutError";
26
- }
27
- };
28
- var RateLimitError = class extends WebhooksCCError {
29
- constructor(retryAfter) {
30
- const message = retryAfter ? `Rate limited, retry after ${retryAfter}s` : "Rate limited";
31
- super(429, message);
32
- this.name = "RateLimitError";
33
- this.retryAfter = retryAfter;
34
- }
35
- };
36
-
37
- // src/utils.ts
38
- var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)$/;
39
- function parseDuration(input) {
40
- if (typeof input === "number") {
41
- if (!Number.isFinite(input) || input < 0) {
42
- throw new Error(`Invalid duration: must be a finite non-negative number, got ${input}`);
43
- }
44
- return input;
45
- }
46
- const trimmed = input.trim();
47
- const asNumber = Number(trimmed);
48
- if (!isNaN(asNumber) && trimmed.length > 0) {
49
- if (!Number.isFinite(asNumber) || asNumber < 0) {
50
- throw new Error(`Invalid duration: must be a finite non-negative number, got "${input}"`);
51
- }
52
- return asNumber;
53
- }
54
- const match = DURATION_REGEX.exec(trimmed);
55
- if (!match) {
56
- throw new Error(`Invalid duration: "${input}"`);
57
- }
58
- const value = parseFloat(match[1]);
59
- switch (match[2]) {
60
- case "ms":
61
- return value;
62
- case "s":
63
- return value * 1e3;
64
- case "m":
65
- return value * 6e4;
66
- case "h":
67
- return value * 36e5;
68
- default:
69
- throw new Error(`Invalid duration: "${input}"`);
70
- }
71
- }
1
+ import {
2
+ NotFoundError,
3
+ RateLimitError,
4
+ TimeoutError,
5
+ UnauthorizedError,
6
+ WebhooksCCError,
7
+ diffRequests,
8
+ parseDuration
9
+ } from "./chunk-7IMPSHQY.mjs";
72
10
 
73
11
  // src/sse.ts
74
12
  async function* parseSSE(stream) {
@@ -150,14 +88,95 @@ var DEFAULT_TEMPLATE_BY_PROVIDER = {
150
88
  stripe: "payment_intent.succeeded",
151
89
  github: "push",
152
90
  shopify: "orders/create",
153
- twilio: "messaging.inbound"
91
+ twilio: "messaging.inbound",
92
+ slack: "event_callback",
93
+ paddle: "transaction.completed",
94
+ linear: "issue.create"
154
95
  };
155
96
  var PROVIDER_TEMPLATES = {
156
97
  stripe: ["payment_intent.succeeded", "checkout.session.completed", "invoice.paid"],
157
98
  github: ["push", "pull_request.opened", "ping"],
158
99
  shopify: ["orders/create", "orders/paid", "products/update", "app/uninstalled"],
159
- twilio: ["messaging.inbound", "messaging.status_callback", "voice.incoming_call"]
100
+ twilio: ["messaging.inbound", "messaging.status_callback", "voice.incoming_call"],
101
+ slack: ["event_callback", "slash_command", "url_verification"],
102
+ paddle: ["transaction.completed", "subscription.created", "subscription.updated"],
103
+ linear: ["issue.create", "issue.update", "comment.create"]
160
104
  };
105
+ var TEMPLATE_PROVIDERS = [
106
+ "stripe",
107
+ "github",
108
+ "shopify",
109
+ "twilio",
110
+ "slack",
111
+ "paddle",
112
+ "linear",
113
+ "standard-webhooks"
114
+ ];
115
+ var TEMPLATE_METADATA = Object.freeze({
116
+ stripe: Object.freeze({
117
+ provider: "stripe",
118
+ templates: Object.freeze([...PROVIDER_TEMPLATES.stripe]),
119
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.stripe,
120
+ secretRequired: true,
121
+ signatureHeader: "stripe-signature",
122
+ signatureAlgorithm: "hmac-sha256"
123
+ }),
124
+ github: Object.freeze({
125
+ provider: "github",
126
+ templates: Object.freeze([...PROVIDER_TEMPLATES.github]),
127
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.github,
128
+ secretRequired: true,
129
+ signatureHeader: "x-hub-signature-256",
130
+ signatureAlgorithm: "hmac-sha256"
131
+ }),
132
+ shopify: Object.freeze({
133
+ provider: "shopify",
134
+ templates: Object.freeze([...PROVIDER_TEMPLATES.shopify]),
135
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.shopify,
136
+ secretRequired: true,
137
+ signatureHeader: "x-shopify-hmac-sha256",
138
+ signatureAlgorithm: "hmac-sha256"
139
+ }),
140
+ twilio: Object.freeze({
141
+ provider: "twilio",
142
+ templates: Object.freeze([...PROVIDER_TEMPLATES.twilio]),
143
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.twilio,
144
+ secretRequired: true,
145
+ signatureHeader: "x-twilio-signature",
146
+ signatureAlgorithm: "hmac-sha1"
147
+ }),
148
+ slack: Object.freeze({
149
+ provider: "slack",
150
+ templates: Object.freeze([...PROVIDER_TEMPLATES.slack]),
151
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.slack,
152
+ secretRequired: true,
153
+ signatureHeader: "x-slack-signature",
154
+ signatureAlgorithm: "hmac-sha256"
155
+ }),
156
+ paddle: Object.freeze({
157
+ provider: "paddle",
158
+ templates: Object.freeze([...PROVIDER_TEMPLATES.paddle]),
159
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.paddle,
160
+ secretRequired: true,
161
+ signatureHeader: "paddle-signature",
162
+ signatureAlgorithm: "hmac-sha256"
163
+ }),
164
+ linear: Object.freeze({
165
+ provider: "linear",
166
+ templates: Object.freeze([...PROVIDER_TEMPLATES.linear]),
167
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.linear,
168
+ secretRequired: true,
169
+ signatureHeader: "linear-signature",
170
+ signatureAlgorithm: "hmac-sha256"
171
+ }),
172
+ "standard-webhooks": Object.freeze({
173
+ provider: "standard-webhooks",
174
+ templates: Object.freeze([]),
175
+ secretRequired: true,
176
+ signatureHeader: "webhook-signature",
177
+ signatureAlgorithm: "hmac-sha256"
178
+ })
179
+ });
161
180
  function randomHex(length) {
162
181
  const bytes = new Uint8Array(Math.ceil(length / 2));
163
182
  globalThis.crypto.getRandomValues(bytes);
@@ -560,6 +579,181 @@ function buildTemplatePayload(provider, template, event, now, bodyOverride) {
560
579
  };
561
580
  }
562
581
  if (provider !== "twilio") {
582
+ if (provider === "slack") {
583
+ const eventCallbackPayload = {
584
+ token: randomHex(24),
585
+ team_id: `T${randomHex(8).toUpperCase()}`,
586
+ api_app_id: `A${randomHex(8).toUpperCase()}`,
587
+ type: "event_callback",
588
+ event: {
589
+ type: "app_mention",
590
+ user: `U${randomHex(8).toUpperCase()}`,
591
+ text: "hello from webhooks.cc",
592
+ ts: `${nowSec}.000100`,
593
+ channel: `C${randomHex(8).toUpperCase()}`,
594
+ event_ts: `${nowSec}.000100`
595
+ },
596
+ event_id: `Ev${randomHex(12)}`,
597
+ event_time: nowSec,
598
+ authed_users: [`U${randomHex(8).toUpperCase()}`]
599
+ };
600
+ const verificationPayload = {
601
+ token: randomHex(24),
602
+ challenge: randomHex(16),
603
+ type: "url_verification"
604
+ };
605
+ const defaultSlashCommand = {
606
+ token: randomHex(24),
607
+ team_id: `T${randomHex(8).toUpperCase()}`,
608
+ team_domain: "webhooks-cc",
609
+ channel_id: `C${randomHex(8).toUpperCase()}`,
610
+ channel_name: "general",
611
+ user_id: `U${randomHex(8).toUpperCase()}`,
612
+ user_name: "webhooks-bot",
613
+ command: "/webhook-test",
614
+ text: "hello world",
615
+ response_url: "https://hooks.slack.com/commands/demo",
616
+ trigger_id: randomHex(12)
617
+ };
618
+ if (template === "slash_command") {
619
+ let body2;
620
+ if (bodyOverride === void 0) {
621
+ body2 = formEncode(defaultSlashCommand);
622
+ } else if (typeof bodyOverride === "string") {
623
+ body2 = bodyOverride;
624
+ } else {
625
+ const params = asStringRecord(bodyOverride);
626
+ if (!params) {
627
+ throw new Error("Slack slash_command body override must be a string or an object");
628
+ }
629
+ body2 = formEncode(params);
630
+ }
631
+ return {
632
+ body: body2,
633
+ contentType: "application/x-www-form-urlencoded",
634
+ headers: {
635
+ "user-agent": "Slackbot 1.0 (+https://api.slack.com/robots)"
636
+ }
637
+ };
638
+ }
639
+ const payload = bodyOverride ?? (template === "url_verification" ? verificationPayload : eventCallbackPayload);
640
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
641
+ return {
642
+ body,
643
+ contentType: "application/json",
644
+ headers: {
645
+ "user-agent": "Slackbot 1.0 (+https://api.slack.com/robots)"
646
+ }
647
+ };
648
+ }
649
+ if (provider === "paddle") {
650
+ const payloadByTemplate = {
651
+ "transaction.completed": {
652
+ event_id: randomUuid(),
653
+ event_type: "transaction.completed",
654
+ occurred_at: nowIso,
655
+ notification_id: randomUuid(),
656
+ data: {
657
+ id: `txn_${randomHex(12)}`,
658
+ status: "completed",
659
+ customer_id: `ctm_${randomHex(12)}`,
660
+ currency_code: "USD",
661
+ total: "49.00"
662
+ }
663
+ },
664
+ "subscription.created": {
665
+ event_id: randomUuid(),
666
+ event_type: "subscription.created",
667
+ occurred_at: nowIso,
668
+ notification_id: randomUuid(),
669
+ data: {
670
+ id: `sub_${randomHex(12)}`,
671
+ status: "active",
672
+ customer_id: `ctm_${randomHex(12)}`,
673
+ next_billed_at: nowIso
674
+ }
675
+ },
676
+ "subscription.updated": {
677
+ event_id: randomUuid(),
678
+ event_type: "subscription.updated",
679
+ occurred_at: nowIso,
680
+ notification_id: randomUuid(),
681
+ data: {
682
+ id: `sub_${randomHex(12)}`,
683
+ status: "past_due",
684
+ customer_id: `ctm_${randomHex(12)}`,
685
+ next_billed_at: nowIso
686
+ }
687
+ }
688
+ };
689
+ const payload = bodyOverride ?? payloadByTemplate[template];
690
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
691
+ return {
692
+ body,
693
+ contentType: "application/json",
694
+ headers: {
695
+ "user-agent": "Paddle/1.0"
696
+ }
697
+ };
698
+ }
699
+ if (provider === "linear") {
700
+ const issueId = randomUuid();
701
+ const payloadByTemplate = {
702
+ "issue.create": {
703
+ action: "create",
704
+ type: "Issue",
705
+ webhookTimestamp: nowIso,
706
+ data: {
707
+ id: issueId,
708
+ identifier: "ENG-42",
709
+ title: "Investigate webhook retry regression",
710
+ description: "Created from the webhooks.cc Linear template",
711
+ url: `https://linear.app/webhooks-cc/issue/ENG-42/${issueId}`
712
+ }
713
+ },
714
+ "issue.update": {
715
+ action: "update",
716
+ type: "Issue",
717
+ webhookTimestamp: nowIso,
718
+ data: {
719
+ id: issueId,
720
+ identifier: "ENG-42",
721
+ title: "Investigate webhook retry regression",
722
+ state: {
723
+ name: "In Progress"
724
+ },
725
+ url: `https://linear.app/webhooks-cc/issue/ENG-42/${issueId}`
726
+ }
727
+ },
728
+ "comment.create": {
729
+ action: "create",
730
+ type: "Comment",
731
+ webhookTimestamp: nowIso,
732
+ data: {
733
+ id: randomUuid(),
734
+ body: "Looks good from the webhook sandbox.",
735
+ issue: {
736
+ id: issueId,
737
+ identifier: "ENG-42",
738
+ title: "Investigate webhook retry regression"
739
+ },
740
+ user: {
741
+ id: randomUuid(),
742
+ name: "webhooks.cc bot"
743
+ }
744
+ }
745
+ }
746
+ };
747
+ const payload = bodyOverride ?? payloadByTemplate[template];
748
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
749
+ return {
750
+ body,
751
+ contentType: "application/json",
752
+ headers: {
753
+ "user-agent": "Linear/1.0"
754
+ }
755
+ };
756
+ }
563
757
  throw new Error(`Unsupported provider: ${provider}`);
564
758
  }
565
759
  const defaultTwilioParamsByTemplate = {
@@ -710,6 +904,19 @@ function buildTwilioSignaturePayload(endpointUrl, params) {
710
904
  }
711
905
  return payload;
712
906
  }
907
+ function decodeStandardWebhookSecret(secret) {
908
+ let rawSecret = secret;
909
+ const hadPrefix = rawSecret.startsWith("whsec_");
910
+ if (hadPrefix) {
911
+ rawSecret = rawSecret.slice(6);
912
+ }
913
+ try {
914
+ return fromBase64(rawSecret);
915
+ } catch {
916
+ const raw = hadPrefix ? secret : rawSecret;
917
+ return new TextEncoder().encode(raw);
918
+ }
919
+ }
713
920
  async function buildTemplateSendOptions(endpointUrl, options) {
714
921
  if (options.provider === "standard-webhooks") {
715
922
  const method2 = (options.method ?? "POST").toUpperCase();
@@ -718,11 +925,7 @@ async function buildTemplateSendOptions(endpointUrl, options) {
718
925
  const msgId = options.event ? `msg_${options.event}_${randomHex(8)}` : `msg_${randomHex(16)}`;
719
926
  const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
720
927
  const signingInput = `${msgId}.${timestamp}.${body}`;
721
- let rawSecret = options.secret;
722
- if (rawSecret.startsWith("whsec_")) {
723
- rawSecret = rawSecret.slice(6);
724
- }
725
- const secretBytes = fromBase64(rawSecret);
928
+ const secretBytes = decodeStandardWebhookSecret(options.secret);
726
929
  const signature = await hmacSignRaw("SHA-256", secretBytes, signingInput);
727
930
  return {
728
931
  method: method2,
@@ -770,6 +973,21 @@ async function buildTemplateSendOptions(endpointUrl, options) {
770
973
  const signature = await hmacSign("SHA-1", options.secret, signaturePayload);
771
974
  headers["x-twilio-signature"] = toBase64(signature);
772
975
  }
976
+ if (provider === "slack") {
977
+ const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
978
+ const signature = await hmacSign("SHA-256", options.secret, `v0:${timestamp}:${built.body}`);
979
+ headers["x-slack-request-timestamp"] = String(timestamp);
980
+ headers["x-slack-signature"] = `v0=${toHex(signature)}`;
981
+ }
982
+ if (provider === "paddle") {
983
+ const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
984
+ const signature = await hmacSign("SHA-256", options.secret, `${timestamp}:${built.body}`);
985
+ headers["paddle-signature"] = `ts=${timestamp};h1=${toHex(signature)}`;
986
+ }
987
+ if (provider === "linear") {
988
+ const signature = await hmacSign("SHA-256", options.secret, built.body);
989
+ headers["linear-signature"] = `sha256=${toHex(signature)}`;
990
+ }
773
991
  return {
774
992
  method,
775
993
  headers: {
@@ -780,15 +998,8 @@ async function buildTemplateSendOptions(endpointUrl, options) {
780
998
  };
781
999
  }
782
1000
 
783
- // src/client.ts
784
- var DEFAULT_BASE_URL = "https://webhooks.cc";
785
- var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
786
- var DEFAULT_TIMEOUT = 3e4;
787
- var SDK_VERSION = "0.5.0";
788
- var MIN_POLL_INTERVAL = 10;
789
- var MAX_POLL_INTERVAL = 6e4;
790
- var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
791
- var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
1001
+ // src/request-export.ts
1002
+ var OMITTED_EXPORT_HEADERS = /* @__PURE__ */ new Set([
792
1003
  "host",
793
1004
  "connection",
794
1005
  "content-length",
@@ -796,10 +1007,12 @@ var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
796
1007
  "keep-alive",
797
1008
  "te",
798
1009
  "trailer",
799
- "upgrade"
800
- ]);
801
- var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "proxy-authorization", "set-cookie"]);
802
- var PROXY_HEADERS = /* @__PURE__ */ new Set([
1010
+ "upgrade",
1011
+ "authorization",
1012
+ "cookie",
1013
+ "proxy-authorization",
1014
+ "set-cookie",
1015
+ "accept-encoding",
803
1016
  "cdn-loop",
804
1017
  "cf-connecting-ip",
805
1018
  "cf-ipcountry",
@@ -812,81 +1025,861 @@ var PROXY_HEADERS = /* @__PURE__ */ new Set([
812
1025
  "x-real-ip",
813
1026
  "true-client-ip"
814
1027
  ]);
815
- var ApiError = WebhooksCCError;
816
- function mapStatusToError(status, message, response) {
817
- const isGeneric = message.length < 30;
818
- switch (status) {
819
- case 401: {
820
- const hint = isGeneric ? `${message} \u2014 Get an API key at https://webhooks.cc/account` : message;
821
- return new UnauthorizedError(hint);
1028
+ var VALID_HTTP_METHOD = /^[A-Z]+$/;
1029
+ function normalizePath(path) {
1030
+ return path.startsWith("/") ? path : `/${path}`;
1031
+ }
1032
+ function buildRequestUrl(endpointUrl, request) {
1033
+ const url = new URL(`${endpointUrl}${normalizePath(request.path)}`);
1034
+ for (const [key, value] of Object.entries(request.queryParams)) {
1035
+ url.searchParams.set(key, value);
1036
+ }
1037
+ return url.toString();
1038
+ }
1039
+ function shouldIncludeHeader(name) {
1040
+ return !OMITTED_EXPORT_HEADERS.has(name.toLowerCase());
1041
+ }
1042
+ function escapeForShellDoubleQuotes(value) {
1043
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
1044
+ }
1045
+ function escapeForShellSingleQuotes(value) {
1046
+ return value.replace(/'/g, "'\\''");
1047
+ }
1048
+ function buildCurlExport(endpointUrl, requests) {
1049
+ return requests.map((request) => {
1050
+ const method = VALID_HTTP_METHOD.test(request.method) ? request.method : "GET";
1051
+ const parts = [`curl -X ${method}`];
1052
+ for (const [key, value] of Object.entries(request.headers)) {
1053
+ if (!shouldIncludeHeader(key)) {
1054
+ continue;
1055
+ }
1056
+ parts.push(`-H "${escapeForShellDoubleQuotes(key)}: ${escapeForShellDoubleQuotes(value)}"`);
822
1057
  }
823
- case 404: {
824
- const hint = isGeneric ? `${message} \u2014 Use client.endpoints.list() to see available endpoints.` : message;
825
- return new NotFoundError(hint);
1058
+ if (request.body) {
1059
+ parts.push(`-d '${escapeForShellSingleQuotes(request.body)}'`);
826
1060
  }
827
- case 429: {
828
- const retryAfterHeader = response.headers.get("retry-after");
829
- let retryAfter;
830
- if (retryAfterHeader) {
831
- const parsed = parseInt(retryAfterHeader, 10);
832
- retryAfter = Number.isNaN(parsed) ? void 0 : parsed;
833
- }
834
- return new RateLimitError(retryAfter);
1061
+ parts.push(`"${escapeForShellDoubleQuotes(buildRequestUrl(endpointUrl, request))}"`);
1062
+ return parts.join(" \\\n ");
1063
+ });
1064
+ }
1065
+ function buildHarExport(endpointUrl, requests, creatorVersion) {
1066
+ return {
1067
+ log: {
1068
+ version: "1.2",
1069
+ creator: {
1070
+ name: "@webhooks-cc/sdk",
1071
+ version: creatorVersion
1072
+ },
1073
+ entries: requests.map((request) => {
1074
+ const contentType = request.contentType ?? "application/octet-stream";
1075
+ return {
1076
+ startedDateTime: new Date(request.receivedAt).toISOString(),
1077
+ time: 0,
1078
+ request: {
1079
+ method: request.method,
1080
+ url: buildRequestUrl(endpointUrl, request),
1081
+ httpVersion: "HTTP/1.1",
1082
+ headers: Object.entries(request.headers).filter(([key]) => shouldIncludeHeader(key)).map(([name, value]) => ({ name, value })),
1083
+ queryString: Object.entries(request.queryParams).map(([name, value]) => ({
1084
+ name,
1085
+ value
1086
+ })),
1087
+ headersSize: -1,
1088
+ bodySize: request.body ? new TextEncoder().encode(request.body).length : 0,
1089
+ ...request.body ? {
1090
+ postData: {
1091
+ mimeType: contentType,
1092
+ text: request.body
1093
+ }
1094
+ } : {}
1095
+ },
1096
+ response: {
1097
+ status: 0,
1098
+ statusText: "",
1099
+ httpVersion: "HTTP/1.1",
1100
+ headers: [],
1101
+ cookies: [],
1102
+ content: {
1103
+ size: 0,
1104
+ mimeType: "x-unknown"
1105
+ },
1106
+ redirectURL: "",
1107
+ headersSize: -1,
1108
+ bodySize: -1
1109
+ },
1110
+ cache: {},
1111
+ timings: {
1112
+ send: 0,
1113
+ wait: 0,
1114
+ receive: 0
1115
+ }
1116
+ };
1117
+ })
835
1118
  }
836
- default:
837
- return new WebhooksCCError(status, message);
1119
+ };
1120
+ }
1121
+
1122
+ // src/verify.ts
1123
+ function requireSecret(secret, functionName) {
1124
+ if (!secret || typeof secret !== "string") {
1125
+ throw new Error(`${functionName} requires a non-empty secret`);
838
1126
  }
839
1127
  }
840
- var SAFE_PATH_SEGMENT_REGEX = /^[a-zA-Z0-9_-]+$/;
841
- function validatePathSegment(segment, name) {
842
- if (!SAFE_PATH_SEGMENT_REGEX.test(segment)) {
843
- throw new Error(
844
- `Invalid ${name}: must contain only alphanumeric characters, hyphens, and underscores`
1128
+ function getHeader(headers, name) {
1129
+ const target = name.toLowerCase();
1130
+ for (const [key, value] of Object.entries(headers)) {
1131
+ if (key.toLowerCase() === target) {
1132
+ return value;
1133
+ }
1134
+ }
1135
+ return void 0;
1136
+ }
1137
+ function normalizeBody(body) {
1138
+ return body ?? "";
1139
+ }
1140
+ function hexToBytes(hex) {
1141
+ const normalized = hex.trim().toLowerCase();
1142
+ if (!/^[a-f0-9]+$/.test(normalized) || normalized.length % 2 !== 0) {
1143
+ throw new Error("Expected a hex-encoded value");
1144
+ }
1145
+ const bytes = new Uint8Array(normalized.length / 2);
1146
+ for (let index = 0; index < normalized.length; index += 2) {
1147
+ bytes[index / 2] = parseInt(normalized.slice(index, index + 2), 16);
1148
+ }
1149
+ return bytes;
1150
+ }
1151
+ function timingSafeEqual(left, right) {
1152
+ if (left.length !== right.length) {
1153
+ return false;
1154
+ }
1155
+ let mismatch = 0;
1156
+ for (let i = 0; i < left.length; i++) {
1157
+ mismatch |= left.charCodeAt(i) ^ right.charCodeAt(i);
1158
+ }
1159
+ return mismatch === 0;
1160
+ }
1161
+ function parseStripeHeader(signatureHeader) {
1162
+ if (!signatureHeader) {
1163
+ return null;
1164
+ }
1165
+ let timestamp;
1166
+ const signatures = [];
1167
+ for (const part of signatureHeader.split(",")) {
1168
+ const trimmed = part.trim();
1169
+ if (!trimmed) {
1170
+ continue;
1171
+ }
1172
+ const separatorIndex = trimmed.indexOf("=");
1173
+ if (separatorIndex === -1) {
1174
+ continue;
1175
+ }
1176
+ const key = trimmed.slice(0, separatorIndex).trim();
1177
+ const value = trimmed.slice(separatorIndex + 1).trim();
1178
+ if (!value) {
1179
+ continue;
1180
+ }
1181
+ if (key === "t") {
1182
+ timestamp = value;
1183
+ continue;
1184
+ }
1185
+ if (key === "v1") {
1186
+ signatures.push(value.toLowerCase());
1187
+ }
1188
+ }
1189
+ if (!timestamp || signatures.length === 0) {
1190
+ return null;
1191
+ }
1192
+ return { timestamp, signatures };
1193
+ }
1194
+ function parseStandardSignatures(signatureHeader) {
1195
+ if (!signatureHeader) {
1196
+ return [];
1197
+ }
1198
+ const matches = Array.from(
1199
+ signatureHeader.matchAll(/v1,([A-Za-z0-9+/=]+)/g),
1200
+ (match) => match[1]
1201
+ );
1202
+ if (matches.length > 0) {
1203
+ return matches;
1204
+ }
1205
+ const [version, signature] = signatureHeader.split(",", 2);
1206
+ if (version?.trim() === "v1" && signature?.trim()) {
1207
+ return [signature.trim()];
1208
+ }
1209
+ return [];
1210
+ }
1211
+ function parsePaddleSignature(signatureHeader) {
1212
+ if (!signatureHeader) {
1213
+ return null;
1214
+ }
1215
+ let timestamp;
1216
+ const signatures = [];
1217
+ for (const part of signatureHeader.split(/[;,]/)) {
1218
+ const trimmed = part.trim();
1219
+ if (!trimmed) {
1220
+ continue;
1221
+ }
1222
+ const separatorIndex = trimmed.indexOf("=");
1223
+ if (separatorIndex === -1) {
1224
+ continue;
1225
+ }
1226
+ const key = trimmed.slice(0, separatorIndex).trim().toLowerCase();
1227
+ const value = trimmed.slice(separatorIndex + 1).trim();
1228
+ if (!value) {
1229
+ continue;
1230
+ }
1231
+ if (key === "ts") {
1232
+ timestamp = value;
1233
+ continue;
1234
+ }
1235
+ if (key === "h1") {
1236
+ signatures.push(value.toLowerCase());
1237
+ }
1238
+ }
1239
+ if (!timestamp || signatures.length === 0) {
1240
+ return null;
1241
+ }
1242
+ return { timestamp, signatures };
1243
+ }
1244
+ function toTwilioParams(body) {
1245
+ if (body === void 0) {
1246
+ return [];
1247
+ }
1248
+ if (typeof body === "string") {
1249
+ return Array.from(new URLSearchParams(body).entries());
1250
+ }
1251
+ return Object.entries(body).map(([key, value]) => [key, String(value)]);
1252
+ }
1253
+ async function verifyStripeSignature(body, signatureHeader, secret) {
1254
+ requireSecret(secret, "verifyStripeSignature");
1255
+ const parsed = parseStripeHeader(signatureHeader);
1256
+ if (!parsed) {
1257
+ return false;
1258
+ }
1259
+ const expected = toHex(
1260
+ await hmacSign("SHA-256", secret, `${parsed.timestamp}.${normalizeBody(body)}`)
1261
+ ).toLowerCase();
1262
+ return parsed.signatures.some((signature) => timingSafeEqual(signature, expected));
1263
+ }
1264
+ async function verifyGitHubSignature(body, signatureHeader, secret) {
1265
+ requireSecret(secret, "verifyGitHubSignature");
1266
+ if (!signatureHeader) {
1267
+ return false;
1268
+ }
1269
+ const match = signatureHeader.trim().match(/^sha256=(.+)$/i);
1270
+ if (!match) {
1271
+ return false;
1272
+ }
1273
+ const expected = toHex(await hmacSign("SHA-256", secret, normalizeBody(body))).toLowerCase();
1274
+ return timingSafeEqual(match[1].toLowerCase(), expected);
1275
+ }
1276
+ async function verifyShopifySignature(body, signatureHeader, secret) {
1277
+ requireSecret(secret, "verifyShopifySignature");
1278
+ if (!signatureHeader) {
1279
+ return false;
1280
+ }
1281
+ const expected = toBase64(await hmacSign("SHA-256", secret, normalizeBody(body)));
1282
+ return timingSafeEqual(signatureHeader.trim(), expected);
1283
+ }
1284
+ async function verifyTwilioSignature(url, body, signatureHeader, secret) {
1285
+ requireSecret(secret, "verifyTwilioSignature");
1286
+ if (!url) {
1287
+ throw new Error("verifyTwilioSignature requires the signed URL");
1288
+ }
1289
+ if (!signatureHeader) {
1290
+ return false;
1291
+ }
1292
+ const expected = toBase64(
1293
+ await hmacSign("SHA-1", secret, buildTwilioSignaturePayload(url, toTwilioParams(body)))
1294
+ );
1295
+ return timingSafeEqual(signatureHeader.trim(), expected);
1296
+ }
1297
+ async function verifySlackSignature(body, headers, secret) {
1298
+ requireSecret(secret, "verifySlackSignature");
1299
+ const signatureHeader = getHeader(headers, "x-slack-signature");
1300
+ const timestamp = getHeader(headers, "x-slack-request-timestamp");
1301
+ if (!signatureHeader || !timestamp) {
1302
+ return false;
1303
+ }
1304
+ const match = signatureHeader.trim().match(/^v0=(.+)$/i);
1305
+ if (!match) {
1306
+ return false;
1307
+ }
1308
+ const expected = toHex(
1309
+ await hmacSign("SHA-256", secret, `v0:${timestamp}:${normalizeBody(body)}`)
1310
+ ).toLowerCase();
1311
+ return timingSafeEqual(match[1].toLowerCase(), expected);
1312
+ }
1313
+ async function verifyPaddleSignature(body, signatureHeader, secret) {
1314
+ requireSecret(secret, "verifyPaddleSignature");
1315
+ const parsed = parsePaddleSignature(signatureHeader);
1316
+ if (!parsed) {
1317
+ return false;
1318
+ }
1319
+ const expected = toHex(
1320
+ await hmacSign("SHA-256", secret, `${parsed.timestamp}:${normalizeBody(body)}`)
1321
+ ).toLowerCase();
1322
+ return parsed.signatures.some((signature) => timingSafeEqual(signature, expected));
1323
+ }
1324
+ async function verifyLinearSignature(body, signatureHeader, secret) {
1325
+ requireSecret(secret, "verifyLinearSignature");
1326
+ if (!signatureHeader) {
1327
+ return false;
1328
+ }
1329
+ const match = signatureHeader.trim().match(/^(?:sha256=)?(.+)$/i);
1330
+ if (!match) {
1331
+ return false;
1332
+ }
1333
+ const expected = toHex(await hmacSign("SHA-256", secret, normalizeBody(body))).toLowerCase();
1334
+ return timingSafeEqual(match[1].toLowerCase(), expected);
1335
+ }
1336
+ async function verifyDiscordSignature(body, headers, publicKey) {
1337
+ if (!publicKey || typeof publicKey !== "string") {
1338
+ throw new Error("verifyDiscordSignature requires a non-empty public key");
1339
+ }
1340
+ const signatureHeader = getHeader(headers, "x-signature-ed25519");
1341
+ const timestamp = getHeader(headers, "x-signature-timestamp");
1342
+ if (!signatureHeader || !timestamp) {
1343
+ return false;
1344
+ }
1345
+ if (!globalThis.crypto?.subtle) {
1346
+ throw new Error("crypto.subtle is required for Discord signature verification");
1347
+ }
1348
+ try {
1349
+ const publicKeyBytes = hexToBytes(publicKey);
1350
+ const signatureBytes = hexToBytes(signatureHeader);
1351
+ const publicKeyData = new Uint8Array(publicKeyBytes.byteLength);
1352
+ publicKeyData.set(publicKeyBytes);
1353
+ const signatureData = new Uint8Array(signatureBytes.byteLength);
1354
+ signatureData.set(signatureBytes);
1355
+ const key = await globalThis.crypto.subtle.importKey(
1356
+ "raw",
1357
+ publicKeyData,
1358
+ { name: "Ed25519" },
1359
+ false,
1360
+ ["verify"]
845
1361
  );
1362
+ return await globalThis.crypto.subtle.verify(
1363
+ "Ed25519",
1364
+ key,
1365
+ signatureData,
1366
+ new TextEncoder().encode(`${timestamp}${normalizeBody(body)}`)
1367
+ );
1368
+ } catch {
1369
+ return false;
846
1370
  }
847
1371
  }
848
- var WebhooksCC = class {
849
- constructor(options) {
850
- this.endpoints = {
851
- create: async (options = {}) => {
852
- return this.request("POST", "/endpoints", options);
853
- },
854
- list: async () => {
855
- return this.request("GET", "/endpoints");
856
- },
857
- get: async (slug) => {
858
- validatePathSegment(slug, "slug");
859
- return this.request("GET", `/endpoints/${slug}`);
860
- },
861
- update: async (slug, options) => {
862
- validatePathSegment(slug, "slug");
863
- if (options.mockResponse && options.mockResponse !== null) {
864
- const { status } = options.mockResponse;
865
- if (!Number.isInteger(status) || status < 100 || status > 599) {
866
- throw new Error(`Invalid mock response status: ${status}. Must be an integer 100-599.`);
867
- }
868
- }
869
- return this.request("PATCH", `/endpoints/${slug}`, options);
870
- },
871
- delete: async (slug) => {
872
- validatePathSegment(slug, "slug");
873
- await this.request("DELETE", `/endpoints/${slug}`);
874
- },
875
- send: async (slug, options = {}) => {
876
- validatePathSegment(slug, "slug");
877
- const rawMethod = (options.method ?? "POST").toUpperCase();
878
- if (!ALLOWED_METHODS.has(rawMethod)) {
879
- throw new Error(
880
- `Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
881
- );
882
- }
883
- const { headers = {}, body } = options;
884
- const method = rawMethod;
885
- const url = `${this.webhookUrl}/w/${slug}`;
886
- const fetchHeaders = { ...headers };
887
- const hasContentType = Object.keys(fetchHeaders).some(
888
- (k) => k.toLowerCase() === "content-type"
889
- );
1372
+ async function verifyStandardWebhookSignature(body, headers, secret) {
1373
+ requireSecret(secret, "verifyStandardWebhookSignature");
1374
+ const messageId = getHeader(headers, "webhook-id");
1375
+ const timestamp = getHeader(headers, "webhook-timestamp");
1376
+ const signatureHeader = getHeader(headers, "webhook-signature");
1377
+ if (!messageId || !timestamp || !signatureHeader) {
1378
+ return false;
1379
+ }
1380
+ const expected = toBase64(
1381
+ await hmacSignRaw(
1382
+ "SHA-256",
1383
+ decodeStandardWebhookSecret(secret),
1384
+ `${messageId}.${timestamp}.${normalizeBody(body)}`
1385
+ )
1386
+ );
1387
+ return parseStandardSignatures(signatureHeader).some(
1388
+ (signature) => timingSafeEqual(signature, expected)
1389
+ );
1390
+ }
1391
+ async function verifySignature(request, options) {
1392
+ let valid = false;
1393
+ if (options.provider === "stripe") {
1394
+ valid = await verifyStripeSignature(
1395
+ request.body,
1396
+ getHeader(request.headers, "stripe-signature"),
1397
+ options.secret
1398
+ );
1399
+ }
1400
+ if (options.provider === "github") {
1401
+ valid = await verifyGitHubSignature(
1402
+ request.body,
1403
+ getHeader(request.headers, "x-hub-signature-256"),
1404
+ options.secret
1405
+ );
1406
+ }
1407
+ if (options.provider === "shopify") {
1408
+ valid = await verifyShopifySignature(
1409
+ request.body,
1410
+ getHeader(request.headers, "x-shopify-hmac-sha256"),
1411
+ options.secret
1412
+ );
1413
+ }
1414
+ if (options.provider === "twilio") {
1415
+ if (!options.url) {
1416
+ throw new Error('verifySignature for provider "twilio" requires options.url');
1417
+ }
1418
+ valid = await verifyTwilioSignature(
1419
+ options.url,
1420
+ request.body,
1421
+ getHeader(request.headers, "x-twilio-signature"),
1422
+ options.secret
1423
+ );
1424
+ }
1425
+ if (options.provider === "slack") {
1426
+ valid = await verifySlackSignature(request.body, request.headers, options.secret);
1427
+ }
1428
+ if (options.provider === "paddle") {
1429
+ valid = await verifyPaddleSignature(
1430
+ request.body,
1431
+ getHeader(request.headers, "paddle-signature"),
1432
+ options.secret
1433
+ );
1434
+ }
1435
+ if (options.provider === "linear") {
1436
+ valid = await verifyLinearSignature(
1437
+ request.body,
1438
+ getHeader(request.headers, "linear-signature"),
1439
+ options.secret
1440
+ );
1441
+ }
1442
+ if (options.provider === "discord") {
1443
+ valid = await verifyDiscordSignature(request.body, request.headers, options.publicKey);
1444
+ }
1445
+ if (options.provider === "standard-webhooks") {
1446
+ valid = await verifyStandardWebhookSignature(request.body, request.headers, options.secret);
1447
+ }
1448
+ return { valid };
1449
+ }
1450
+
1451
+ // src/flow.ts
1452
+ var WebhookFlowBuilder = class {
1453
+ constructor(client) {
1454
+ this.client = client;
1455
+ this.createOptions = {};
1456
+ this.deleteAfterRun = false;
1457
+ }
1458
+ createEndpoint(options = {}) {
1459
+ this.createOptions = { ...this.createOptions, ...options };
1460
+ return this;
1461
+ }
1462
+ setMock(mockResponse) {
1463
+ this.mockResponse = mockResponse;
1464
+ return this;
1465
+ }
1466
+ send(options = {}) {
1467
+ this.sendStep = { kind: "send", options };
1468
+ return this;
1469
+ }
1470
+ sendTemplate(options) {
1471
+ this.sendStep = { kind: "sendTemplate", options };
1472
+ return this;
1473
+ }
1474
+ waitForCapture(options = {}) {
1475
+ this.waitOptions = options;
1476
+ return this;
1477
+ }
1478
+ verifySignature(options) {
1479
+ this.verificationOptions = options;
1480
+ return this;
1481
+ }
1482
+ replayTo(targetUrl) {
1483
+ this.replayTargetUrl = targetUrl;
1484
+ return this;
1485
+ }
1486
+ cleanup() {
1487
+ this.deleteAfterRun = true;
1488
+ return this;
1489
+ }
1490
+ async run() {
1491
+ let endpoint;
1492
+ let result;
1493
+ let request;
1494
+ try {
1495
+ endpoint = await this.client.endpoints.create(this.createOptions);
1496
+ if (this.mockResponse !== void 0) {
1497
+ await this.client.endpoints.update(endpoint.slug, {
1498
+ mockResponse: this.mockResponse
1499
+ });
1500
+ }
1501
+ if (this.sendStep?.kind === "send") {
1502
+ await this.client.endpoints.send(endpoint.slug, this.sendStep.options);
1503
+ }
1504
+ if (this.sendStep?.kind === "sendTemplate") {
1505
+ await this.client.endpoints.sendTemplate(endpoint.slug, this.sendStep.options);
1506
+ }
1507
+ if (this.waitOptions) {
1508
+ request = await this.client.requests.waitFor(endpoint.slug, this.waitOptions);
1509
+ }
1510
+ let verification;
1511
+ if (this.verificationOptions) {
1512
+ if (!request) {
1513
+ throw new Error("Flow verification requires waitForCapture() to run first");
1514
+ }
1515
+ const resolvedUrl = typeof this.verificationOptions.url === "function" ? this.verificationOptions.url(endpoint, request) : this.verificationOptions.url ?? (this.verificationOptions.provider === "twilio" ? endpoint.url : void 0);
1516
+ if (this.verificationOptions.provider === "discord") {
1517
+ verification = await verifySignature(request, {
1518
+ provider: "discord",
1519
+ publicKey: this.verificationOptions.publicKey
1520
+ });
1521
+ } else {
1522
+ verification = await verifySignature(request, {
1523
+ provider: this.verificationOptions.provider,
1524
+ secret: this.verificationOptions.secret,
1525
+ ...resolvedUrl ? { url: resolvedUrl } : {}
1526
+ });
1527
+ }
1528
+ }
1529
+ let replayResponse;
1530
+ if (this.replayTargetUrl) {
1531
+ if (!request) {
1532
+ throw new Error("Flow replay requires waitForCapture() to run first");
1533
+ }
1534
+ replayResponse = await this.client.requests.replay(request.id, this.replayTargetUrl);
1535
+ }
1536
+ result = {
1537
+ endpoint,
1538
+ request,
1539
+ verification,
1540
+ replayResponse,
1541
+ cleanedUp: false
1542
+ };
1543
+ return result;
1544
+ } finally {
1545
+ if (this.deleteAfterRun && endpoint) {
1546
+ try {
1547
+ await this.client.endpoints.delete(endpoint.slug);
1548
+ if (result) {
1549
+ result.cleanedUp = true;
1550
+ }
1551
+ } catch (error) {
1552
+ if (error instanceof NotFoundError) {
1553
+ if (result) {
1554
+ result.cleanedUp = true;
1555
+ }
1556
+ }
1557
+ }
1558
+ }
1559
+ }
1560
+ }
1561
+ };
1562
+
1563
+ // src/client.ts
1564
+ var DEFAULT_BASE_URL = "https://webhooks.cc";
1565
+ var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
1566
+ var DEFAULT_TIMEOUT = 3e4;
1567
+ var DEFAULT_RETRY_ATTEMPTS = 1;
1568
+ var DEFAULT_RETRY_BACKOFF_MS = 1e3;
1569
+ var DEFAULT_RETRY_STATUSES = [429, 500, 502, 503, 504];
1570
+ var SDK_VERSION = "0.6.0";
1571
+ var WAIT_FOR_LOOKBACK_MS = 5 * 60 * 1e3;
1572
+ var DEFAULT_EXPORT_PAGE_SIZE = 100;
1573
+ var PROVIDER_PARAM_DESCRIPTION = TEMPLATE_PROVIDERS.map((provider) => `"${provider}"`).join("|");
1574
+ var MIN_POLL_INTERVAL = 10;
1575
+ var MAX_POLL_INTERVAL = 6e4;
1576
+ var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
1577
+ var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
1578
+ "host",
1579
+ "connection",
1580
+ "content-length",
1581
+ "transfer-encoding",
1582
+ "keep-alive",
1583
+ "te",
1584
+ "trailer",
1585
+ "upgrade"
1586
+ ]);
1587
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "proxy-authorization", "set-cookie"]);
1588
+ var PROXY_HEADERS = /* @__PURE__ */ new Set([
1589
+ "cdn-loop",
1590
+ "cf-connecting-ip",
1591
+ "cf-ipcountry",
1592
+ "cf-ray",
1593
+ "cf-visitor",
1594
+ "via",
1595
+ "x-forwarded-for",
1596
+ "x-forwarded-host",
1597
+ "x-forwarded-proto",
1598
+ "x-real-ip",
1599
+ "true-client-ip"
1600
+ ]);
1601
+ var ApiError = WebhooksCCError;
1602
+ function mapStatusToError(status, message, response) {
1603
+ const isGeneric = message.length < 30;
1604
+ switch (status) {
1605
+ case 401: {
1606
+ const hint = isGeneric ? `${message} \u2014 Get an API key at https://webhooks.cc/account` : message;
1607
+ return new UnauthorizedError(hint);
1608
+ }
1609
+ case 404: {
1610
+ const hint = isGeneric ? `${message} \u2014 Use client.endpoints.list() to see available endpoints.` : message;
1611
+ return new NotFoundError(hint);
1612
+ }
1613
+ case 429: {
1614
+ const retryAfterHeader = response.headers.get("retry-after");
1615
+ let retryAfter;
1616
+ if (retryAfterHeader) {
1617
+ const parsed = parseInt(retryAfterHeader, 10);
1618
+ retryAfter = Number.isNaN(parsed) ? void 0 : parsed;
1619
+ }
1620
+ return new RateLimitError(retryAfter);
1621
+ }
1622
+ default:
1623
+ return new WebhooksCCError(status, message);
1624
+ }
1625
+ }
1626
+ var SAFE_PATH_SEGMENT_REGEX = /^[a-zA-Z0-9_-]+$/;
1627
+ function validatePathSegment(segment, name) {
1628
+ if (!SAFE_PATH_SEGMENT_REGEX.test(segment)) {
1629
+ throw new Error(
1630
+ `Invalid ${name}: must contain only alphanumeric characters, hyphens, and underscores`
1631
+ );
1632
+ }
1633
+ }
1634
+ function resolveTimestampFilter(value, now) {
1635
+ if (typeof value === "number") {
1636
+ return value;
1637
+ }
1638
+ const trimmed = value.trim();
1639
+ if (trimmed.length === 0) {
1640
+ throw new Error("Invalid timestamp filter: value cannot be empty");
1641
+ }
1642
+ const asNumber = Number(trimmed);
1643
+ if (!Number.isNaN(asNumber)) {
1644
+ return asNumber;
1645
+ }
1646
+ return now - parseDuration(trimmed);
1647
+ }
1648
+ function buildSearchQuery(filters, includePagination) {
1649
+ const params = new URLSearchParams();
1650
+ const now = Date.now();
1651
+ if (filters.slug !== void 0) {
1652
+ validatePathSegment(filters.slug, "slug");
1653
+ params.set("slug", filters.slug);
1654
+ }
1655
+ if (filters.method !== void 0) {
1656
+ params.set("method", filters.method);
1657
+ }
1658
+ if (filters.q !== void 0) {
1659
+ params.set("q", filters.q);
1660
+ }
1661
+ if (filters.from !== void 0) {
1662
+ params.set("from", String(resolveTimestampFilter(filters.from, now)));
1663
+ }
1664
+ if (filters.to !== void 0) {
1665
+ params.set("to", String(resolveTimestampFilter(filters.to, now)));
1666
+ }
1667
+ if (includePagination && filters.limit !== void 0) {
1668
+ params.set("limit", String(filters.limit));
1669
+ }
1670
+ if (includePagination && filters.offset !== void 0) {
1671
+ params.set("offset", String(filters.offset));
1672
+ }
1673
+ if (includePagination && filters.order !== void 0) {
1674
+ params.set("order", filters.order);
1675
+ }
1676
+ const query = params.toString();
1677
+ return query ? `?${query}` : "";
1678
+ }
1679
+ function buildClearQuery(options = {}) {
1680
+ const params = new URLSearchParams();
1681
+ if (options.before !== void 0) {
1682
+ params.set("before", String(resolveTimestampFilter(options.before, Date.now())));
1683
+ }
1684
+ const query = params.toString();
1685
+ return query ? `?${query}` : "";
1686
+ }
1687
+ function buildPaginatedListQuery(options = {}) {
1688
+ const params = new URLSearchParams();
1689
+ if (options.limit !== void 0) {
1690
+ params.set("limit", String(options.limit));
1691
+ }
1692
+ if (options.cursor !== void 0) {
1693
+ params.set("cursor", options.cursor);
1694
+ }
1695
+ const query = params.toString();
1696
+ return query ? `?${query}` : "";
1697
+ }
1698
+ function parseRetryAfterHeader(response) {
1699
+ const retryAfterHeader = response.headers.get("retry-after");
1700
+ if (!retryAfterHeader) {
1701
+ return void 0;
1702
+ }
1703
+ const parsedSeconds = parseInt(retryAfterHeader, 10);
1704
+ if (!Number.isNaN(parsedSeconds) && parsedSeconds >= 0) {
1705
+ return parsedSeconds;
1706
+ }
1707
+ return void 0;
1708
+ }
1709
+ function normalizeRetryOptions(retry) {
1710
+ const maxAttempts = Math.max(1, Math.floor(retry?.maxAttempts ?? DEFAULT_RETRY_ATTEMPTS));
1711
+ const backoffMs = Math.max(0, Math.floor(retry?.backoffMs ?? DEFAULT_RETRY_BACKOFF_MS));
1712
+ return {
1713
+ maxAttempts,
1714
+ backoffMs,
1715
+ retryOn: new Set(retry?.retryOn ?? DEFAULT_RETRY_STATUSES)
1716
+ };
1717
+ }
1718
+ function normalizeExportPageSize(limit) {
1719
+ if (limit === void 0) {
1720
+ return DEFAULT_EXPORT_PAGE_SIZE;
1721
+ }
1722
+ return Math.max(1, Math.min(DEFAULT_EXPORT_PAGE_SIZE, Math.floor(limit)));
1723
+ }
1724
+ function buildStreamPath(slug, since) {
1725
+ const params = new URLSearchParams();
1726
+ if (since !== void 0) {
1727
+ params.set("since", String(Math.max(0, Math.floor(since))));
1728
+ }
1729
+ const query = params.toString();
1730
+ return `/api/stream/${slug}${query ? `?${query}` : ""}`;
1731
+ }
1732
+ function normalizeReconnectBackoff(value) {
1733
+ if (value === void 0) {
1734
+ return DEFAULT_RETRY_BACKOFF_MS;
1735
+ }
1736
+ return Math.max(0, parseDuration(value));
1737
+ }
1738
+ function shouldReconnectStreamError(error) {
1739
+ if (error instanceof UnauthorizedError || error instanceof NotFoundError) {
1740
+ return false;
1741
+ }
1742
+ if (error instanceof WebhooksCCError) {
1743
+ return error.statusCode === 429 || error.statusCode >= 500;
1744
+ }
1745
+ return error instanceof Error;
1746
+ }
1747
+ function parseStreamRequest(data) {
1748
+ try {
1749
+ const parsed = JSON.parse(data);
1750
+ if (typeof parsed.endpointId !== "string" || typeof parsed.method !== "string" || typeof parsed.receivedAt !== "number" || typeof parsed.headers !== "object" || parsed.headers === null) {
1751
+ return null;
1752
+ }
1753
+ return {
1754
+ id: typeof parsed._id === "string" ? parsed._id : typeof parsed.id === "string" ? parsed.id : "",
1755
+ endpointId: parsed.endpointId,
1756
+ method: parsed.method,
1757
+ path: typeof parsed.path === "string" ? parsed.path : "/",
1758
+ headers: parsed.headers,
1759
+ body: typeof parsed.body === "string" ? parsed.body : void 0,
1760
+ queryParams: typeof parsed.queryParams === "object" && parsed.queryParams !== null ? parsed.queryParams : {},
1761
+ contentType: typeof parsed.contentType === "string" ? parsed.contentType : void 0,
1762
+ ip: typeof parsed.ip === "string" ? parsed.ip : "unknown",
1763
+ size: typeof parsed.size === "number" ? parsed.size : 0,
1764
+ receivedAt: parsed.receivedAt
1765
+ };
1766
+ } catch {
1767
+ return null;
1768
+ }
1769
+ }
1770
+ async function collectMatchingRequests(fetchRequests, options) {
1771
+ const timeout = parseDuration(options.timeout ?? 3e4);
1772
+ const rawPollInterval = parseDuration(options.pollInterval ?? 500);
1773
+ const safePollInterval = Math.max(
1774
+ MIN_POLL_INTERVAL,
1775
+ Math.min(MAX_POLL_INTERVAL, rawPollInterval)
1776
+ );
1777
+ const desiredCount = Math.max(1, Math.floor(options.count));
1778
+ const start = Date.now();
1779
+ let lastChecked = start - WAIT_FOR_LOOKBACK_MS;
1780
+ let iterations = 0;
1781
+ const MAX_ITERATIONS = 1e4;
1782
+ const collected = [];
1783
+ const seenRequestIds = /* @__PURE__ */ new Set();
1784
+ while (Date.now() - start < timeout && iterations < MAX_ITERATIONS) {
1785
+ iterations++;
1786
+ const checkTime = Date.now();
1787
+ try {
1788
+ const requests = (await fetchRequests(lastChecked)).slice().sort((left, right) => left.receivedAt - right.receivedAt);
1789
+ lastChecked = checkTime;
1790
+ for (const request of requests) {
1791
+ if (seenRequestIds.has(request.id)) {
1792
+ continue;
1793
+ }
1794
+ seenRequestIds.add(request.id);
1795
+ if (options.match && !options.match(request)) {
1796
+ continue;
1797
+ }
1798
+ collected.push(request);
1799
+ if (collected.length >= desiredCount) {
1800
+ return collected.slice(0, desiredCount);
1801
+ }
1802
+ }
1803
+ } catch (error) {
1804
+ if (error instanceof WebhooksCCError) {
1805
+ if (error instanceof UnauthorizedError || error instanceof NotFoundError) {
1806
+ throw error;
1807
+ }
1808
+ if (error.statusCode < 500 && !(error instanceof RateLimitError)) {
1809
+ throw error;
1810
+ }
1811
+ }
1812
+ }
1813
+ await sleep(safePollInterval);
1814
+ }
1815
+ throw new TimeoutError(timeout);
1816
+ }
1817
+ function validateMockResponse(mockResponse, fieldName) {
1818
+ const { status } = mockResponse;
1819
+ if (!Number.isInteger(status) || status < 100 || status > 599) {
1820
+ throw new Error(`Invalid ${fieldName} status: ${status}. Must be an integer 100-599.`);
1821
+ }
1822
+ }
1823
+ var WebhooksCC = class {
1824
+ constructor(options) {
1825
+ this.endpoints = {
1826
+ create: async (options = {}) => {
1827
+ if (options.mockResponse) {
1828
+ validateMockResponse(options.mockResponse, "mock response");
1829
+ }
1830
+ const body = {};
1831
+ if (options.name !== void 0) {
1832
+ body.name = options.name;
1833
+ }
1834
+ if (options.mockResponse !== void 0) {
1835
+ body.mockResponse = options.mockResponse;
1836
+ }
1837
+ const isEphemeral = options.ephemeral === true || options.expiresIn !== void 0;
1838
+ if (isEphemeral) {
1839
+ body.isEphemeral = true;
1840
+ }
1841
+ if (options.expiresIn !== void 0) {
1842
+ const durationMs = parseDuration(options.expiresIn);
1843
+ if (durationMs <= 0) {
1844
+ throw new Error("expiresIn must be greater than 0");
1845
+ }
1846
+ body.expiresAt = Date.now() + durationMs;
1847
+ }
1848
+ return this.request("POST", "/endpoints", body);
1849
+ },
1850
+ list: async () => {
1851
+ return this.request("GET", "/endpoints");
1852
+ },
1853
+ get: async (slug) => {
1854
+ validatePathSegment(slug, "slug");
1855
+ return this.request("GET", `/endpoints/${slug}`);
1856
+ },
1857
+ update: async (slug, options) => {
1858
+ validatePathSegment(slug, "slug");
1859
+ if (options.mockResponse && options.mockResponse !== null) {
1860
+ validateMockResponse(options.mockResponse, "mock response");
1861
+ }
1862
+ return this.request("PATCH", `/endpoints/${slug}`, options);
1863
+ },
1864
+ delete: async (slug) => {
1865
+ validatePathSegment(slug, "slug");
1866
+ await this.request("DELETE", `/endpoints/${slug}`);
1867
+ },
1868
+ send: async (slug, options = {}) => {
1869
+ validatePathSegment(slug, "slug");
1870
+ const rawMethod = (options.method ?? "POST").toUpperCase();
1871
+ if (!ALLOWED_METHODS.has(rawMethod)) {
1872
+ throw new Error(
1873
+ `Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
1874
+ );
1875
+ }
1876
+ const { headers = {}, body } = options;
1877
+ const method = rawMethod;
1878
+ const url = `${this.webhookUrl}/w/${slug}`;
1879
+ const fetchHeaders = { ...headers };
1880
+ const hasContentType = Object.keys(fetchHeaders).some(
1881
+ (k) => k.toLowerCase() === "content-type"
1882
+ );
890
1883
  if (body !== void 0 && !hasContentType) {
891
1884
  fetchHeaders["Content-Type"] = "application/json";
892
1885
  }
@@ -907,6 +1900,78 @@ var WebhooksCC = class {
907
1900
  return this.endpoints.send(slug, sendOptions);
908
1901
  }
909
1902
  };
1903
+ this.templates = {
1904
+ listProviders: () => {
1905
+ return [...TEMPLATE_PROVIDERS];
1906
+ },
1907
+ get: (provider) => {
1908
+ return TEMPLATE_METADATA[provider];
1909
+ }
1910
+ };
1911
+ this.usage = async () => {
1912
+ return this.request("GET", "/usage");
1913
+ };
1914
+ this.flow = () => {
1915
+ return new WebhookFlowBuilder(this);
1916
+ };
1917
+ /**
1918
+ * Build a request without sending it. Returns the computed method, URL,
1919
+ * headers, and body — including any provider signatures. Useful for
1920
+ * debugging what sendTo would actually send.
1921
+ *
1922
+ * @param url - Target URL (http or https)
1923
+ * @param options - Same options as sendTo
1924
+ * @returns The computed request details
1925
+ */
1926
+ this.buildRequest = async (url, options = {}) => {
1927
+ let parsed;
1928
+ try {
1929
+ parsed = new URL(url);
1930
+ } catch {
1931
+ throw new Error(`Invalid URL: "${url}" is not a valid URL`);
1932
+ }
1933
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1934
+ throw new Error("Invalid URL: only http and https protocols are supported");
1935
+ }
1936
+ if (options.provider) {
1937
+ if (!options.secret || typeof options.secret !== "string") {
1938
+ throw new Error("buildRequest with a provider requires a non-empty secret");
1939
+ }
1940
+ const sendOptions = await buildTemplateSendOptions(url, {
1941
+ provider: options.provider,
1942
+ template: options.template,
1943
+ secret: options.secret,
1944
+ event: options.event,
1945
+ body: options.body,
1946
+ method: options.method,
1947
+ headers: options.headers,
1948
+ timestamp: options.timestamp
1949
+ });
1950
+ return {
1951
+ url,
1952
+ method: (sendOptions.method ?? "POST").toUpperCase(),
1953
+ headers: sendOptions.headers ?? {},
1954
+ body: sendOptions.body !== void 0 ? typeof sendOptions.body === "string" ? sendOptions.body : JSON.stringify(sendOptions.body) : void 0
1955
+ };
1956
+ }
1957
+ const method = (options.method ?? "POST").toUpperCase();
1958
+ if (!ALLOWED_METHODS.has(method)) {
1959
+ throw new Error(
1960
+ `Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
1961
+ );
1962
+ }
1963
+ const headers = { ...options.headers ?? {} };
1964
+ const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
1965
+ if (options.body !== void 0 && !hasContentType) {
1966
+ headers["Content-Type"] = "application/json";
1967
+ }
1968
+ return {
1969
+ url,
1970
+ method,
1971
+ headers,
1972
+ body: options.body !== void 0 ? typeof options.body === "string" ? options.body : JSON.stringify(options.body) : void 0
1973
+ };
1974
+ };
910
1975
  /**
911
1976
  * Send a webhook directly to any URL with optional provider signing.
912
1977
  * Use this for local integration testing — send properly signed webhooks
@@ -978,10 +2043,86 @@ var WebhooksCC = class {
978
2043
  `/endpoints/${endpointSlug}/requests${query ? `?${query}` : ""}`
979
2044
  );
980
2045
  },
2046
+ listPaginated: async (endpointSlug, options = {}) => {
2047
+ validatePathSegment(endpointSlug, "endpointSlug");
2048
+ return this.request(
2049
+ "GET",
2050
+ `/endpoints/${endpointSlug}/requests/paginated${buildPaginatedListQuery(options)}`
2051
+ );
2052
+ },
981
2053
  get: async (requestId) => {
982
2054
  validatePathSegment(requestId, "requestId");
983
2055
  return this.request("GET", `/requests/${requestId}`);
984
2056
  },
2057
+ waitForAll: async (endpointSlug, options) => {
2058
+ validatePathSegment(endpointSlug, "endpointSlug");
2059
+ const listLimit = Math.min(1e3, Math.max(100, Math.floor(options.count) * 2));
2060
+ return collectMatchingRequests(
2061
+ (since) => this.requests.list(endpointSlug, {
2062
+ since,
2063
+ limit: listLimit
2064
+ }),
2065
+ options
2066
+ );
2067
+ },
2068
+ search: async (filters = {}) => {
2069
+ return this.request(
2070
+ "GET",
2071
+ `/search/requests${buildSearchQuery(filters, true)}`
2072
+ );
2073
+ },
2074
+ count: async (filters = {}) => {
2075
+ const response = await this.request(
2076
+ "GET",
2077
+ `/search/requests/count${buildSearchQuery(filters, false)}`
2078
+ );
2079
+ return response.count;
2080
+ },
2081
+ clear: async (endpointSlug, options = {}) => {
2082
+ validatePathSegment(endpointSlug, "endpointSlug");
2083
+ await this.request(
2084
+ "DELETE",
2085
+ `/endpoints/${endpointSlug}/requests${buildClearQuery(options)}`
2086
+ );
2087
+ },
2088
+ export: async (endpointSlug, options) => {
2089
+ validatePathSegment(endpointSlug, "endpointSlug");
2090
+ const endpoint = await this.endpoints.get(endpointSlug);
2091
+ const endpointUrl = endpoint.url ?? `${this.webhookUrl}/w/${endpoint.slug}`;
2092
+ const requests = [];
2093
+ const pageSize = normalizeExportPageSize(options.limit);
2094
+ let cursor;
2095
+ while (true) {
2096
+ const remaining = options.limit !== void 0 ? Math.max(0, options.limit - requests.length) : pageSize;
2097
+ if (options.limit !== void 0 && remaining === 0) {
2098
+ break;
2099
+ }
2100
+ const page = await this.requests.listPaginated(endpointSlug, {
2101
+ limit: options.limit !== void 0 ? Math.min(pageSize, remaining) : pageSize,
2102
+ cursor
2103
+ });
2104
+ for (const request of page.items) {
2105
+ if (options.since !== void 0 && request.receivedAt <= options.since) {
2106
+ continue;
2107
+ }
2108
+ requests.push(request);
2109
+ if (options.limit !== void 0 && requests.length >= options.limit) {
2110
+ break;
2111
+ }
2112
+ }
2113
+ if (!page.hasMore || !page.cursor) {
2114
+ break;
2115
+ }
2116
+ if (options.limit !== void 0 && requests.length >= options.limit) {
2117
+ break;
2118
+ }
2119
+ cursor = page.cursor;
2120
+ }
2121
+ if (options.format === "curl") {
2122
+ return buildCurlExport(endpointUrl, requests);
2123
+ }
2124
+ return buildHarExport(endpointUrl, requests, SDK_VERSION);
2125
+ },
985
2126
  /**
986
2127
  * Polls for incoming requests until one matches or timeout expires.
987
2128
  *
@@ -995,47 +2136,11 @@ var WebhooksCC = class {
995
2136
  * @throws Error if timeout expires or max iterations (10000) reached
996
2137
  */
997
2138
  waitFor: async (endpointSlug, options = {}) => {
998
- validatePathSegment(endpointSlug, "endpointSlug");
999
- const timeout = parseDuration(options.timeout ?? 3e4);
1000
- const rawPollInterval = parseDuration(options.pollInterval ?? 500);
1001
- const { match } = options;
1002
- const safePollInterval = Math.max(
1003
- MIN_POLL_INTERVAL,
1004
- Math.min(MAX_POLL_INTERVAL, rawPollInterval)
1005
- );
1006
- const start = Date.now();
1007
- let lastChecked = start - 5 * 60 * 1e3;
1008
- const MAX_ITERATIONS = 1e4;
1009
- let iterations = 0;
1010
- while (Date.now() - start < timeout && iterations < MAX_ITERATIONS) {
1011
- iterations++;
1012
- const checkTime = Date.now();
1013
- try {
1014
- const requests = await this.requests.list(endpointSlug, {
1015
- since: lastChecked,
1016
- limit: 100
1017
- });
1018
- lastChecked = checkTime;
1019
- const matched = match ? requests.find(match) : requests[0];
1020
- if (matched) {
1021
- return matched;
1022
- }
1023
- } catch (error) {
1024
- if (error instanceof WebhooksCCError) {
1025
- if (error instanceof UnauthorizedError) {
1026
- throw error;
1027
- }
1028
- if (error instanceof NotFoundError) {
1029
- throw error;
1030
- }
1031
- if (error.statusCode < 500 && !(error instanceof RateLimitError)) {
1032
- throw error;
1033
- }
1034
- }
1035
- }
1036
- await sleep(safePollInterval);
1037
- }
1038
- throw new TimeoutError(timeout);
2139
+ const [request] = await this.requests.waitForAll(endpointSlug, {
2140
+ ...options,
2141
+ count: 1
2142
+ });
2143
+ return request;
1039
2144
  },
1040
2145
  /**
1041
2146
  * Replay a captured request to a target URL.
@@ -1078,20 +2183,25 @@ var WebhooksCC = class {
1078
2183
  * The connection is closed when the iterator is broken, the signal is aborted,
1079
2184
  * or the timeout expires.
1080
2185
  *
1081
- * No automatic reconnection if the connection drops, the iterator ends.
2186
+ * Reconnection is opt-in and resumes from the last yielded request timestamp.
1082
2187
  */
1083
2188
  subscribe: (slug, options = {}) => {
1084
2189
  validatePathSegment(slug, "slug");
1085
- const { signal, timeout } = options;
2190
+ const { signal, timeout, reconnect = false, onReconnect } = options;
1086
2191
  const baseUrl = this.baseUrl;
1087
2192
  const apiKey = this.apiKey;
1088
2193
  const timeoutMs = timeout !== void 0 ? parseDuration(timeout) : void 0;
2194
+ const maxReconnectAttempts = Math.max(0, Math.floor(options.maxReconnectAttempts ?? 5));
2195
+ const reconnectBackoffMs = normalizeReconnectBackoff(options.reconnectBackoffMs);
1089
2196
  return {
1090
2197
  [Symbol.asyncIterator]() {
1091
2198
  const controller = new AbortController();
1092
2199
  let timeoutId;
1093
2200
  let iterator = null;
1094
2201
  let started = false;
2202
+ let reconnectAttempts = 0;
2203
+ let lastReceivedAt;
2204
+ const seenRequestIds = /* @__PURE__ */ new Set();
1095
2205
  const onAbort = () => controller.abort();
1096
2206
  if (signal) {
1097
2207
  if (signal.aborted) {
@@ -1108,7 +2218,10 @@ var WebhooksCC = class {
1108
2218
  if (signal) signal.removeEventListener("abort", onAbort);
1109
2219
  };
1110
2220
  const start = async () => {
1111
- const url = `${baseUrl}/api/stream/${slug}`;
2221
+ const url = `${baseUrl}${buildStreamPath(
2222
+ slug,
2223
+ lastReceivedAt !== void 0 ? lastReceivedAt - 1 : void 0
2224
+ )}`;
1112
2225
  const connectController = new AbortController();
1113
2226
  const connectTimeout = setTimeout(() => connectController.abort(), 3e4);
1114
2227
  controller.signal.addEventListener("abort", () => connectController.abort(), {
@@ -1124,12 +2237,10 @@ var WebhooksCC = class {
1124
2237
  clearTimeout(connectTimeout);
1125
2238
  }
1126
2239
  if (!response.ok) {
1127
- cleanup();
1128
2240
  const text = await response.text();
1129
2241
  throw mapStatusToError(response.status, text, response);
1130
2242
  }
1131
2243
  if (!response.body) {
1132
- cleanup();
1133
2244
  throw new Error("SSE response has no body");
1134
2245
  }
1135
2246
  controller.signal.addEventListener(
@@ -1142,6 +2253,24 @@ var WebhooksCC = class {
1142
2253
  );
1143
2254
  return parseSSE(response.body);
1144
2255
  };
2256
+ const reconnectStream = async () => {
2257
+ if (!reconnect || reconnectAttempts >= maxReconnectAttempts || controller.signal.aborted) {
2258
+ cleanup();
2259
+ return false;
2260
+ }
2261
+ reconnectAttempts++;
2262
+ try {
2263
+ onReconnect?.(reconnectAttempts);
2264
+ } catch {
2265
+ }
2266
+ await sleep(reconnectBackoffMs * 2 ** (reconnectAttempts - 1));
2267
+ if (controller.signal.aborted) {
2268
+ cleanup();
2269
+ return false;
2270
+ }
2271
+ iterator = await start();
2272
+ return true;
2273
+ };
1145
2274
  return {
1146
2275
  [Symbol.asyncIterator]() {
1147
2276
  return this;
@@ -1155,32 +2284,26 @@ var WebhooksCC = class {
1155
2284
  while (iterator) {
1156
2285
  const { done, value } = await iterator.next();
1157
2286
  if (done) {
1158
- cleanup();
2287
+ iterator = null;
2288
+ if (await reconnectStream()) {
2289
+ continue;
2290
+ }
1159
2291
  return { done: true, value: void 0 };
1160
2292
  }
2293
+ reconnectAttempts = 0;
1161
2294
  if (value.event === "request") {
1162
- try {
1163
- const data = JSON.parse(value.data);
1164
- if (!data.endpointId || !data.method || !data.headers || !data.receivedAt) {
1165
- continue;
1166
- }
1167
- const req = {
1168
- id: data._id ?? data.id,
1169
- endpointId: data.endpointId,
1170
- method: data.method,
1171
- path: data.path ?? "/",
1172
- headers: data.headers,
1173
- body: data.body ?? void 0,
1174
- queryParams: data.queryParams ?? {},
1175
- contentType: data.contentType ?? void 0,
1176
- ip: data.ip ?? "unknown",
1177
- size: data.size ?? 0,
1178
- receivedAt: data.receivedAt
1179
- };
1180
- return { done: false, value: req };
1181
- } catch {
2295
+ const req = parseStreamRequest(value.data);
2296
+ if (!req) {
2297
+ continue;
2298
+ }
2299
+ if (req.id && seenRequestIds.has(req.id)) {
1182
2300
  continue;
1183
2301
  }
2302
+ if (req.id) {
2303
+ seenRequestIds.add(req.id);
2304
+ }
2305
+ lastReceivedAt = req.receivedAt;
2306
+ return { done: false, value: req };
1184
2307
  }
1185
2308
  if (value.event === "timeout" || value.event === "endpoint_deleted") {
1186
2309
  cleanup();
@@ -1190,11 +2313,17 @@ var WebhooksCC = class {
1190
2313
  cleanup();
1191
2314
  return { done: true, value: void 0 };
1192
2315
  } catch (error) {
1193
- cleanup();
1194
- controller.abort();
1195
2316
  if (error instanceof Error && error.name === "AbortError") {
2317
+ cleanup();
2318
+ controller.abort();
1196
2319
  return { done: true, value: void 0 };
1197
2320
  }
2321
+ iterator = null;
2322
+ if (shouldReconnectStreamError(error) && await reconnectStream()) {
2323
+ return this.next();
2324
+ }
2325
+ cleanup();
2326
+ controller.abort();
1198
2327
  throw error;
1199
2328
  }
1200
2329
  },
@@ -1218,67 +2347,100 @@ var WebhooksCC = class {
1218
2347
  this.baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL);
1219
2348
  this.webhookUrl = stripTrailingSlashes(options.webhookUrl ?? DEFAULT_WEBHOOK_URL);
1220
2349
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
2350
+ this.retry = normalizeRetryOptions(options.retry);
1221
2351
  this.hooks = options.hooks ?? {};
1222
2352
  }
1223
2353
  async request(method, path, body) {
1224
2354
  const url = `${this.baseUrl}/api${path}`;
1225
- const controller = new AbortController();
1226
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1227
- const start = Date.now();
1228
- try {
1229
- this.hooks.onRequest?.({ method, url });
1230
- } catch {
1231
- }
1232
- try {
1233
- const response = await fetch(url, {
1234
- method,
1235
- headers: {
1236
- Authorization: `Bearer ${this.apiKey}`,
1237
- "Content-Type": "application/json"
1238
- },
1239
- body: body ? JSON.stringify(body) : void 0,
1240
- signal: controller.signal
1241
- });
1242
- const durationMs = Date.now() - start;
1243
- if (!response.ok) {
1244
- const errorText = await response.text();
1245
- const sanitizedError = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
1246
- const error = mapStatusToError(response.status, sanitizedError, response);
1247
- try {
1248
- this.hooks.onError?.({ method, url, error, durationMs });
1249
- } catch {
1250
- }
1251
- throw error;
1252
- }
2355
+ let attempt = 0;
2356
+ while (true) {
2357
+ attempt++;
2358
+ const controller = new AbortController();
2359
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2360
+ const start = Date.now();
1253
2361
  try {
1254
- this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
2362
+ this.hooks.onRequest?.({ method, url });
1255
2363
  } catch {
1256
2364
  }
1257
- if (response.status === 204 || response.headers.get("content-length") === "0") {
1258
- return void 0;
1259
- }
1260
- const contentType = response.headers.get("content-type");
1261
- if (contentType && !contentType.includes("application/json")) {
1262
- throw new Error(`Unexpected content type: ${contentType}`);
1263
- }
1264
- return response.json();
1265
- } catch (error) {
1266
- if (error instanceof Error && error.name === "AbortError") {
1267
- const timeoutError = new TimeoutError(this.timeout);
2365
+ try {
2366
+ const response = await fetch(url, {
2367
+ method,
2368
+ headers: {
2369
+ Authorization: `Bearer ${this.apiKey}`,
2370
+ "Content-Type": "application/json"
2371
+ },
2372
+ body: body ? JSON.stringify(body) : void 0,
2373
+ signal: controller.signal
2374
+ });
2375
+ const durationMs = Date.now() - start;
2376
+ if (!response.ok) {
2377
+ const errorText = await response.text();
2378
+ const sanitizedError = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
2379
+ const error = mapStatusToError(response.status, sanitizedError, response);
2380
+ try {
2381
+ this.hooks.onError?.({ method, url, error, durationMs });
2382
+ } catch {
2383
+ }
2384
+ if (attempt < this.retry.maxAttempts && this.retry.retryOn.has(response.status)) {
2385
+ const retryDelayMs = response.status === 429 && parseRetryAfterHeader(response) !== void 0 ? (parseRetryAfterHeader(response) ?? 0) * 1e3 : this.retry.backoffMs * 2 ** (attempt - 1);
2386
+ await sleep(retryDelayMs);
2387
+ continue;
2388
+ }
2389
+ throw error;
2390
+ }
1268
2391
  try {
1269
- this.hooks.onError?.({
1270
- method,
1271
- url,
1272
- error: timeoutError,
1273
- durationMs: Date.now() - start
1274
- });
2392
+ this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
1275
2393
  } catch {
1276
2394
  }
1277
- throw timeoutError;
2395
+ if (response.status === 204 || response.headers.get("content-length") === "0") {
2396
+ return void 0;
2397
+ }
2398
+ const contentType = response.headers.get("content-type");
2399
+ if (contentType && !contentType.includes("application/json")) {
2400
+ throw new Error(`Unexpected content type: ${contentType}`);
2401
+ }
2402
+ return response.json();
2403
+ } catch (error) {
2404
+ if (error instanceof WebhooksCCError) {
2405
+ throw error;
2406
+ }
2407
+ if (error instanceof Error && error.name === "AbortError") {
2408
+ const timeoutError = new TimeoutError(this.timeout);
2409
+ try {
2410
+ this.hooks.onError?.({
2411
+ method,
2412
+ url,
2413
+ error: timeoutError,
2414
+ durationMs: Date.now() - start
2415
+ });
2416
+ } catch {
2417
+ }
2418
+ if (attempt < this.retry.maxAttempts) {
2419
+ await sleep(this.retry.backoffMs * 2 ** (attempt - 1));
2420
+ continue;
2421
+ }
2422
+ throw timeoutError;
2423
+ }
2424
+ const isNetworkError = error instanceof Error;
2425
+ if (isNetworkError) {
2426
+ try {
2427
+ this.hooks.onError?.({
2428
+ method,
2429
+ url,
2430
+ error,
2431
+ durationMs: Date.now() - start
2432
+ });
2433
+ } catch {
2434
+ }
2435
+ }
2436
+ if (attempt < this.retry.maxAttempts && isNetworkError) {
2437
+ await sleep(this.retry.backoffMs * 2 ** (attempt - 1));
2438
+ continue;
2439
+ }
2440
+ throw error;
2441
+ } finally {
2442
+ clearTimeout(timeoutId);
1278
2443
  }
1279
- throw error;
1280
- } finally {
1281
- clearTimeout(timeoutId);
1282
2444
  }
1283
2445
  }
1284
2446
  /** Returns a static description of all SDK operations (no API call). */
@@ -1288,7 +2450,12 @@ var WebhooksCC = class {
1288
2450
  endpoints: {
1289
2451
  create: {
1290
2452
  description: "Create a webhook endpoint",
1291
- params: { name: "string?" }
2453
+ params: {
2454
+ name: "string?",
2455
+ ephemeral: "boolean?",
2456
+ expiresIn: "number|string?",
2457
+ mockResponse: "object?"
2458
+ }
1292
2459
  },
1293
2460
  list: {
1294
2461
  description: "List all endpoints",
@@ -1314,18 +2481,48 @@ var WebhooksCC = class {
1314
2481
  description: "Send a provider template webhook with signed headers",
1315
2482
  params: {
1316
2483
  slug: "string",
1317
- provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"',
2484
+ provider: PROVIDER_PARAM_DESCRIPTION,
1318
2485
  template: "string?",
1319
2486
  secret: "string",
1320
2487
  event: "string?"
1321
2488
  }
1322
2489
  }
1323
2490
  },
2491
+ templates: {
2492
+ listProviders: {
2493
+ description: "List supported template providers",
2494
+ params: {}
2495
+ },
2496
+ get: {
2497
+ description: "Get static metadata for a template provider",
2498
+ params: {
2499
+ provider: PROVIDER_PARAM_DESCRIPTION
2500
+ }
2501
+ }
2502
+ },
2503
+ usage: {
2504
+ description: "Get current request usage and remaining quota",
2505
+ params: {}
2506
+ },
2507
+ flow: {
2508
+ description: "Create a fluent webhook flow builder for common capture/verify/replay flows",
2509
+ params: {}
2510
+ },
1324
2511
  sendTo: {
1325
2512
  description: "Send a webhook directly to any URL with optional provider signing",
1326
2513
  params: {
1327
2514
  url: "string",
1328
- provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
2515
+ provider: `${PROVIDER_PARAM_DESCRIPTION}?`,
2516
+ secret: "string?",
2517
+ body: "unknown?",
2518
+ headers: "Record<string, string>?"
2519
+ }
2520
+ },
2521
+ buildRequest: {
2522
+ description: "Build a request without sending it \u2014 returns computed method, URL, headers, and body including provider signatures",
2523
+ params: {
2524
+ url: "string",
2525
+ provider: `${PROVIDER_PARAM_DESCRIPTION}?`,
1329
2526
  secret: "string?",
1330
2527
  body: "unknown?",
1331
2528
  headers: "Record<string, string>?"
@@ -1336,10 +2533,24 @@ var WebhooksCC = class {
1336
2533
  description: "List captured requests",
1337
2534
  params: { endpointSlug: "string", limit: "number?", since: "number?" }
1338
2535
  },
2536
+ listPaginated: {
2537
+ description: "List captured requests with cursor-based pagination",
2538
+ params: { endpointSlug: "string", limit: "number?", cursor: "string?" }
2539
+ },
1339
2540
  get: {
1340
2541
  description: "Get request by ID",
1341
2542
  params: { requestId: "string" }
1342
2543
  },
2544
+ waitForAll: {
2545
+ description: "Poll until multiple matching requests arrive",
2546
+ params: {
2547
+ endpointSlug: "string",
2548
+ count: "number",
2549
+ timeout: "number|string?",
2550
+ pollInterval: "number|string?",
2551
+ match: "function?"
2552
+ }
2553
+ },
1343
2554
  waitFor: {
1344
2555
  description: "Poll until a matching request arrives",
1345
2556
  params: {
@@ -1350,11 +2561,58 @@ var WebhooksCC = class {
1350
2561
  },
1351
2562
  subscribe: {
1352
2563
  description: "Stream requests via SSE",
1353
- params: { slug: "string", signal: "AbortSignal?", timeout: "number|string?" }
2564
+ params: {
2565
+ slug: "string",
2566
+ signal: "AbortSignal?",
2567
+ timeout: "number|string?",
2568
+ reconnect: "boolean?",
2569
+ maxReconnectAttempts: "number?",
2570
+ reconnectBackoffMs: "number|string?",
2571
+ onReconnect: "function?"
2572
+ }
1354
2573
  },
1355
2574
  replay: {
1356
2575
  description: "Replay a captured request to a URL",
1357
2576
  params: { requestId: "string", targetUrl: "string" }
2577
+ },
2578
+ export: {
2579
+ description: "Export captured requests as HAR or cURL commands",
2580
+ params: {
2581
+ endpointSlug: "string",
2582
+ format: '"har"|"curl"',
2583
+ limit: "number?",
2584
+ since: "number?"
2585
+ }
2586
+ },
2587
+ search: {
2588
+ description: "Search retained requests across path, body, and headers",
2589
+ params: {
2590
+ slug: "string?",
2591
+ method: "string?",
2592
+ q: "string?",
2593
+ from: "number|string?",
2594
+ to: "number|string?",
2595
+ limit: "number?",
2596
+ offset: "number?",
2597
+ order: '"asc"|"desc"?'
2598
+ }
2599
+ },
2600
+ count: {
2601
+ description: "Count retained requests matching search filters",
2602
+ params: {
2603
+ slug: "string?",
2604
+ method: "string?",
2605
+ q: "string?",
2606
+ from: "number|string?",
2607
+ to: "number|string?"
2608
+ }
2609
+ },
2610
+ clear: {
2611
+ description: "Delete captured requests for an endpoint",
2612
+ params: {
2613
+ endpointSlug: "string",
2614
+ before: "number|string?"
2615
+ }
1358
2616
  }
1359
2617
  }
1360
2618
  };
@@ -1370,6 +2628,49 @@ function stripTrailingSlashes(url) {
1370
2628
  }
1371
2629
 
1372
2630
  // src/helpers.ts
2631
+ function getHeaderValue(headers, name) {
2632
+ const target = name.toLowerCase();
2633
+ for (const [key, value] of Object.entries(headers)) {
2634
+ if (key.toLowerCase() === target) {
2635
+ return value;
2636
+ }
2637
+ }
2638
+ return void 0;
2639
+ }
2640
+ function getContentType(request) {
2641
+ return request.contentType ?? getHeaderValue(request.headers, "content-type");
2642
+ }
2643
+ function normalizeContentType(value) {
2644
+ if (!value) {
2645
+ return void 0;
2646
+ }
2647
+ return value.split(";", 1)[0]?.trim().toLowerCase();
2648
+ }
2649
+ function getJsonPathValue(body, path) {
2650
+ const parts = path.split(".");
2651
+ let current = body;
2652
+ for (const part of parts) {
2653
+ if (current === null || current === void 0) {
2654
+ return void 0;
2655
+ }
2656
+ if (Array.isArray(current)) {
2657
+ const index = Number(part);
2658
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) {
2659
+ return void 0;
2660
+ }
2661
+ current = current[index];
2662
+ continue;
2663
+ }
2664
+ if (typeof current !== "object") {
2665
+ return void 0;
2666
+ }
2667
+ if (!Object.prototype.hasOwnProperty.call(current, part)) {
2668
+ return void 0;
2669
+ }
2670
+ current = current[part];
2671
+ }
2672
+ return current;
2673
+ }
1373
2674
  function parseJsonBody(request) {
1374
2675
  if (!request.body) return void 0;
1375
2676
  try {
@@ -1392,6 +2693,50 @@ function matchJsonField(field, value) {
1392
2693
  return body[field] === value;
1393
2694
  };
1394
2695
  }
2696
+ function parseFormBody(request) {
2697
+ if (!request.body) {
2698
+ return void 0;
2699
+ }
2700
+ const contentType = normalizeContentType(getContentType(request));
2701
+ if (contentType !== "application/x-www-form-urlencoded") {
2702
+ return void 0;
2703
+ }
2704
+ const parsed = {};
2705
+ for (const [key, value] of new URLSearchParams(request.body).entries()) {
2706
+ const existing = parsed[key];
2707
+ if (existing === void 0) {
2708
+ parsed[key] = value;
2709
+ continue;
2710
+ }
2711
+ if (Array.isArray(existing)) {
2712
+ existing.push(value);
2713
+ continue;
2714
+ }
2715
+ parsed[key] = [existing, value];
2716
+ }
2717
+ return parsed;
2718
+ }
2719
+ function parseBody(request) {
2720
+ if (!request.body) {
2721
+ return void 0;
2722
+ }
2723
+ const contentType = normalizeContentType(getContentType(request));
2724
+ if (contentType === "application/json" || contentType?.endsWith("+json")) {
2725
+ const parsed = parseJsonBody(request);
2726
+ return parsed === void 0 ? request.body : parsed;
2727
+ }
2728
+ if (contentType === "application/x-www-form-urlencoded") {
2729
+ return parseFormBody(request);
2730
+ }
2731
+ return request.body;
2732
+ }
2733
+ function extractJsonField(request, path) {
2734
+ const body = parseJsonBody(request);
2735
+ if (body === void 0) {
2736
+ return void 0;
2737
+ }
2738
+ return getJsonPathValue(body, path);
2739
+ }
1395
2740
  function isShopifyWebhook(request) {
1396
2741
  return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-shopify-hmac-sha256");
1397
2742
  }
@@ -1407,23 +2752,85 @@ function isPaddleWebhook(request) {
1407
2752
  function isLinearWebhook(request) {
1408
2753
  return Object.keys(request.headers).some((k) => k.toLowerCase() === "linear-signature");
1409
2754
  }
2755
+ function isDiscordWebhook(request) {
2756
+ const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
2757
+ return keys.includes("x-signature-ed25519") && keys.includes("x-signature-timestamp");
2758
+ }
1410
2759
  function isStandardWebhook(request) {
1411
2760
  const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
1412
2761
  return keys.includes("webhook-id") && keys.includes("webhook-timestamp") && keys.includes("webhook-signature");
1413
2762
  }
1414
2763
 
1415
2764
  // src/matchers.ts
2765
+ function getHeaderValue2(headers, name) {
2766
+ const lowerName = name.toLowerCase();
2767
+ for (const [key, value] of Object.entries(headers)) {
2768
+ if (key.toLowerCase() === lowerName) {
2769
+ return value;
2770
+ }
2771
+ }
2772
+ return void 0;
2773
+ }
2774
+ function globToRegExp(pattern) {
2775
+ let source = "^";
2776
+ for (let index = 0; index < pattern.length; index++) {
2777
+ const char = pattern[index];
2778
+ if (char === "*") {
2779
+ if (pattern[index + 1] === "*") {
2780
+ source += ".*";
2781
+ index++;
2782
+ } else {
2783
+ source += "[^/]*";
2784
+ }
2785
+ continue;
2786
+ }
2787
+ source += /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
2788
+ }
2789
+ source += "$";
2790
+ return new RegExp(source);
2791
+ }
2792
+ function isDeepSubset(expected, actual) {
2793
+ if (Array.isArray(expected)) {
2794
+ return Array.isArray(actual) && expected.every((value, index) => isDeepSubset(value, actual[index]));
2795
+ }
2796
+ if (expected && typeof expected === "object") {
2797
+ if (!actual || typeof actual !== "object" || Array.isArray(actual)) {
2798
+ return false;
2799
+ }
2800
+ return Object.entries(expected).every(([key, value]) => {
2801
+ if (!Object.prototype.hasOwnProperty.call(actual, key)) {
2802
+ return false;
2803
+ }
2804
+ return isDeepSubset(value, actual[key]);
2805
+ });
2806
+ }
2807
+ return Object.is(expected, actual);
2808
+ }
1416
2809
  function matchMethod(method) {
1417
2810
  const upper = method.toUpperCase();
1418
2811
  return (request) => request.method.toUpperCase() === upper;
1419
2812
  }
1420
2813
  function matchHeader(name, value) {
1421
- const lowerName = name.toLowerCase();
1422
2814
  return (request) => {
1423
- const entry = Object.entries(request.headers).find(([k]) => k.toLowerCase() === lowerName);
1424
- if (!entry) return false;
2815
+ const headerValue = getHeaderValue2(request.headers, name);
2816
+ if (headerValue === void 0) return false;
1425
2817
  if (value === void 0) return true;
1426
- return entry[1] === value;
2818
+ return headerValue === value;
2819
+ };
2820
+ }
2821
+ function matchPath(pattern) {
2822
+ const regex = globToRegExp(pattern);
2823
+ return (request) => regex.test(request.path);
2824
+ }
2825
+ function matchQueryParam(key, value) {
2826
+ return (request) => {
2827
+ if (!Object.prototype.hasOwnProperty.call(request.queryParams, key)) {
2828
+ return false;
2829
+ }
2830
+ if (value === void 0) {
2831
+ return true;
2832
+ }
2833
+ return request.queryParams[key] === value;
1427
2834
  };
1428
2835
  }
1429
2836
  function matchBodyPath(path, value) {
@@ -1448,6 +2855,23 @@ function matchBodyPath(path, value) {
1448
2855
  return current === value;
1449
2856
  };
1450
2857
  }
2858
+ function matchBodySubset(subset) {
2859
+ return (request) => {
2860
+ const body = parseJsonBody(request);
2861
+ return isDeepSubset(subset, body);
2862
+ };
2863
+ }
2864
+ function matchContentType(type) {
2865
+ const expected = type.trim().toLowerCase();
2866
+ return (request) => {
2867
+ const raw = request.contentType ?? getHeaderValue2(request.headers, "content-type");
2868
+ if (!raw) {
2869
+ return false;
2870
+ }
2871
+ const normalized = raw.trim().toLowerCase();
2872
+ return normalized === expected || normalized.startsWith(`${expected};`);
2873
+ };
2874
+ }
1451
2875
  function matchAll(first, ...rest) {
1452
2876
  const matchers = [first, ...rest];
1453
2877
  return (request) => matchers.every((m) => m(request));
@@ -1460,10 +2884,15 @@ export {
1460
2884
  ApiError,
1461
2885
  NotFoundError,
1462
2886
  RateLimitError,
2887
+ TEMPLATE_METADATA,
1463
2888
  TimeoutError,
1464
2889
  UnauthorizedError,
2890
+ WebhookFlowBuilder,
1465
2891
  WebhooksCC,
1466
2892
  WebhooksCCError,
2893
+ diffRequests,
2894
+ extractJsonField,
2895
+ isDiscordWebhook,
1467
2896
  isGitHubWebhook,
1468
2897
  isLinearWebhook,
1469
2898
  isPaddleWebhook,
@@ -1475,10 +2904,26 @@ export {
1475
2904
  matchAll,
1476
2905
  matchAny,
1477
2906
  matchBodyPath,
2907
+ matchBodySubset,
2908
+ matchContentType,
1478
2909
  matchHeader,
1479
2910
  matchJsonField,
1480
2911
  matchMethod,
2912
+ matchPath,
2913
+ matchQueryParam,
2914
+ parseBody,
1481
2915
  parseDuration,
2916
+ parseFormBody,
1482
2917
  parseJsonBody,
1483
- parseSSE
2918
+ parseSSE,
2919
+ verifyDiscordSignature,
2920
+ verifyGitHubSignature,
2921
+ verifyLinearSignature,
2922
+ verifyPaddleSignature,
2923
+ verifyShopifySignature,
2924
+ verifySignature,
2925
+ verifySlackSignature,
2926
+ verifyStandardWebhookSignature,
2927
+ verifyStripeSignature,
2928
+ verifyTwilioSignature
1484
2929
  };