@webhooks-cc/sdk 0.6.0 → 1.0.1

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,18 +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
- const hadPrefix = rawSecret.startsWith("whsec_");
723
- if (hadPrefix) {
724
- rawSecret = rawSecret.slice(6);
725
- }
726
- let secretBytes;
727
- try {
728
- secretBytes = fromBase64(rawSecret);
729
- } catch {
730
- const raw = hadPrefix ? options.secret : rawSecret;
731
- secretBytes = new TextEncoder().encode(raw);
732
- }
928
+ const secretBytes = decodeStandardWebhookSecret(options.secret);
733
929
  const signature = await hmacSignRaw("SHA-256", secretBytes, signingInput);
734
930
  return {
735
931
  method: method2,
@@ -777,6 +973,21 @@ async function buildTemplateSendOptions(endpointUrl, options) {
777
973
  const signature = await hmacSign("SHA-1", options.secret, signaturePayload);
778
974
  headers["x-twilio-signature"] = toBase64(signature);
779
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
+ }
780
991
  return {
781
992
  method,
782
993
  headers: {
@@ -787,15 +998,8 @@ async function buildTemplateSendOptions(endpointUrl, options) {
787
998
  };
788
999
  }
789
1000
 
790
- // src/client.ts
791
- var DEFAULT_BASE_URL = "https://webhooks.cc";
792
- var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
793
- var DEFAULT_TIMEOUT = 3e4;
794
- var SDK_VERSION = "0.6.0";
795
- var MIN_POLL_INTERVAL = 10;
796
- var MAX_POLL_INTERVAL = 6e4;
797
- var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
798
- var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
1001
+ // src/request-export.ts
1002
+ var OMITTED_EXPORT_HEADERS = /* @__PURE__ */ new Set([
799
1003
  "host",
800
1004
  "connection",
801
1005
  "content-length",
@@ -803,10 +1007,12 @@ var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
803
1007
  "keep-alive",
804
1008
  "te",
805
1009
  "trailer",
806
- "upgrade"
807
- ]);
808
- var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "proxy-authorization", "set-cookie"]);
809
- var PROXY_HEADERS = /* @__PURE__ */ new Set([
1010
+ "upgrade",
1011
+ "authorization",
1012
+ "cookie",
1013
+ "proxy-authorization",
1014
+ "set-cookie",
1015
+ "accept-encoding",
810
1016
  "cdn-loop",
811
1017
  "cf-connecting-ip",
812
1018
  "cf-ipcountry",
@@ -819,78 +1025,861 @@ var PROXY_HEADERS = /* @__PURE__ */ new Set([
819
1025
  "x-real-ip",
820
1026
  "true-client-ip"
821
1027
  ]);
822
- var ApiError = WebhooksCCError;
823
- function mapStatusToError(status, message, response) {
824
- const isGeneric = message.length < 30;
825
- switch (status) {
826
- case 401: {
827
- const hint = isGeneric ? `${message} \u2014 Get an API key at https://webhooks.cc/account` : message;
828
- 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)}"`);
829
1057
  }
830
- case 404: {
831
- const hint = isGeneric ? `${message} \u2014 Use client.endpoints.list() to see available endpoints.` : message;
832
- return new NotFoundError(hint);
1058
+ if (request.body) {
1059
+ parts.push(`-d '${escapeForShellSingleQuotes(request.body)}'`);
833
1060
  }
834
- case 429: {
835
- const retryAfterHeader = response.headers.get("retry-after");
836
- let retryAfter;
837
- if (retryAfterHeader) {
838
- const parsed = parseInt(retryAfterHeader, 10);
839
- retryAfter = Number.isNaN(parsed) ? void 0 : parsed;
840
- }
841
- 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
+ })
842
1118
  }
843
- default:
844
- 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`);
845
1126
  }
846
1127
  }
847
- var SAFE_PATH_SEGMENT_REGEX = /^[a-zA-Z0-9_-]+$/;
848
- function validatePathSegment(segment, name) {
849
- if (!SAFE_PATH_SEGMENT_REGEX.test(segment)) {
850
- throw new Error(
851
- `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"]
1361
+ );
1362
+ return await globalThis.crypto.subtle.verify(
1363
+ "Ed25519",
1364
+ key,
1365
+ signatureData,
1366
+ new TextEncoder().encode(`${timestamp}${normalizeBody(body)}`)
852
1367
  );
1368
+ } catch {
1369
+ return false;
853
1370
  }
854
1371
  }
855
- var WebhooksCC = class {
856
- constructor(options) {
857
- this.endpoints = {
858
- create: async (options = {}) => {
859
- return this.request("POST", "/endpoints", options);
860
- },
861
- list: async () => {
862
- return this.request("GET", "/endpoints");
863
- },
864
- get: async (slug) => {
865
- validatePathSegment(slug, "slug");
866
- return this.request("GET", `/endpoints/${slug}`);
867
- },
868
- update: async (slug, options) => {
869
- validatePathSegment(slug, "slug");
870
- if (options.mockResponse && options.mockResponse !== null) {
871
- const { status } = options.mockResponse;
872
- if (!Number.isInteger(status) || status < 100 || status > 599) {
873
- throw new Error(`Invalid mock response status: ${status}. Must be an integer 100-599.`);
874
- }
875
- }
876
- return this.request("PATCH", `/endpoints/${slug}`, options);
877
- },
878
- delete: async (slug) => {
879
- validatePathSegment(slug, "slug");
880
- await this.request("DELETE", `/endpoints/${slug}`);
881
- },
882
- send: async (slug, options = {}) => {
883
- validatePathSegment(slug, "slug");
884
- const rawMethod = (options.method ?? "POST").toUpperCase();
885
- if (!ALLOWED_METHODS.has(rawMethod)) {
886
- throw new Error(
887
- `Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
888
- );
889
- }
890
- const { headers = {}, body } = options;
891
- const method = rawMethod;
892
- const url = `${this.webhookUrl}/w/${slug}`;
893
- const fetchHeaders = { ...headers };
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, delay } = 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
+ if (delay !== void 0 && (!Number.isInteger(delay) || delay < 0 || delay > 3e4)) {
1823
+ throw new Error(`Invalid ${fieldName} delay: ${delay}. Must be an integer 0-30000.`);
1824
+ }
1825
+ }
1826
+ var WebhooksCC = class {
1827
+ constructor(options) {
1828
+ this.endpoints = {
1829
+ create: async (options = {}) => {
1830
+ if (options.mockResponse) {
1831
+ validateMockResponse(options.mockResponse, "mock response");
1832
+ }
1833
+ const body = {};
1834
+ if (options.name !== void 0) {
1835
+ body.name = options.name;
1836
+ }
1837
+ if (options.mockResponse !== void 0) {
1838
+ body.mockResponse = options.mockResponse;
1839
+ }
1840
+ const isEphemeral = options.ephemeral === true || options.expiresIn !== void 0;
1841
+ if (isEphemeral) {
1842
+ body.isEphemeral = true;
1843
+ }
1844
+ if (options.expiresIn !== void 0) {
1845
+ const durationMs = parseDuration(options.expiresIn);
1846
+ if (durationMs <= 0) {
1847
+ throw new Error("expiresIn must be greater than 0");
1848
+ }
1849
+ body.expiresAt = Date.now() + durationMs;
1850
+ }
1851
+ return this.request("POST", "/endpoints", body);
1852
+ },
1853
+ list: async () => {
1854
+ return this.request("GET", "/endpoints");
1855
+ },
1856
+ get: async (slug) => {
1857
+ validatePathSegment(slug, "slug");
1858
+ return this.request("GET", `/endpoints/${slug}`);
1859
+ },
1860
+ update: async (slug, options) => {
1861
+ validatePathSegment(slug, "slug");
1862
+ if (options.mockResponse && options.mockResponse !== null) {
1863
+ validateMockResponse(options.mockResponse, "mock response");
1864
+ }
1865
+ return this.request("PATCH", `/endpoints/${slug}`, options);
1866
+ },
1867
+ delete: async (slug) => {
1868
+ validatePathSegment(slug, "slug");
1869
+ await this.request("DELETE", `/endpoints/${slug}`);
1870
+ },
1871
+ send: async (slug, options = {}) => {
1872
+ validatePathSegment(slug, "slug");
1873
+ const rawMethod = (options.method ?? "POST").toUpperCase();
1874
+ if (!ALLOWED_METHODS.has(rawMethod)) {
1875
+ throw new Error(
1876
+ `Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
1877
+ );
1878
+ }
1879
+ const { headers = {}, body } = options;
1880
+ const method = rawMethod;
1881
+ const url = `${this.webhookUrl}/w/${slug}`;
1882
+ const fetchHeaders = { ...headers };
894
1883
  const hasContentType = Object.keys(fetchHeaders).some(
895
1884
  (k) => k.toLowerCase() === "content-type"
896
1885
  );
@@ -914,6 +1903,20 @@ var WebhooksCC = class {
914
1903
  return this.endpoints.send(slug, sendOptions);
915
1904
  }
916
1905
  };
1906
+ this.templates = {
1907
+ listProviders: () => {
1908
+ return [...TEMPLATE_PROVIDERS];
1909
+ },
1910
+ get: (provider) => {
1911
+ return TEMPLATE_METADATA[provider];
1912
+ }
1913
+ };
1914
+ this.usage = async () => {
1915
+ return this.request("GET", "/usage");
1916
+ };
1917
+ this.flow = () => {
1918
+ return new WebhookFlowBuilder(this);
1919
+ };
917
1920
  /**
918
1921
  * Build a request without sending it. Returns the computed method, URL,
919
1922
  * headers, and body — including any provider signatures. Useful for
@@ -1043,10 +2046,86 @@ var WebhooksCC = class {
1043
2046
  `/endpoints/${endpointSlug}/requests${query ? `?${query}` : ""}`
1044
2047
  );
1045
2048
  },
2049
+ listPaginated: async (endpointSlug, options = {}) => {
2050
+ validatePathSegment(endpointSlug, "endpointSlug");
2051
+ return this.request(
2052
+ "GET",
2053
+ `/endpoints/${endpointSlug}/requests/paginated${buildPaginatedListQuery(options)}`
2054
+ );
2055
+ },
1046
2056
  get: async (requestId) => {
1047
2057
  validatePathSegment(requestId, "requestId");
1048
2058
  return this.request("GET", `/requests/${requestId}`);
1049
2059
  },
2060
+ waitForAll: async (endpointSlug, options) => {
2061
+ validatePathSegment(endpointSlug, "endpointSlug");
2062
+ const listLimit = Math.min(1e3, Math.max(100, Math.floor(options.count) * 2));
2063
+ return collectMatchingRequests(
2064
+ (since) => this.requests.list(endpointSlug, {
2065
+ since,
2066
+ limit: listLimit
2067
+ }),
2068
+ options
2069
+ );
2070
+ },
2071
+ search: async (filters = {}) => {
2072
+ return this.request(
2073
+ "GET",
2074
+ `/search/requests${buildSearchQuery(filters, true)}`
2075
+ );
2076
+ },
2077
+ count: async (filters = {}) => {
2078
+ const response = await this.request(
2079
+ "GET",
2080
+ `/search/requests/count${buildSearchQuery(filters, false)}`
2081
+ );
2082
+ return response.count;
2083
+ },
2084
+ clear: async (endpointSlug, options = {}) => {
2085
+ validatePathSegment(endpointSlug, "endpointSlug");
2086
+ await this.request(
2087
+ "DELETE",
2088
+ `/endpoints/${endpointSlug}/requests${buildClearQuery(options)}`
2089
+ );
2090
+ },
2091
+ export: async (endpointSlug, options) => {
2092
+ validatePathSegment(endpointSlug, "endpointSlug");
2093
+ const endpoint = await this.endpoints.get(endpointSlug);
2094
+ const endpointUrl = endpoint.url ?? `${this.webhookUrl}/w/${endpoint.slug}`;
2095
+ const requests = [];
2096
+ const pageSize = normalizeExportPageSize(options.limit);
2097
+ let cursor;
2098
+ while (true) {
2099
+ const remaining = options.limit !== void 0 ? Math.max(0, options.limit - requests.length) : pageSize;
2100
+ if (options.limit !== void 0 && remaining === 0) {
2101
+ break;
2102
+ }
2103
+ const page = await this.requests.listPaginated(endpointSlug, {
2104
+ limit: options.limit !== void 0 ? Math.min(pageSize, remaining) : pageSize,
2105
+ cursor
2106
+ });
2107
+ for (const request of page.items) {
2108
+ if (options.since !== void 0 && request.receivedAt <= options.since) {
2109
+ continue;
2110
+ }
2111
+ requests.push(request);
2112
+ if (options.limit !== void 0 && requests.length >= options.limit) {
2113
+ break;
2114
+ }
2115
+ }
2116
+ if (!page.hasMore || !page.cursor) {
2117
+ break;
2118
+ }
2119
+ if (options.limit !== void 0 && requests.length >= options.limit) {
2120
+ break;
2121
+ }
2122
+ cursor = page.cursor;
2123
+ }
2124
+ if (options.format === "curl") {
2125
+ return buildCurlExport(endpointUrl, requests);
2126
+ }
2127
+ return buildHarExport(endpointUrl, requests, SDK_VERSION);
2128
+ },
1050
2129
  /**
1051
2130
  * Polls for incoming requests until one matches or timeout expires.
1052
2131
  *
@@ -1060,47 +2139,11 @@ var WebhooksCC = class {
1060
2139
  * @throws Error if timeout expires or max iterations (10000) reached
1061
2140
  */
1062
2141
  waitFor: async (endpointSlug, options = {}) => {
1063
- validatePathSegment(endpointSlug, "endpointSlug");
1064
- const timeout = parseDuration(options.timeout ?? 3e4);
1065
- const rawPollInterval = parseDuration(options.pollInterval ?? 500);
1066
- const { match } = options;
1067
- const safePollInterval = Math.max(
1068
- MIN_POLL_INTERVAL,
1069
- Math.min(MAX_POLL_INTERVAL, rawPollInterval)
1070
- );
1071
- const start = Date.now();
1072
- let lastChecked = start - 5 * 60 * 1e3;
1073
- const MAX_ITERATIONS = 1e4;
1074
- let iterations = 0;
1075
- while (Date.now() - start < timeout && iterations < MAX_ITERATIONS) {
1076
- iterations++;
1077
- const checkTime = Date.now();
1078
- try {
1079
- const requests = await this.requests.list(endpointSlug, {
1080
- since: lastChecked,
1081
- limit: 100
1082
- });
1083
- lastChecked = checkTime;
1084
- const matched = match ? requests.find(match) : requests[0];
1085
- if (matched) {
1086
- return matched;
1087
- }
1088
- } catch (error) {
1089
- if (error instanceof WebhooksCCError) {
1090
- if (error instanceof UnauthorizedError) {
1091
- throw error;
1092
- }
1093
- if (error instanceof NotFoundError) {
1094
- throw error;
1095
- }
1096
- if (error.statusCode < 500 && !(error instanceof RateLimitError)) {
1097
- throw error;
1098
- }
1099
- }
1100
- }
1101
- await sleep(safePollInterval);
1102
- }
1103
- throw new TimeoutError(timeout);
2142
+ const [request] = await this.requests.waitForAll(endpointSlug, {
2143
+ ...options,
2144
+ count: 1
2145
+ });
2146
+ return request;
1104
2147
  },
1105
2148
  /**
1106
2149
  * Replay a captured request to a target URL.
@@ -1143,20 +2186,25 @@ var WebhooksCC = class {
1143
2186
  * The connection is closed when the iterator is broken, the signal is aborted,
1144
2187
  * or the timeout expires.
1145
2188
  *
1146
- * No automatic reconnection if the connection drops, the iterator ends.
2189
+ * Reconnection is opt-in and resumes from the last yielded request timestamp.
1147
2190
  */
1148
2191
  subscribe: (slug, options = {}) => {
1149
2192
  validatePathSegment(slug, "slug");
1150
- const { signal, timeout } = options;
2193
+ const { signal, timeout, reconnect = false, onReconnect } = options;
1151
2194
  const baseUrl = this.baseUrl;
1152
2195
  const apiKey = this.apiKey;
1153
2196
  const timeoutMs = timeout !== void 0 ? parseDuration(timeout) : void 0;
2197
+ const maxReconnectAttempts = Math.max(0, Math.floor(options.maxReconnectAttempts ?? 5));
2198
+ const reconnectBackoffMs = normalizeReconnectBackoff(options.reconnectBackoffMs);
1154
2199
  return {
1155
2200
  [Symbol.asyncIterator]() {
1156
2201
  const controller = new AbortController();
1157
2202
  let timeoutId;
1158
2203
  let iterator = null;
1159
2204
  let started = false;
2205
+ let reconnectAttempts = 0;
2206
+ let lastReceivedAt;
2207
+ const seenRequestIds = /* @__PURE__ */ new Set();
1160
2208
  const onAbort = () => controller.abort();
1161
2209
  if (signal) {
1162
2210
  if (signal.aborted) {
@@ -1173,7 +2221,10 @@ var WebhooksCC = class {
1173
2221
  if (signal) signal.removeEventListener("abort", onAbort);
1174
2222
  };
1175
2223
  const start = async () => {
1176
- const url = `${baseUrl}/api/stream/${slug}`;
2224
+ const url = `${baseUrl}${buildStreamPath(
2225
+ slug,
2226
+ lastReceivedAt !== void 0 ? lastReceivedAt - 1 : void 0
2227
+ )}`;
1177
2228
  const connectController = new AbortController();
1178
2229
  const connectTimeout = setTimeout(() => connectController.abort(), 3e4);
1179
2230
  controller.signal.addEventListener("abort", () => connectController.abort(), {
@@ -1189,12 +2240,10 @@ var WebhooksCC = class {
1189
2240
  clearTimeout(connectTimeout);
1190
2241
  }
1191
2242
  if (!response.ok) {
1192
- cleanup();
1193
2243
  const text = await response.text();
1194
2244
  throw mapStatusToError(response.status, text, response);
1195
2245
  }
1196
2246
  if (!response.body) {
1197
- cleanup();
1198
2247
  throw new Error("SSE response has no body");
1199
2248
  }
1200
2249
  controller.signal.addEventListener(
@@ -1207,6 +2256,24 @@ var WebhooksCC = class {
1207
2256
  );
1208
2257
  return parseSSE(response.body);
1209
2258
  };
2259
+ const reconnectStream = async () => {
2260
+ if (!reconnect || reconnectAttempts >= maxReconnectAttempts || controller.signal.aborted) {
2261
+ cleanup();
2262
+ return false;
2263
+ }
2264
+ reconnectAttempts++;
2265
+ try {
2266
+ onReconnect?.(reconnectAttempts);
2267
+ } catch {
2268
+ }
2269
+ await sleep(reconnectBackoffMs * 2 ** (reconnectAttempts - 1));
2270
+ if (controller.signal.aborted) {
2271
+ cleanup();
2272
+ return false;
2273
+ }
2274
+ iterator = await start();
2275
+ return true;
2276
+ };
1210
2277
  return {
1211
2278
  [Symbol.asyncIterator]() {
1212
2279
  return this;
@@ -1220,32 +2287,26 @@ var WebhooksCC = class {
1220
2287
  while (iterator) {
1221
2288
  const { done, value } = await iterator.next();
1222
2289
  if (done) {
1223
- cleanup();
2290
+ iterator = null;
2291
+ if (await reconnectStream()) {
2292
+ continue;
2293
+ }
1224
2294
  return { done: true, value: void 0 };
1225
2295
  }
2296
+ reconnectAttempts = 0;
1226
2297
  if (value.event === "request") {
1227
- try {
1228
- const data = JSON.parse(value.data);
1229
- if (!data.endpointId || !data.method || !data.headers || !data.receivedAt) {
1230
- continue;
1231
- }
1232
- const req = {
1233
- id: data._id ?? data.id,
1234
- endpointId: data.endpointId,
1235
- method: data.method,
1236
- path: data.path ?? "/",
1237
- headers: data.headers,
1238
- body: data.body ?? void 0,
1239
- queryParams: data.queryParams ?? {},
1240
- contentType: data.contentType ?? void 0,
1241
- ip: data.ip ?? "unknown",
1242
- size: data.size ?? 0,
1243
- receivedAt: data.receivedAt
1244
- };
1245
- return { done: false, value: req };
1246
- } catch {
2298
+ const req = parseStreamRequest(value.data);
2299
+ if (!req) {
1247
2300
  continue;
1248
2301
  }
2302
+ if (req.id && seenRequestIds.has(req.id)) {
2303
+ continue;
2304
+ }
2305
+ if (req.id) {
2306
+ seenRequestIds.add(req.id);
2307
+ }
2308
+ lastReceivedAt = req.receivedAt;
2309
+ return { done: false, value: req };
1249
2310
  }
1250
2311
  if (value.event === "timeout" || value.event === "endpoint_deleted") {
1251
2312
  cleanup();
@@ -1255,11 +2316,17 @@ var WebhooksCC = class {
1255
2316
  cleanup();
1256
2317
  return { done: true, value: void 0 };
1257
2318
  } catch (error) {
1258
- cleanup();
1259
- controller.abort();
1260
2319
  if (error instanceof Error && error.name === "AbortError") {
2320
+ cleanup();
2321
+ controller.abort();
1261
2322
  return { done: true, value: void 0 };
1262
2323
  }
2324
+ iterator = null;
2325
+ if (shouldReconnectStreamError(error) && await reconnectStream()) {
2326
+ return this.next();
2327
+ }
2328
+ cleanup();
2329
+ controller.abort();
1263
2330
  throw error;
1264
2331
  }
1265
2332
  },
@@ -1283,67 +2350,100 @@ var WebhooksCC = class {
1283
2350
  this.baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL);
1284
2351
  this.webhookUrl = stripTrailingSlashes(options.webhookUrl ?? DEFAULT_WEBHOOK_URL);
1285
2352
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
2353
+ this.retry = normalizeRetryOptions(options.retry);
1286
2354
  this.hooks = options.hooks ?? {};
1287
2355
  }
1288
2356
  async request(method, path, body) {
1289
2357
  const url = `${this.baseUrl}/api${path}`;
1290
- const controller = new AbortController();
1291
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1292
- const start = Date.now();
1293
- try {
1294
- this.hooks.onRequest?.({ method, url });
1295
- } catch {
1296
- }
1297
- try {
1298
- const response = await fetch(url, {
1299
- method,
1300
- headers: {
1301
- Authorization: `Bearer ${this.apiKey}`,
1302
- "Content-Type": "application/json"
1303
- },
1304
- body: body ? JSON.stringify(body) : void 0,
1305
- signal: controller.signal
1306
- });
1307
- const durationMs = Date.now() - start;
1308
- if (!response.ok) {
1309
- const errorText = await response.text();
1310
- const sanitizedError = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
1311
- const error = mapStatusToError(response.status, sanitizedError, response);
1312
- try {
1313
- this.hooks.onError?.({ method, url, error, durationMs });
1314
- } catch {
1315
- }
1316
- throw error;
1317
- }
2358
+ let attempt = 0;
2359
+ while (true) {
2360
+ attempt++;
2361
+ const controller = new AbortController();
2362
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2363
+ const start = Date.now();
1318
2364
  try {
1319
- this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
2365
+ this.hooks.onRequest?.({ method, url });
1320
2366
  } catch {
1321
2367
  }
1322
- if (response.status === 204 || response.headers.get("content-length") === "0") {
1323
- return void 0;
1324
- }
1325
- const contentType = response.headers.get("content-type");
1326
- if (contentType && !contentType.includes("application/json")) {
1327
- throw new Error(`Unexpected content type: ${contentType}`);
1328
- }
1329
- return response.json();
1330
- } catch (error) {
1331
- if (error instanceof Error && error.name === "AbortError") {
1332
- const timeoutError = new TimeoutError(this.timeout);
2368
+ try {
2369
+ const response = await fetch(url, {
2370
+ method,
2371
+ headers: {
2372
+ Authorization: `Bearer ${this.apiKey}`,
2373
+ "Content-Type": "application/json"
2374
+ },
2375
+ body: body ? JSON.stringify(body) : void 0,
2376
+ signal: controller.signal
2377
+ });
2378
+ const durationMs = Date.now() - start;
2379
+ if (!response.ok) {
2380
+ const errorText = await response.text();
2381
+ const sanitizedError = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
2382
+ const error = mapStatusToError(response.status, sanitizedError, response);
2383
+ try {
2384
+ this.hooks.onError?.({ method, url, error, durationMs });
2385
+ } catch {
2386
+ }
2387
+ if (attempt < this.retry.maxAttempts && this.retry.retryOn.has(response.status)) {
2388
+ const retryDelayMs = response.status === 429 && parseRetryAfterHeader(response) !== void 0 ? (parseRetryAfterHeader(response) ?? 0) * 1e3 : this.retry.backoffMs * 2 ** (attempt - 1);
2389
+ await sleep(retryDelayMs);
2390
+ continue;
2391
+ }
2392
+ throw error;
2393
+ }
1333
2394
  try {
1334
- this.hooks.onError?.({
1335
- method,
1336
- url,
1337
- error: timeoutError,
1338
- durationMs: Date.now() - start
1339
- });
2395
+ this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
1340
2396
  } catch {
1341
2397
  }
1342
- throw timeoutError;
2398
+ if (response.status === 204 || response.headers.get("content-length") === "0") {
2399
+ return void 0;
2400
+ }
2401
+ const contentType = response.headers.get("content-type");
2402
+ if (contentType && !contentType.includes("application/json")) {
2403
+ throw new Error(`Unexpected content type: ${contentType}`);
2404
+ }
2405
+ return response.json();
2406
+ } catch (error) {
2407
+ if (error instanceof WebhooksCCError) {
2408
+ throw error;
2409
+ }
2410
+ if (error instanceof Error && error.name === "AbortError") {
2411
+ const timeoutError = new TimeoutError(this.timeout);
2412
+ try {
2413
+ this.hooks.onError?.({
2414
+ method,
2415
+ url,
2416
+ error: timeoutError,
2417
+ durationMs: Date.now() - start
2418
+ });
2419
+ } catch {
2420
+ }
2421
+ if (attempt < this.retry.maxAttempts) {
2422
+ await sleep(this.retry.backoffMs * 2 ** (attempt - 1));
2423
+ continue;
2424
+ }
2425
+ throw timeoutError;
2426
+ }
2427
+ const isNetworkError = error instanceof Error;
2428
+ if (isNetworkError) {
2429
+ try {
2430
+ this.hooks.onError?.({
2431
+ method,
2432
+ url,
2433
+ error,
2434
+ durationMs: Date.now() - start
2435
+ });
2436
+ } catch {
2437
+ }
2438
+ }
2439
+ if (attempt < this.retry.maxAttempts && isNetworkError) {
2440
+ await sleep(this.retry.backoffMs * 2 ** (attempt - 1));
2441
+ continue;
2442
+ }
2443
+ throw error;
2444
+ } finally {
2445
+ clearTimeout(timeoutId);
1343
2446
  }
1344
- throw error;
1345
- } finally {
1346
- clearTimeout(timeoutId);
1347
2447
  }
1348
2448
  }
1349
2449
  /** Returns a static description of all SDK operations (no API call). */
@@ -1353,7 +2453,12 @@ var WebhooksCC = class {
1353
2453
  endpoints: {
1354
2454
  create: {
1355
2455
  description: "Create a webhook endpoint",
1356
- params: { name: "string?" }
2456
+ params: {
2457
+ name: "string?",
2458
+ ephemeral: "boolean?",
2459
+ expiresIn: "number|string?",
2460
+ mockResponse: "object?"
2461
+ }
1357
2462
  },
1358
2463
  list: {
1359
2464
  description: "List all endpoints",
@@ -1379,18 +2484,38 @@ var WebhooksCC = class {
1379
2484
  description: "Send a provider template webhook with signed headers",
1380
2485
  params: {
1381
2486
  slug: "string",
1382
- provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"',
2487
+ provider: PROVIDER_PARAM_DESCRIPTION,
1383
2488
  template: "string?",
1384
2489
  secret: "string",
1385
2490
  event: "string?"
1386
2491
  }
1387
2492
  }
1388
2493
  },
2494
+ templates: {
2495
+ listProviders: {
2496
+ description: "List supported template providers",
2497
+ params: {}
2498
+ },
2499
+ get: {
2500
+ description: "Get static metadata for a template provider",
2501
+ params: {
2502
+ provider: PROVIDER_PARAM_DESCRIPTION
2503
+ }
2504
+ }
2505
+ },
2506
+ usage: {
2507
+ description: "Get current request usage and remaining quota",
2508
+ params: {}
2509
+ },
2510
+ flow: {
2511
+ description: "Create a fluent webhook flow builder for common capture/verify/replay flows",
2512
+ params: {}
2513
+ },
1389
2514
  sendTo: {
1390
2515
  description: "Send a webhook directly to any URL with optional provider signing",
1391
2516
  params: {
1392
2517
  url: "string",
1393
- provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
2518
+ provider: `${PROVIDER_PARAM_DESCRIPTION}?`,
1394
2519
  secret: "string?",
1395
2520
  body: "unknown?",
1396
2521
  headers: "Record<string, string>?"
@@ -1400,7 +2525,7 @@ var WebhooksCC = class {
1400
2525
  description: "Build a request without sending it \u2014 returns computed method, URL, headers, and body including provider signatures",
1401
2526
  params: {
1402
2527
  url: "string",
1403
- provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
2528
+ provider: `${PROVIDER_PARAM_DESCRIPTION}?`,
1404
2529
  secret: "string?",
1405
2530
  body: "unknown?",
1406
2531
  headers: "Record<string, string>?"
@@ -1411,10 +2536,24 @@ var WebhooksCC = class {
1411
2536
  description: "List captured requests",
1412
2537
  params: { endpointSlug: "string", limit: "number?", since: "number?" }
1413
2538
  },
2539
+ listPaginated: {
2540
+ description: "List captured requests with cursor-based pagination",
2541
+ params: { endpointSlug: "string", limit: "number?", cursor: "string?" }
2542
+ },
1414
2543
  get: {
1415
2544
  description: "Get request by ID",
1416
2545
  params: { requestId: "string" }
1417
2546
  },
2547
+ waitForAll: {
2548
+ description: "Poll until multiple matching requests arrive",
2549
+ params: {
2550
+ endpointSlug: "string",
2551
+ count: "number",
2552
+ timeout: "number|string?",
2553
+ pollInterval: "number|string?",
2554
+ match: "function?"
2555
+ }
2556
+ },
1418
2557
  waitFor: {
1419
2558
  description: "Poll until a matching request arrives",
1420
2559
  params: {
@@ -1425,11 +2564,58 @@ var WebhooksCC = class {
1425
2564
  },
1426
2565
  subscribe: {
1427
2566
  description: "Stream requests via SSE",
1428
- params: { slug: "string", signal: "AbortSignal?", timeout: "number|string?" }
2567
+ params: {
2568
+ slug: "string",
2569
+ signal: "AbortSignal?",
2570
+ timeout: "number|string?",
2571
+ reconnect: "boolean?",
2572
+ maxReconnectAttempts: "number?",
2573
+ reconnectBackoffMs: "number|string?",
2574
+ onReconnect: "function?"
2575
+ }
1429
2576
  },
1430
2577
  replay: {
1431
2578
  description: "Replay a captured request to a URL",
1432
2579
  params: { requestId: "string", targetUrl: "string" }
2580
+ },
2581
+ export: {
2582
+ description: "Export captured requests as HAR or cURL commands",
2583
+ params: {
2584
+ endpointSlug: "string",
2585
+ format: '"har"|"curl"',
2586
+ limit: "number?",
2587
+ since: "number?"
2588
+ }
2589
+ },
2590
+ search: {
2591
+ description: "Search retained requests across path, body, and headers",
2592
+ params: {
2593
+ slug: "string?",
2594
+ method: "string?",
2595
+ q: "string?",
2596
+ from: "number|string?",
2597
+ to: "number|string?",
2598
+ limit: "number?",
2599
+ offset: "number?",
2600
+ order: '"asc"|"desc"?'
2601
+ }
2602
+ },
2603
+ count: {
2604
+ description: "Count retained requests matching search filters",
2605
+ params: {
2606
+ slug: "string?",
2607
+ method: "string?",
2608
+ q: "string?",
2609
+ from: "number|string?",
2610
+ to: "number|string?"
2611
+ }
2612
+ },
2613
+ clear: {
2614
+ description: "Delete captured requests for an endpoint",
2615
+ params: {
2616
+ endpointSlug: "string",
2617
+ before: "number|string?"
2618
+ }
1433
2619
  }
1434
2620
  }
1435
2621
  };
@@ -1445,6 +2631,49 @@ function stripTrailingSlashes(url) {
1445
2631
  }
1446
2632
 
1447
2633
  // src/helpers.ts
2634
+ function getHeaderValue(headers, name) {
2635
+ const target = name.toLowerCase();
2636
+ for (const [key, value] of Object.entries(headers)) {
2637
+ if (key.toLowerCase() === target) {
2638
+ return value;
2639
+ }
2640
+ }
2641
+ return void 0;
2642
+ }
2643
+ function getContentType(request) {
2644
+ return request.contentType ?? getHeaderValue(request.headers, "content-type");
2645
+ }
2646
+ function normalizeContentType(value) {
2647
+ if (!value) {
2648
+ return void 0;
2649
+ }
2650
+ return value.split(";", 1)[0]?.trim().toLowerCase();
2651
+ }
2652
+ function getJsonPathValue(body, path) {
2653
+ const parts = path.split(".");
2654
+ let current = body;
2655
+ for (const part of parts) {
2656
+ if (current === null || current === void 0) {
2657
+ return void 0;
2658
+ }
2659
+ if (Array.isArray(current)) {
2660
+ const index = Number(part);
2661
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) {
2662
+ return void 0;
2663
+ }
2664
+ current = current[index];
2665
+ continue;
2666
+ }
2667
+ if (typeof current !== "object") {
2668
+ return void 0;
2669
+ }
2670
+ if (!Object.prototype.hasOwnProperty.call(current, part)) {
2671
+ return void 0;
2672
+ }
2673
+ current = current[part];
2674
+ }
2675
+ return current;
2676
+ }
1448
2677
  function parseJsonBody(request) {
1449
2678
  if (!request.body) return void 0;
1450
2679
  try {
@@ -1467,6 +2696,50 @@ function matchJsonField(field, value) {
1467
2696
  return body[field] === value;
1468
2697
  };
1469
2698
  }
2699
+ function parseFormBody(request) {
2700
+ if (!request.body) {
2701
+ return void 0;
2702
+ }
2703
+ const contentType = normalizeContentType(getContentType(request));
2704
+ if (contentType !== "application/x-www-form-urlencoded") {
2705
+ return void 0;
2706
+ }
2707
+ const parsed = {};
2708
+ for (const [key, value] of new URLSearchParams(request.body).entries()) {
2709
+ const existing = parsed[key];
2710
+ if (existing === void 0) {
2711
+ parsed[key] = value;
2712
+ continue;
2713
+ }
2714
+ if (Array.isArray(existing)) {
2715
+ existing.push(value);
2716
+ continue;
2717
+ }
2718
+ parsed[key] = [existing, value];
2719
+ }
2720
+ return parsed;
2721
+ }
2722
+ function parseBody(request) {
2723
+ if (!request.body) {
2724
+ return void 0;
2725
+ }
2726
+ const contentType = normalizeContentType(getContentType(request));
2727
+ if (contentType === "application/json" || contentType?.endsWith("+json")) {
2728
+ const parsed = parseJsonBody(request);
2729
+ return parsed === void 0 ? request.body : parsed;
2730
+ }
2731
+ if (contentType === "application/x-www-form-urlencoded") {
2732
+ return parseFormBody(request);
2733
+ }
2734
+ return request.body;
2735
+ }
2736
+ function extractJsonField(request, path) {
2737
+ const body = parseJsonBody(request);
2738
+ if (body === void 0) {
2739
+ return void 0;
2740
+ }
2741
+ return getJsonPathValue(body, path);
2742
+ }
1470
2743
  function isShopifyWebhook(request) {
1471
2744
  return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-shopify-hmac-sha256");
1472
2745
  }
@@ -1482,23 +2755,85 @@ function isPaddleWebhook(request) {
1482
2755
  function isLinearWebhook(request) {
1483
2756
  return Object.keys(request.headers).some((k) => k.toLowerCase() === "linear-signature");
1484
2757
  }
2758
+ function isDiscordWebhook(request) {
2759
+ const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
2760
+ return keys.includes("x-signature-ed25519") && keys.includes("x-signature-timestamp");
2761
+ }
1485
2762
  function isStandardWebhook(request) {
1486
2763
  const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
1487
2764
  return keys.includes("webhook-id") && keys.includes("webhook-timestamp") && keys.includes("webhook-signature");
1488
2765
  }
1489
2766
 
1490
2767
  // src/matchers.ts
2768
+ function getHeaderValue2(headers, name) {
2769
+ const lowerName = name.toLowerCase();
2770
+ for (const [key, value] of Object.entries(headers)) {
2771
+ if (key.toLowerCase() === lowerName) {
2772
+ return value;
2773
+ }
2774
+ }
2775
+ return void 0;
2776
+ }
2777
+ function globToRegExp(pattern) {
2778
+ let source = "^";
2779
+ for (let index = 0; index < pattern.length; index++) {
2780
+ const char = pattern[index];
2781
+ if (char === "*") {
2782
+ if (pattern[index + 1] === "*") {
2783
+ source += ".*";
2784
+ index++;
2785
+ } else {
2786
+ source += "[^/]*";
2787
+ }
2788
+ continue;
2789
+ }
2790
+ source += /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
2791
+ }
2792
+ source += "$";
2793
+ return new RegExp(source);
2794
+ }
2795
+ function isDeepSubset(expected, actual) {
2796
+ if (Array.isArray(expected)) {
2797
+ return Array.isArray(actual) && expected.every((value, index) => isDeepSubset(value, actual[index]));
2798
+ }
2799
+ if (expected && typeof expected === "object") {
2800
+ if (!actual || typeof actual !== "object" || Array.isArray(actual)) {
2801
+ return false;
2802
+ }
2803
+ return Object.entries(expected).every(([key, value]) => {
2804
+ if (!Object.prototype.hasOwnProperty.call(actual, key)) {
2805
+ return false;
2806
+ }
2807
+ return isDeepSubset(value, actual[key]);
2808
+ });
2809
+ }
2810
+ return Object.is(expected, actual);
2811
+ }
1491
2812
  function matchMethod(method) {
1492
2813
  const upper = method.toUpperCase();
1493
2814
  return (request) => request.method.toUpperCase() === upper;
1494
2815
  }
1495
2816
  function matchHeader(name, value) {
1496
- const lowerName = name.toLowerCase();
1497
2817
  return (request) => {
1498
- const entry = Object.entries(request.headers).find(([k]) => k.toLowerCase() === lowerName);
1499
- if (!entry) return false;
2818
+ const headerValue = getHeaderValue2(request.headers, name);
2819
+ if (headerValue === void 0) return false;
1500
2820
  if (value === void 0) return true;
1501
- return entry[1] === value;
2821
+ return headerValue === value;
2822
+ };
2823
+ }
2824
+ function matchPath(pattern) {
2825
+ const regex = globToRegExp(pattern);
2826
+ return (request) => regex.test(request.path);
2827
+ }
2828
+ function matchQueryParam(key, value) {
2829
+ return (request) => {
2830
+ if (!Object.prototype.hasOwnProperty.call(request.queryParams, key)) {
2831
+ return false;
2832
+ }
2833
+ if (value === void 0) {
2834
+ return true;
2835
+ }
2836
+ return request.queryParams[key] === value;
1502
2837
  };
1503
2838
  }
1504
2839
  function matchBodyPath(path, value) {
@@ -1523,6 +2858,23 @@ function matchBodyPath(path, value) {
1523
2858
  return current === value;
1524
2859
  };
1525
2860
  }
2861
+ function matchBodySubset(subset) {
2862
+ return (request) => {
2863
+ const body = parseJsonBody(request);
2864
+ return isDeepSubset(subset, body);
2865
+ };
2866
+ }
2867
+ function matchContentType(type) {
2868
+ const expected = type.trim().toLowerCase();
2869
+ return (request) => {
2870
+ const raw = request.contentType ?? getHeaderValue2(request.headers, "content-type");
2871
+ if (!raw) {
2872
+ return false;
2873
+ }
2874
+ const normalized = raw.trim().toLowerCase();
2875
+ return normalized === expected || normalized.startsWith(`${expected};`);
2876
+ };
2877
+ }
1526
2878
  function matchAll(first, ...rest) {
1527
2879
  const matchers = [first, ...rest];
1528
2880
  return (request) => matchers.every((m) => m(request));
@@ -1535,10 +2887,15 @@ export {
1535
2887
  ApiError,
1536
2888
  NotFoundError,
1537
2889
  RateLimitError,
2890
+ TEMPLATE_METADATA,
1538
2891
  TimeoutError,
1539
2892
  UnauthorizedError,
2893
+ WebhookFlowBuilder,
1540
2894
  WebhooksCC,
1541
2895
  WebhooksCCError,
2896
+ diffRequests,
2897
+ extractJsonField,
2898
+ isDiscordWebhook,
1542
2899
  isGitHubWebhook,
1543
2900
  isLinearWebhook,
1544
2901
  isPaddleWebhook,
@@ -1550,10 +2907,26 @@ export {
1550
2907
  matchAll,
1551
2908
  matchAny,
1552
2909
  matchBodyPath,
2910
+ matchBodySubset,
2911
+ matchContentType,
1553
2912
  matchHeader,
1554
2913
  matchJsonField,
1555
2914
  matchMethod,
2915
+ matchPath,
2916
+ matchQueryParam,
2917
+ parseBody,
1556
2918
  parseDuration,
2919
+ parseFormBody,
1557
2920
  parseJsonBody,
1558
- parseSSE
2921
+ parseSSE,
2922
+ verifyDiscordSignature,
2923
+ verifyGitHubSignature,
2924
+ verifyLinearSignature,
2925
+ verifyPaddleSignature,
2926
+ verifyShopifySignature,
2927
+ verifySignature,
2928
+ verifySlackSignature,
2929
+ verifyStandardWebhookSignature,
2930
+ verifyStripeSignature,
2931
+ verifyTwilioSignature
1559
2932
  };