@webhooks-cc/sdk 0.6.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,858 @@ 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 } = 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 };
894
1880
  const hasContentType = Object.keys(fetchHeaders).some(
895
1881
  (k) => k.toLowerCase() === "content-type"
896
1882
  );
@@ -914,6 +1900,20 @@ var WebhooksCC = class {
914
1900
  return this.endpoints.send(slug, sendOptions);
915
1901
  }
916
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
+ };
917
1917
  /**
918
1918
  * Build a request without sending it. Returns the computed method, URL,
919
1919
  * headers, and body — including any provider signatures. Useful for
@@ -1043,10 +2043,86 @@ var WebhooksCC = class {
1043
2043
  `/endpoints/${endpointSlug}/requests${query ? `?${query}` : ""}`
1044
2044
  );
1045
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
+ },
1046
2053
  get: async (requestId) => {
1047
2054
  validatePathSegment(requestId, "requestId");
1048
2055
  return this.request("GET", `/requests/${requestId}`);
1049
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
+ },
1050
2126
  /**
1051
2127
  * Polls for incoming requests until one matches or timeout expires.
1052
2128
  *
@@ -1060,47 +2136,11 @@ var WebhooksCC = class {
1060
2136
  * @throws Error if timeout expires or max iterations (10000) reached
1061
2137
  */
1062
2138
  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);
2139
+ const [request] = await this.requests.waitForAll(endpointSlug, {
2140
+ ...options,
2141
+ count: 1
2142
+ });
2143
+ return request;
1104
2144
  },
1105
2145
  /**
1106
2146
  * Replay a captured request to a target URL.
@@ -1143,20 +2183,25 @@ var WebhooksCC = class {
1143
2183
  * The connection is closed when the iterator is broken, the signal is aborted,
1144
2184
  * or the timeout expires.
1145
2185
  *
1146
- * No automatic reconnection if the connection drops, the iterator ends.
2186
+ * Reconnection is opt-in and resumes from the last yielded request timestamp.
1147
2187
  */
1148
2188
  subscribe: (slug, options = {}) => {
1149
2189
  validatePathSegment(slug, "slug");
1150
- const { signal, timeout } = options;
2190
+ const { signal, timeout, reconnect = false, onReconnect } = options;
1151
2191
  const baseUrl = this.baseUrl;
1152
2192
  const apiKey = this.apiKey;
1153
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);
1154
2196
  return {
1155
2197
  [Symbol.asyncIterator]() {
1156
2198
  const controller = new AbortController();
1157
2199
  let timeoutId;
1158
2200
  let iterator = null;
1159
2201
  let started = false;
2202
+ let reconnectAttempts = 0;
2203
+ let lastReceivedAt;
2204
+ const seenRequestIds = /* @__PURE__ */ new Set();
1160
2205
  const onAbort = () => controller.abort();
1161
2206
  if (signal) {
1162
2207
  if (signal.aborted) {
@@ -1173,7 +2218,10 @@ var WebhooksCC = class {
1173
2218
  if (signal) signal.removeEventListener("abort", onAbort);
1174
2219
  };
1175
2220
  const start = async () => {
1176
- const url = `${baseUrl}/api/stream/${slug}`;
2221
+ const url = `${baseUrl}${buildStreamPath(
2222
+ slug,
2223
+ lastReceivedAt !== void 0 ? lastReceivedAt - 1 : void 0
2224
+ )}`;
1177
2225
  const connectController = new AbortController();
1178
2226
  const connectTimeout = setTimeout(() => connectController.abort(), 3e4);
1179
2227
  controller.signal.addEventListener("abort", () => connectController.abort(), {
@@ -1189,12 +2237,10 @@ var WebhooksCC = class {
1189
2237
  clearTimeout(connectTimeout);
1190
2238
  }
1191
2239
  if (!response.ok) {
1192
- cleanup();
1193
2240
  const text = await response.text();
1194
2241
  throw mapStatusToError(response.status, text, response);
1195
2242
  }
1196
2243
  if (!response.body) {
1197
- cleanup();
1198
2244
  throw new Error("SSE response has no body");
1199
2245
  }
1200
2246
  controller.signal.addEventListener(
@@ -1207,6 +2253,24 @@ var WebhooksCC = class {
1207
2253
  );
1208
2254
  return parseSSE(response.body);
1209
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
+ };
1210
2274
  return {
1211
2275
  [Symbol.asyncIterator]() {
1212
2276
  return this;
@@ -1220,32 +2284,26 @@ var WebhooksCC = class {
1220
2284
  while (iterator) {
1221
2285
  const { done, value } = await iterator.next();
1222
2286
  if (done) {
1223
- cleanup();
2287
+ iterator = null;
2288
+ if (await reconnectStream()) {
2289
+ continue;
2290
+ }
1224
2291
  return { done: true, value: void 0 };
1225
2292
  }
2293
+ reconnectAttempts = 0;
1226
2294
  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 {
2295
+ const req = parseStreamRequest(value.data);
2296
+ if (!req) {
2297
+ continue;
2298
+ }
2299
+ if (req.id && seenRequestIds.has(req.id)) {
1247
2300
  continue;
1248
2301
  }
2302
+ if (req.id) {
2303
+ seenRequestIds.add(req.id);
2304
+ }
2305
+ lastReceivedAt = req.receivedAt;
2306
+ return { done: false, value: req };
1249
2307
  }
1250
2308
  if (value.event === "timeout" || value.event === "endpoint_deleted") {
1251
2309
  cleanup();
@@ -1255,11 +2313,17 @@ var WebhooksCC = class {
1255
2313
  cleanup();
1256
2314
  return { done: true, value: void 0 };
1257
2315
  } catch (error) {
1258
- cleanup();
1259
- controller.abort();
1260
2316
  if (error instanceof Error && error.name === "AbortError") {
2317
+ cleanup();
2318
+ controller.abort();
1261
2319
  return { done: true, value: void 0 };
1262
2320
  }
2321
+ iterator = null;
2322
+ if (shouldReconnectStreamError(error) && await reconnectStream()) {
2323
+ return this.next();
2324
+ }
2325
+ cleanup();
2326
+ controller.abort();
1263
2327
  throw error;
1264
2328
  }
1265
2329
  },
@@ -1283,67 +2347,100 @@ var WebhooksCC = class {
1283
2347
  this.baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL);
1284
2348
  this.webhookUrl = stripTrailingSlashes(options.webhookUrl ?? DEFAULT_WEBHOOK_URL);
1285
2349
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
2350
+ this.retry = normalizeRetryOptions(options.retry);
1286
2351
  this.hooks = options.hooks ?? {};
1287
2352
  }
1288
2353
  async request(method, path, body) {
1289
2354
  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
- }
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();
1318
2361
  try {
1319
- this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
2362
+ this.hooks.onRequest?.({ method, url });
1320
2363
  } catch {
1321
2364
  }
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);
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
+ }
1333
2391
  try {
1334
- this.hooks.onError?.({
1335
- method,
1336
- url,
1337
- error: timeoutError,
1338
- durationMs: Date.now() - start
1339
- });
2392
+ this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
1340
2393
  } catch {
1341
2394
  }
1342
- 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);
1343
2443
  }
1344
- throw error;
1345
- } finally {
1346
- clearTimeout(timeoutId);
1347
2444
  }
1348
2445
  }
1349
2446
  /** Returns a static description of all SDK operations (no API call). */
@@ -1353,7 +2450,12 @@ var WebhooksCC = class {
1353
2450
  endpoints: {
1354
2451
  create: {
1355
2452
  description: "Create a webhook endpoint",
1356
- params: { name: "string?" }
2453
+ params: {
2454
+ name: "string?",
2455
+ ephemeral: "boolean?",
2456
+ expiresIn: "number|string?",
2457
+ mockResponse: "object?"
2458
+ }
1357
2459
  },
1358
2460
  list: {
1359
2461
  description: "List all endpoints",
@@ -1379,18 +2481,38 @@ var WebhooksCC = class {
1379
2481
  description: "Send a provider template webhook with signed headers",
1380
2482
  params: {
1381
2483
  slug: "string",
1382
- provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"',
2484
+ provider: PROVIDER_PARAM_DESCRIPTION,
1383
2485
  template: "string?",
1384
2486
  secret: "string",
1385
2487
  event: "string?"
1386
2488
  }
1387
2489
  }
1388
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
+ },
1389
2511
  sendTo: {
1390
2512
  description: "Send a webhook directly to any URL with optional provider signing",
1391
2513
  params: {
1392
2514
  url: "string",
1393
- provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
2515
+ provider: `${PROVIDER_PARAM_DESCRIPTION}?`,
1394
2516
  secret: "string?",
1395
2517
  body: "unknown?",
1396
2518
  headers: "Record<string, string>?"
@@ -1400,7 +2522,7 @@ var WebhooksCC = class {
1400
2522
  description: "Build a request without sending it \u2014 returns computed method, URL, headers, and body including provider signatures",
1401
2523
  params: {
1402
2524
  url: "string",
1403
- provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
2525
+ provider: `${PROVIDER_PARAM_DESCRIPTION}?`,
1404
2526
  secret: "string?",
1405
2527
  body: "unknown?",
1406
2528
  headers: "Record<string, string>?"
@@ -1411,10 +2533,24 @@ var WebhooksCC = class {
1411
2533
  description: "List captured requests",
1412
2534
  params: { endpointSlug: "string", limit: "number?", since: "number?" }
1413
2535
  },
2536
+ listPaginated: {
2537
+ description: "List captured requests with cursor-based pagination",
2538
+ params: { endpointSlug: "string", limit: "number?", cursor: "string?" }
2539
+ },
1414
2540
  get: {
1415
2541
  description: "Get request by ID",
1416
2542
  params: { requestId: "string" }
1417
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
+ },
1418
2554
  waitFor: {
1419
2555
  description: "Poll until a matching request arrives",
1420
2556
  params: {
@@ -1425,11 +2561,58 @@ var WebhooksCC = class {
1425
2561
  },
1426
2562
  subscribe: {
1427
2563
  description: "Stream requests via SSE",
1428
- 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
+ }
1429
2573
  },
1430
2574
  replay: {
1431
2575
  description: "Replay a captured request to a URL",
1432
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
+ }
1433
2616
  }
1434
2617
  }
1435
2618
  };
@@ -1445,6 +2628,49 @@ function stripTrailingSlashes(url) {
1445
2628
  }
1446
2629
 
1447
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
+ }
1448
2674
  function parseJsonBody(request) {
1449
2675
  if (!request.body) return void 0;
1450
2676
  try {
@@ -1467,6 +2693,50 @@ function matchJsonField(field, value) {
1467
2693
  return body[field] === value;
1468
2694
  };
1469
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
+ }
1470
2740
  function isShopifyWebhook(request) {
1471
2741
  return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-shopify-hmac-sha256");
1472
2742
  }
@@ -1482,23 +2752,85 @@ function isPaddleWebhook(request) {
1482
2752
  function isLinearWebhook(request) {
1483
2753
  return Object.keys(request.headers).some((k) => k.toLowerCase() === "linear-signature");
1484
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
+ }
1485
2759
  function isStandardWebhook(request) {
1486
2760
  const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
1487
2761
  return keys.includes("webhook-id") && keys.includes("webhook-timestamp") && keys.includes("webhook-signature");
1488
2762
  }
1489
2763
 
1490
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
+ }
1491
2809
  function matchMethod(method) {
1492
2810
  const upper = method.toUpperCase();
1493
2811
  return (request) => request.method.toUpperCase() === upper;
1494
2812
  }
1495
2813
  function matchHeader(name, value) {
1496
- const lowerName = name.toLowerCase();
1497
2814
  return (request) => {
1498
- const entry = Object.entries(request.headers).find(([k]) => k.toLowerCase() === lowerName);
1499
- if (!entry) return false;
2815
+ const headerValue = getHeaderValue2(request.headers, name);
2816
+ if (headerValue === void 0) return false;
1500
2817
  if (value === void 0) return true;
1501
- 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;
1502
2834
  };
1503
2835
  }
1504
2836
  function matchBodyPath(path, value) {
@@ -1523,6 +2855,23 @@ function matchBodyPath(path, value) {
1523
2855
  return current === value;
1524
2856
  };
1525
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
+ }
1526
2875
  function matchAll(first, ...rest) {
1527
2876
  const matchers = [first, ...rest];
1528
2877
  return (request) => matchers.every((m) => m(request));
@@ -1535,10 +2884,15 @@ export {
1535
2884
  ApiError,
1536
2885
  NotFoundError,
1537
2886
  RateLimitError,
2887
+ TEMPLATE_METADATA,
1538
2888
  TimeoutError,
1539
2889
  UnauthorizedError,
2890
+ WebhookFlowBuilder,
1540
2891
  WebhooksCC,
1541
2892
  WebhooksCCError,
2893
+ diffRequests,
2894
+ extractJsonField,
2895
+ isDiscordWebhook,
1542
2896
  isGitHubWebhook,
1543
2897
  isLinearWebhook,
1544
2898
  isPaddleWebhook,
@@ -1550,10 +2904,26 @@ export {
1550
2904
  matchAll,
1551
2905
  matchAny,
1552
2906
  matchBodyPath,
2907
+ matchBodySubset,
2908
+ matchContentType,
1553
2909
  matchHeader,
1554
2910
  matchJsonField,
1555
2911
  matchMethod,
2912
+ matchPath,
2913
+ matchQueryParam,
2914
+ parseBody,
1556
2915
  parseDuration,
2916
+ parseFormBody,
1557
2917
  parseJsonBody,
1558
- parseSSE
2918
+ parseSSE,
2919
+ verifyDiscordSignature,
2920
+ verifyGitHubSignature,
2921
+ verifyLinearSignature,
2922
+ verifyPaddleSignature,
2923
+ verifyShopifySignature,
2924
+ verifySignature,
2925
+ verifySlackSignature,
2926
+ verifyStandardWebhookSignature,
2927
+ verifyStripeSignature,
2928
+ verifyTwilioSignature
1559
2929
  };