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