@webhooks-cc/sdk 0.6.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +316 -140
- package/dist/chunk-7IMPSHQY.mjs +236 -0
- package/dist/diff-BlDnipA4.d.mts +703 -0
- package/dist/diff-BlDnipA4.d.ts +703 -0
- package/dist/index.d.mts +137 -340
- package/dist/index.d.ts +137 -340
- package/dist/index.js +1779 -168
- package/dist/index.mjs +1664 -291
- 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,18 +1059,7 @@ async function buildTemplateSendOptions(endpointUrl, options) {
|
|
|
767
1059
|
const msgId = options.event ? `msg_${options.event}_${randomHex(8)}` : `msg_${randomHex(16)}`;
|
|
768
1060
|
const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
769
1061
|
const signingInput = `${msgId}.${timestamp}.${body}`;
|
|
770
|
-
|
|
771
|
-
const hadPrefix = rawSecret.startsWith("whsec_");
|
|
772
|
-
if (hadPrefix) {
|
|
773
|
-
rawSecret = rawSecret.slice(6);
|
|
774
|
-
}
|
|
775
|
-
let secretBytes;
|
|
776
|
-
try {
|
|
777
|
-
secretBytes = fromBase64(rawSecret);
|
|
778
|
-
} catch {
|
|
779
|
-
const raw = hadPrefix ? options.secret : rawSecret;
|
|
780
|
-
secretBytes = new TextEncoder().encode(raw);
|
|
781
|
-
}
|
|
1062
|
+
const secretBytes = decodeStandardWebhookSecret(options.secret);
|
|
782
1063
|
const signature = await hmacSignRaw("SHA-256", secretBytes, signingInput);
|
|
783
1064
|
return {
|
|
784
1065
|
method: method2,
|
|
@@ -826,6 +1107,21 @@ async function buildTemplateSendOptions(endpointUrl, options) {
|
|
|
826
1107
|
const signature = await hmacSign("SHA-1", options.secret, signaturePayload);
|
|
827
1108
|
headers["x-twilio-signature"] = toBase64(signature);
|
|
828
1109
|
}
|
|
1110
|
+
if (provider === "slack") {
|
|
1111
|
+
const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
1112
|
+
const signature = await hmacSign("SHA-256", options.secret, `v0:${timestamp}:${built.body}`);
|
|
1113
|
+
headers["x-slack-request-timestamp"] = String(timestamp);
|
|
1114
|
+
headers["x-slack-signature"] = `v0=${toHex(signature)}`;
|
|
1115
|
+
}
|
|
1116
|
+
if (provider === "paddle") {
|
|
1117
|
+
const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
1118
|
+
const signature = await hmacSign("SHA-256", options.secret, `${timestamp}:${built.body}`);
|
|
1119
|
+
headers["paddle-signature"] = `ts=${timestamp};h1=${toHex(signature)}`;
|
|
1120
|
+
}
|
|
1121
|
+
if (provider === "linear") {
|
|
1122
|
+
const signature = await hmacSign("SHA-256", options.secret, built.body);
|
|
1123
|
+
headers["linear-signature"] = `sha256=${toHex(signature)}`;
|
|
1124
|
+
}
|
|
829
1125
|
return {
|
|
830
1126
|
method,
|
|
831
1127
|
headers: {
|
|
@@ -836,11 +1132,579 @@ async function buildTemplateSendOptions(endpointUrl, options) {
|
|
|
836
1132
|
};
|
|
837
1133
|
}
|
|
838
1134
|
|
|
1135
|
+
// src/request-export.ts
|
|
1136
|
+
var OMITTED_EXPORT_HEADERS = /* @__PURE__ */ new Set([
|
|
1137
|
+
"host",
|
|
1138
|
+
"connection",
|
|
1139
|
+
"content-length",
|
|
1140
|
+
"transfer-encoding",
|
|
1141
|
+
"keep-alive",
|
|
1142
|
+
"te",
|
|
1143
|
+
"trailer",
|
|
1144
|
+
"upgrade",
|
|
1145
|
+
"authorization",
|
|
1146
|
+
"cookie",
|
|
1147
|
+
"proxy-authorization",
|
|
1148
|
+
"set-cookie",
|
|
1149
|
+
"accept-encoding",
|
|
1150
|
+
"cdn-loop",
|
|
1151
|
+
"cf-connecting-ip",
|
|
1152
|
+
"cf-ipcountry",
|
|
1153
|
+
"cf-ray",
|
|
1154
|
+
"cf-visitor",
|
|
1155
|
+
"via",
|
|
1156
|
+
"x-forwarded-for",
|
|
1157
|
+
"x-forwarded-host",
|
|
1158
|
+
"x-forwarded-proto",
|
|
1159
|
+
"x-real-ip",
|
|
1160
|
+
"true-client-ip"
|
|
1161
|
+
]);
|
|
1162
|
+
var VALID_HTTP_METHOD = /^[A-Z]+$/;
|
|
1163
|
+
function normalizePath(path) {
|
|
1164
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
1165
|
+
}
|
|
1166
|
+
function buildRequestUrl(endpointUrl, request) {
|
|
1167
|
+
const url = new URL(`${endpointUrl}${normalizePath(request.path)}`);
|
|
1168
|
+
for (const [key, value] of Object.entries(request.queryParams)) {
|
|
1169
|
+
url.searchParams.set(key, value);
|
|
1170
|
+
}
|
|
1171
|
+
return url.toString();
|
|
1172
|
+
}
|
|
1173
|
+
function shouldIncludeHeader(name) {
|
|
1174
|
+
return !OMITTED_EXPORT_HEADERS.has(name.toLowerCase());
|
|
1175
|
+
}
|
|
1176
|
+
function escapeForShellDoubleQuotes(value) {
|
|
1177
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, "\\`").replace(/\$/g, "\\$").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
1178
|
+
}
|
|
1179
|
+
function escapeForShellSingleQuotes(value) {
|
|
1180
|
+
return value.replace(/'/g, "'\\''");
|
|
1181
|
+
}
|
|
1182
|
+
function buildCurlExport(endpointUrl, requests) {
|
|
1183
|
+
return requests.map((request) => {
|
|
1184
|
+
const method = VALID_HTTP_METHOD.test(request.method) ? request.method : "GET";
|
|
1185
|
+
const parts = [`curl -X ${method}`];
|
|
1186
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
1187
|
+
if (!shouldIncludeHeader(key)) {
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
parts.push(`-H "${escapeForShellDoubleQuotes(key)}: ${escapeForShellDoubleQuotes(value)}"`);
|
|
1191
|
+
}
|
|
1192
|
+
if (request.body) {
|
|
1193
|
+
parts.push(`-d '${escapeForShellSingleQuotes(request.body)}'`);
|
|
1194
|
+
}
|
|
1195
|
+
parts.push(`"${escapeForShellDoubleQuotes(buildRequestUrl(endpointUrl, request))}"`);
|
|
1196
|
+
return parts.join(" \\\n ");
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
function buildHarExport(endpointUrl, requests, creatorVersion) {
|
|
1200
|
+
return {
|
|
1201
|
+
log: {
|
|
1202
|
+
version: "1.2",
|
|
1203
|
+
creator: {
|
|
1204
|
+
name: "@webhooks-cc/sdk",
|
|
1205
|
+
version: creatorVersion
|
|
1206
|
+
},
|
|
1207
|
+
entries: requests.map((request) => {
|
|
1208
|
+
const contentType = request.contentType ?? "application/octet-stream";
|
|
1209
|
+
return {
|
|
1210
|
+
startedDateTime: new Date(request.receivedAt).toISOString(),
|
|
1211
|
+
time: 0,
|
|
1212
|
+
request: {
|
|
1213
|
+
method: request.method,
|
|
1214
|
+
url: buildRequestUrl(endpointUrl, request),
|
|
1215
|
+
httpVersion: "HTTP/1.1",
|
|
1216
|
+
headers: Object.entries(request.headers).filter(([key]) => shouldIncludeHeader(key)).map(([name, value]) => ({ name, value })),
|
|
1217
|
+
queryString: Object.entries(request.queryParams).map(([name, value]) => ({
|
|
1218
|
+
name,
|
|
1219
|
+
value
|
|
1220
|
+
})),
|
|
1221
|
+
headersSize: -1,
|
|
1222
|
+
bodySize: request.body ? new TextEncoder().encode(request.body).length : 0,
|
|
1223
|
+
...request.body ? {
|
|
1224
|
+
postData: {
|
|
1225
|
+
mimeType: contentType,
|
|
1226
|
+
text: request.body
|
|
1227
|
+
}
|
|
1228
|
+
} : {}
|
|
1229
|
+
},
|
|
1230
|
+
response: {
|
|
1231
|
+
status: 0,
|
|
1232
|
+
statusText: "",
|
|
1233
|
+
httpVersion: "HTTP/1.1",
|
|
1234
|
+
headers: [],
|
|
1235
|
+
cookies: [],
|
|
1236
|
+
content: {
|
|
1237
|
+
size: 0,
|
|
1238
|
+
mimeType: "x-unknown"
|
|
1239
|
+
},
|
|
1240
|
+
redirectURL: "",
|
|
1241
|
+
headersSize: -1,
|
|
1242
|
+
bodySize: -1
|
|
1243
|
+
},
|
|
1244
|
+
cache: {},
|
|
1245
|
+
timings: {
|
|
1246
|
+
send: 0,
|
|
1247
|
+
wait: 0,
|
|
1248
|
+
receive: 0
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
})
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/verify.ts
|
|
1257
|
+
function requireSecret(secret, functionName) {
|
|
1258
|
+
if (!secret || typeof secret !== "string") {
|
|
1259
|
+
throw new Error(`${functionName} requires a non-empty secret`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function getHeader(headers, name) {
|
|
1263
|
+
const target = name.toLowerCase();
|
|
1264
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1265
|
+
if (key.toLowerCase() === target) {
|
|
1266
|
+
return value;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return void 0;
|
|
1270
|
+
}
|
|
1271
|
+
function normalizeBody(body) {
|
|
1272
|
+
return body ?? "";
|
|
1273
|
+
}
|
|
1274
|
+
function hexToBytes(hex) {
|
|
1275
|
+
const normalized = hex.trim().toLowerCase();
|
|
1276
|
+
if (!/^[a-f0-9]+$/.test(normalized) || normalized.length % 2 !== 0) {
|
|
1277
|
+
throw new Error("Expected a hex-encoded value");
|
|
1278
|
+
}
|
|
1279
|
+
const bytes = new Uint8Array(normalized.length / 2);
|
|
1280
|
+
for (let index = 0; index < normalized.length; index += 2) {
|
|
1281
|
+
bytes[index / 2] = parseInt(normalized.slice(index, index + 2), 16);
|
|
1282
|
+
}
|
|
1283
|
+
return bytes;
|
|
1284
|
+
}
|
|
1285
|
+
function timingSafeEqual(left, right) {
|
|
1286
|
+
if (left.length !== right.length) {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
let mismatch = 0;
|
|
1290
|
+
for (let i = 0; i < left.length; i++) {
|
|
1291
|
+
mismatch |= left.charCodeAt(i) ^ right.charCodeAt(i);
|
|
1292
|
+
}
|
|
1293
|
+
return mismatch === 0;
|
|
1294
|
+
}
|
|
1295
|
+
function parseStripeHeader(signatureHeader) {
|
|
1296
|
+
if (!signatureHeader) {
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
let timestamp;
|
|
1300
|
+
const signatures = [];
|
|
1301
|
+
for (const part of signatureHeader.split(",")) {
|
|
1302
|
+
const trimmed = part.trim();
|
|
1303
|
+
if (!trimmed) {
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
1307
|
+
if (separatorIndex === -1) {
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
1311
|
+
const value = trimmed.slice(separatorIndex + 1).trim();
|
|
1312
|
+
if (!value) {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (key === "t") {
|
|
1316
|
+
timestamp = value;
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
if (key === "v1") {
|
|
1320
|
+
signatures.push(value.toLowerCase());
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (!timestamp || signatures.length === 0) {
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
return { timestamp, signatures };
|
|
1327
|
+
}
|
|
1328
|
+
function parseStandardSignatures(signatureHeader) {
|
|
1329
|
+
if (!signatureHeader) {
|
|
1330
|
+
return [];
|
|
1331
|
+
}
|
|
1332
|
+
const matches = Array.from(
|
|
1333
|
+
signatureHeader.matchAll(/v1,([A-Za-z0-9+/=]+)/g),
|
|
1334
|
+
(match) => match[1]
|
|
1335
|
+
);
|
|
1336
|
+
if (matches.length > 0) {
|
|
1337
|
+
return matches;
|
|
1338
|
+
}
|
|
1339
|
+
const [version, signature] = signatureHeader.split(",", 2);
|
|
1340
|
+
if (version?.trim() === "v1" && signature?.trim()) {
|
|
1341
|
+
return [signature.trim()];
|
|
1342
|
+
}
|
|
1343
|
+
return [];
|
|
1344
|
+
}
|
|
1345
|
+
function parsePaddleSignature(signatureHeader) {
|
|
1346
|
+
if (!signatureHeader) {
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
let timestamp;
|
|
1350
|
+
const signatures = [];
|
|
1351
|
+
for (const part of signatureHeader.split(/[;,]/)) {
|
|
1352
|
+
const trimmed = part.trim();
|
|
1353
|
+
if (!trimmed) {
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
1357
|
+
if (separatorIndex === -1) {
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
const key = trimmed.slice(0, separatorIndex).trim().toLowerCase();
|
|
1361
|
+
const value = trimmed.slice(separatorIndex + 1).trim();
|
|
1362
|
+
if (!value) {
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
if (key === "ts") {
|
|
1366
|
+
timestamp = value;
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
if (key === "h1") {
|
|
1370
|
+
signatures.push(value.toLowerCase());
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (!timestamp || signatures.length === 0) {
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
return { timestamp, signatures };
|
|
1377
|
+
}
|
|
1378
|
+
function toTwilioParams(body) {
|
|
1379
|
+
if (body === void 0) {
|
|
1380
|
+
return [];
|
|
1381
|
+
}
|
|
1382
|
+
if (typeof body === "string") {
|
|
1383
|
+
return Array.from(new URLSearchParams(body).entries());
|
|
1384
|
+
}
|
|
1385
|
+
return Object.entries(body).map(([key, value]) => [key, String(value)]);
|
|
1386
|
+
}
|
|
1387
|
+
async function verifyStripeSignature(body, signatureHeader, secret) {
|
|
1388
|
+
requireSecret(secret, "verifyStripeSignature");
|
|
1389
|
+
const parsed = parseStripeHeader(signatureHeader);
|
|
1390
|
+
if (!parsed) {
|
|
1391
|
+
return false;
|
|
1392
|
+
}
|
|
1393
|
+
const expected = toHex(
|
|
1394
|
+
await hmacSign("SHA-256", secret, `${parsed.timestamp}.${normalizeBody(body)}`)
|
|
1395
|
+
).toLowerCase();
|
|
1396
|
+
return parsed.signatures.some((signature) => timingSafeEqual(signature, expected));
|
|
1397
|
+
}
|
|
1398
|
+
async function verifyGitHubSignature(body, signatureHeader, secret) {
|
|
1399
|
+
requireSecret(secret, "verifyGitHubSignature");
|
|
1400
|
+
if (!signatureHeader) {
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
1403
|
+
const match = signatureHeader.trim().match(/^sha256=(.+)$/i);
|
|
1404
|
+
if (!match) {
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
const expected = toHex(await hmacSign("SHA-256", secret, normalizeBody(body))).toLowerCase();
|
|
1408
|
+
return timingSafeEqual(match[1].toLowerCase(), expected);
|
|
1409
|
+
}
|
|
1410
|
+
async function verifyShopifySignature(body, signatureHeader, secret) {
|
|
1411
|
+
requireSecret(secret, "verifyShopifySignature");
|
|
1412
|
+
if (!signatureHeader) {
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
1415
|
+
const expected = toBase64(await hmacSign("SHA-256", secret, normalizeBody(body)));
|
|
1416
|
+
return timingSafeEqual(signatureHeader.trim(), expected);
|
|
1417
|
+
}
|
|
1418
|
+
async function verifyTwilioSignature(url, body, signatureHeader, secret) {
|
|
1419
|
+
requireSecret(secret, "verifyTwilioSignature");
|
|
1420
|
+
if (!url) {
|
|
1421
|
+
throw new Error("verifyTwilioSignature requires the signed URL");
|
|
1422
|
+
}
|
|
1423
|
+
if (!signatureHeader) {
|
|
1424
|
+
return false;
|
|
1425
|
+
}
|
|
1426
|
+
const expected = toBase64(
|
|
1427
|
+
await hmacSign("SHA-1", secret, buildTwilioSignaturePayload(url, toTwilioParams(body)))
|
|
1428
|
+
);
|
|
1429
|
+
return timingSafeEqual(signatureHeader.trim(), expected);
|
|
1430
|
+
}
|
|
1431
|
+
async function verifySlackSignature(body, headers, secret) {
|
|
1432
|
+
requireSecret(secret, "verifySlackSignature");
|
|
1433
|
+
const signatureHeader = getHeader(headers, "x-slack-signature");
|
|
1434
|
+
const timestamp = getHeader(headers, "x-slack-request-timestamp");
|
|
1435
|
+
if (!signatureHeader || !timestamp) {
|
|
1436
|
+
return false;
|
|
1437
|
+
}
|
|
1438
|
+
const match = signatureHeader.trim().match(/^v0=(.+)$/i);
|
|
1439
|
+
if (!match) {
|
|
1440
|
+
return false;
|
|
1441
|
+
}
|
|
1442
|
+
const expected = toHex(
|
|
1443
|
+
await hmacSign("SHA-256", secret, `v0:${timestamp}:${normalizeBody(body)}`)
|
|
1444
|
+
).toLowerCase();
|
|
1445
|
+
return timingSafeEqual(match[1].toLowerCase(), expected);
|
|
1446
|
+
}
|
|
1447
|
+
async function verifyPaddleSignature(body, signatureHeader, secret) {
|
|
1448
|
+
requireSecret(secret, "verifyPaddleSignature");
|
|
1449
|
+
const parsed = parsePaddleSignature(signatureHeader);
|
|
1450
|
+
if (!parsed) {
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
const expected = toHex(
|
|
1454
|
+
await hmacSign("SHA-256", secret, `${parsed.timestamp}:${normalizeBody(body)}`)
|
|
1455
|
+
).toLowerCase();
|
|
1456
|
+
return parsed.signatures.some((signature) => timingSafeEqual(signature, expected));
|
|
1457
|
+
}
|
|
1458
|
+
async function verifyLinearSignature(body, signatureHeader, secret) {
|
|
1459
|
+
requireSecret(secret, "verifyLinearSignature");
|
|
1460
|
+
if (!signatureHeader) {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
const match = signatureHeader.trim().match(/^(?:sha256=)?(.+)$/i);
|
|
1464
|
+
if (!match) {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
const expected = toHex(await hmacSign("SHA-256", secret, normalizeBody(body))).toLowerCase();
|
|
1468
|
+
return timingSafeEqual(match[1].toLowerCase(), expected);
|
|
1469
|
+
}
|
|
1470
|
+
async function verifyDiscordSignature(body, headers, publicKey) {
|
|
1471
|
+
if (!publicKey || typeof publicKey !== "string") {
|
|
1472
|
+
throw new Error("verifyDiscordSignature requires a non-empty public key");
|
|
1473
|
+
}
|
|
1474
|
+
const signatureHeader = getHeader(headers, "x-signature-ed25519");
|
|
1475
|
+
const timestamp = getHeader(headers, "x-signature-timestamp");
|
|
1476
|
+
if (!signatureHeader || !timestamp) {
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
if (!globalThis.crypto?.subtle) {
|
|
1480
|
+
throw new Error("crypto.subtle is required for Discord signature verification");
|
|
1481
|
+
}
|
|
1482
|
+
try {
|
|
1483
|
+
const publicKeyBytes = hexToBytes(publicKey);
|
|
1484
|
+
const signatureBytes = hexToBytes(signatureHeader);
|
|
1485
|
+
const publicKeyData = new Uint8Array(publicKeyBytes.byteLength);
|
|
1486
|
+
publicKeyData.set(publicKeyBytes);
|
|
1487
|
+
const signatureData = new Uint8Array(signatureBytes.byteLength);
|
|
1488
|
+
signatureData.set(signatureBytes);
|
|
1489
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
1490
|
+
"raw",
|
|
1491
|
+
publicKeyData,
|
|
1492
|
+
{ name: "Ed25519" },
|
|
1493
|
+
false,
|
|
1494
|
+
["verify"]
|
|
1495
|
+
);
|
|
1496
|
+
return await globalThis.crypto.subtle.verify(
|
|
1497
|
+
"Ed25519",
|
|
1498
|
+
key,
|
|
1499
|
+
signatureData,
|
|
1500
|
+
new TextEncoder().encode(`${timestamp}${normalizeBody(body)}`)
|
|
1501
|
+
);
|
|
1502
|
+
} catch {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
async function verifyStandardWebhookSignature(body, headers, secret) {
|
|
1507
|
+
requireSecret(secret, "verifyStandardWebhookSignature");
|
|
1508
|
+
const messageId = getHeader(headers, "webhook-id");
|
|
1509
|
+
const timestamp = getHeader(headers, "webhook-timestamp");
|
|
1510
|
+
const signatureHeader = getHeader(headers, "webhook-signature");
|
|
1511
|
+
if (!messageId || !timestamp || !signatureHeader) {
|
|
1512
|
+
return false;
|
|
1513
|
+
}
|
|
1514
|
+
const expected = toBase64(
|
|
1515
|
+
await hmacSignRaw(
|
|
1516
|
+
"SHA-256",
|
|
1517
|
+
decodeStandardWebhookSecret(secret),
|
|
1518
|
+
`${messageId}.${timestamp}.${normalizeBody(body)}`
|
|
1519
|
+
)
|
|
1520
|
+
);
|
|
1521
|
+
return parseStandardSignatures(signatureHeader).some(
|
|
1522
|
+
(signature) => timingSafeEqual(signature, expected)
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
async function verifySignature(request, options) {
|
|
1526
|
+
let valid = false;
|
|
1527
|
+
if (options.provider === "stripe") {
|
|
1528
|
+
valid = await verifyStripeSignature(
|
|
1529
|
+
request.body,
|
|
1530
|
+
getHeader(request.headers, "stripe-signature"),
|
|
1531
|
+
options.secret
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
if (options.provider === "github") {
|
|
1535
|
+
valid = await verifyGitHubSignature(
|
|
1536
|
+
request.body,
|
|
1537
|
+
getHeader(request.headers, "x-hub-signature-256"),
|
|
1538
|
+
options.secret
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
if (options.provider === "shopify") {
|
|
1542
|
+
valid = await verifyShopifySignature(
|
|
1543
|
+
request.body,
|
|
1544
|
+
getHeader(request.headers, "x-shopify-hmac-sha256"),
|
|
1545
|
+
options.secret
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
if (options.provider === "twilio") {
|
|
1549
|
+
if (!options.url) {
|
|
1550
|
+
throw new Error('verifySignature for provider "twilio" requires options.url');
|
|
1551
|
+
}
|
|
1552
|
+
valid = await verifyTwilioSignature(
|
|
1553
|
+
options.url,
|
|
1554
|
+
request.body,
|
|
1555
|
+
getHeader(request.headers, "x-twilio-signature"),
|
|
1556
|
+
options.secret
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
if (options.provider === "slack") {
|
|
1560
|
+
valid = await verifySlackSignature(request.body, request.headers, options.secret);
|
|
1561
|
+
}
|
|
1562
|
+
if (options.provider === "paddle") {
|
|
1563
|
+
valid = await verifyPaddleSignature(
|
|
1564
|
+
request.body,
|
|
1565
|
+
getHeader(request.headers, "paddle-signature"),
|
|
1566
|
+
options.secret
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
if (options.provider === "linear") {
|
|
1570
|
+
valid = await verifyLinearSignature(
|
|
1571
|
+
request.body,
|
|
1572
|
+
getHeader(request.headers, "linear-signature"),
|
|
1573
|
+
options.secret
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
if (options.provider === "discord") {
|
|
1577
|
+
valid = await verifyDiscordSignature(request.body, request.headers, options.publicKey);
|
|
1578
|
+
}
|
|
1579
|
+
if (options.provider === "standard-webhooks") {
|
|
1580
|
+
valid = await verifyStandardWebhookSignature(request.body, request.headers, options.secret);
|
|
1581
|
+
}
|
|
1582
|
+
return { valid };
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// src/flow.ts
|
|
1586
|
+
var WebhookFlowBuilder = class {
|
|
1587
|
+
constructor(client) {
|
|
1588
|
+
this.client = client;
|
|
1589
|
+
this.createOptions = {};
|
|
1590
|
+
this.deleteAfterRun = false;
|
|
1591
|
+
}
|
|
1592
|
+
createEndpoint(options = {}) {
|
|
1593
|
+
this.createOptions = { ...this.createOptions, ...options };
|
|
1594
|
+
return this;
|
|
1595
|
+
}
|
|
1596
|
+
setMock(mockResponse) {
|
|
1597
|
+
this.mockResponse = mockResponse;
|
|
1598
|
+
return this;
|
|
1599
|
+
}
|
|
1600
|
+
send(options = {}) {
|
|
1601
|
+
this.sendStep = { kind: "send", options };
|
|
1602
|
+
return this;
|
|
1603
|
+
}
|
|
1604
|
+
sendTemplate(options) {
|
|
1605
|
+
this.sendStep = { kind: "sendTemplate", options };
|
|
1606
|
+
return this;
|
|
1607
|
+
}
|
|
1608
|
+
waitForCapture(options = {}) {
|
|
1609
|
+
this.waitOptions = options;
|
|
1610
|
+
return this;
|
|
1611
|
+
}
|
|
1612
|
+
verifySignature(options) {
|
|
1613
|
+
this.verificationOptions = options;
|
|
1614
|
+
return this;
|
|
1615
|
+
}
|
|
1616
|
+
replayTo(targetUrl) {
|
|
1617
|
+
this.replayTargetUrl = targetUrl;
|
|
1618
|
+
return this;
|
|
1619
|
+
}
|
|
1620
|
+
cleanup() {
|
|
1621
|
+
this.deleteAfterRun = true;
|
|
1622
|
+
return this;
|
|
1623
|
+
}
|
|
1624
|
+
async run() {
|
|
1625
|
+
let endpoint;
|
|
1626
|
+
let result;
|
|
1627
|
+
let request;
|
|
1628
|
+
try {
|
|
1629
|
+
endpoint = await this.client.endpoints.create(this.createOptions);
|
|
1630
|
+
if (this.mockResponse !== void 0) {
|
|
1631
|
+
await this.client.endpoints.update(endpoint.slug, {
|
|
1632
|
+
mockResponse: this.mockResponse
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
if (this.sendStep?.kind === "send") {
|
|
1636
|
+
await this.client.endpoints.send(endpoint.slug, this.sendStep.options);
|
|
1637
|
+
}
|
|
1638
|
+
if (this.sendStep?.kind === "sendTemplate") {
|
|
1639
|
+
await this.client.endpoints.sendTemplate(endpoint.slug, this.sendStep.options);
|
|
1640
|
+
}
|
|
1641
|
+
if (this.waitOptions) {
|
|
1642
|
+
request = await this.client.requests.waitFor(endpoint.slug, this.waitOptions);
|
|
1643
|
+
}
|
|
1644
|
+
let verification;
|
|
1645
|
+
if (this.verificationOptions) {
|
|
1646
|
+
if (!request) {
|
|
1647
|
+
throw new Error("Flow verification requires waitForCapture() to run first");
|
|
1648
|
+
}
|
|
1649
|
+
const resolvedUrl = typeof this.verificationOptions.url === "function" ? this.verificationOptions.url(endpoint, request) : this.verificationOptions.url ?? (this.verificationOptions.provider === "twilio" ? endpoint.url : void 0);
|
|
1650
|
+
if (this.verificationOptions.provider === "discord") {
|
|
1651
|
+
verification = await verifySignature(request, {
|
|
1652
|
+
provider: "discord",
|
|
1653
|
+
publicKey: this.verificationOptions.publicKey
|
|
1654
|
+
});
|
|
1655
|
+
} else {
|
|
1656
|
+
verification = await verifySignature(request, {
|
|
1657
|
+
provider: this.verificationOptions.provider,
|
|
1658
|
+
secret: this.verificationOptions.secret,
|
|
1659
|
+
...resolvedUrl ? { url: resolvedUrl } : {}
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
let replayResponse;
|
|
1664
|
+
if (this.replayTargetUrl) {
|
|
1665
|
+
if (!request) {
|
|
1666
|
+
throw new Error("Flow replay requires waitForCapture() to run first");
|
|
1667
|
+
}
|
|
1668
|
+
replayResponse = await this.client.requests.replay(request.id, this.replayTargetUrl);
|
|
1669
|
+
}
|
|
1670
|
+
result = {
|
|
1671
|
+
endpoint,
|
|
1672
|
+
request,
|
|
1673
|
+
verification,
|
|
1674
|
+
replayResponse,
|
|
1675
|
+
cleanedUp: false
|
|
1676
|
+
};
|
|
1677
|
+
return result;
|
|
1678
|
+
} finally {
|
|
1679
|
+
if (this.deleteAfterRun && endpoint) {
|
|
1680
|
+
try {
|
|
1681
|
+
await this.client.endpoints.delete(endpoint.slug);
|
|
1682
|
+
if (result) {
|
|
1683
|
+
result.cleanedUp = true;
|
|
1684
|
+
}
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
if (error instanceof NotFoundError) {
|
|
1687
|
+
if (result) {
|
|
1688
|
+
result.cleanedUp = true;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
|
|
839
1697
|
// src/client.ts
|
|
840
1698
|
var DEFAULT_BASE_URL = "https://webhooks.cc";
|
|
841
1699
|
var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
|
|
842
1700
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1701
|
+
var DEFAULT_RETRY_ATTEMPTS = 1;
|
|
1702
|
+
var DEFAULT_RETRY_BACKOFF_MS = 1e3;
|
|
1703
|
+
var DEFAULT_RETRY_STATUSES = [429, 500, 502, 503, 504];
|
|
843
1704
|
var SDK_VERSION = "0.6.0";
|
|
1705
|
+
var WAIT_FOR_LOOKBACK_MS = 5 * 60 * 1e3;
|
|
1706
|
+
var DEFAULT_EXPORT_PAGE_SIZE = 100;
|
|
1707
|
+
var PROVIDER_PARAM_DESCRIPTION = TEMPLATE_PROVIDERS.map((provider) => `"${provider}"`).join("|");
|
|
844
1708
|
var MIN_POLL_INTERVAL = 10;
|
|
845
1709
|
var MAX_POLL_INTERVAL = 6e4;
|
|
846
1710
|
var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
|
|
@@ -901,26 +1765,236 @@ function validatePathSegment(segment, name) {
|
|
|
901
1765
|
);
|
|
902
1766
|
}
|
|
903
1767
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1768
|
+
function resolveTimestampFilter(value, now) {
|
|
1769
|
+
if (typeof value === "number") {
|
|
1770
|
+
return value;
|
|
1771
|
+
}
|
|
1772
|
+
const trimmed = value.trim();
|
|
1773
|
+
if (trimmed.length === 0) {
|
|
1774
|
+
throw new Error("Invalid timestamp filter: value cannot be empty");
|
|
1775
|
+
}
|
|
1776
|
+
const asNumber = Number(trimmed);
|
|
1777
|
+
if (!Number.isNaN(asNumber)) {
|
|
1778
|
+
return asNumber;
|
|
1779
|
+
}
|
|
1780
|
+
return now - parseDuration(trimmed);
|
|
1781
|
+
}
|
|
1782
|
+
function buildSearchQuery(filters, includePagination) {
|
|
1783
|
+
const params = new URLSearchParams();
|
|
1784
|
+
const now = Date.now();
|
|
1785
|
+
if (filters.slug !== void 0) {
|
|
1786
|
+
validatePathSegment(filters.slug, "slug");
|
|
1787
|
+
params.set("slug", filters.slug);
|
|
1788
|
+
}
|
|
1789
|
+
if (filters.method !== void 0) {
|
|
1790
|
+
params.set("method", filters.method);
|
|
1791
|
+
}
|
|
1792
|
+
if (filters.q !== void 0) {
|
|
1793
|
+
params.set("q", filters.q);
|
|
1794
|
+
}
|
|
1795
|
+
if (filters.from !== void 0) {
|
|
1796
|
+
params.set("from", String(resolveTimestampFilter(filters.from, now)));
|
|
1797
|
+
}
|
|
1798
|
+
if (filters.to !== void 0) {
|
|
1799
|
+
params.set("to", String(resolveTimestampFilter(filters.to, now)));
|
|
1800
|
+
}
|
|
1801
|
+
if (includePagination && filters.limit !== void 0) {
|
|
1802
|
+
params.set("limit", String(filters.limit));
|
|
1803
|
+
}
|
|
1804
|
+
if (includePagination && filters.offset !== void 0) {
|
|
1805
|
+
params.set("offset", String(filters.offset));
|
|
1806
|
+
}
|
|
1807
|
+
if (includePagination && filters.order !== void 0) {
|
|
1808
|
+
params.set("order", filters.order);
|
|
1809
|
+
}
|
|
1810
|
+
const query = params.toString();
|
|
1811
|
+
return query ? `?${query}` : "";
|
|
1812
|
+
}
|
|
1813
|
+
function buildClearQuery(options = {}) {
|
|
1814
|
+
const params = new URLSearchParams();
|
|
1815
|
+
if (options.before !== void 0) {
|
|
1816
|
+
params.set("before", String(resolveTimestampFilter(options.before, Date.now())));
|
|
1817
|
+
}
|
|
1818
|
+
const query = params.toString();
|
|
1819
|
+
return query ? `?${query}` : "";
|
|
1820
|
+
}
|
|
1821
|
+
function buildPaginatedListQuery(options = {}) {
|
|
1822
|
+
const params = new URLSearchParams();
|
|
1823
|
+
if (options.limit !== void 0) {
|
|
1824
|
+
params.set("limit", String(options.limit));
|
|
1825
|
+
}
|
|
1826
|
+
if (options.cursor !== void 0) {
|
|
1827
|
+
params.set("cursor", options.cursor);
|
|
1828
|
+
}
|
|
1829
|
+
const query = params.toString();
|
|
1830
|
+
return query ? `?${query}` : "";
|
|
1831
|
+
}
|
|
1832
|
+
function parseRetryAfterHeader(response) {
|
|
1833
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
1834
|
+
if (!retryAfterHeader) {
|
|
1835
|
+
return void 0;
|
|
1836
|
+
}
|
|
1837
|
+
const parsedSeconds = parseInt(retryAfterHeader, 10);
|
|
1838
|
+
if (!Number.isNaN(parsedSeconds) && parsedSeconds >= 0) {
|
|
1839
|
+
return parsedSeconds;
|
|
1840
|
+
}
|
|
1841
|
+
return void 0;
|
|
1842
|
+
}
|
|
1843
|
+
function normalizeRetryOptions(retry) {
|
|
1844
|
+
const maxAttempts = Math.max(1, Math.floor(retry?.maxAttempts ?? DEFAULT_RETRY_ATTEMPTS));
|
|
1845
|
+
const backoffMs = Math.max(0, Math.floor(retry?.backoffMs ?? DEFAULT_RETRY_BACKOFF_MS));
|
|
1846
|
+
return {
|
|
1847
|
+
maxAttempts,
|
|
1848
|
+
backoffMs,
|
|
1849
|
+
retryOn: new Set(retry?.retryOn ?? DEFAULT_RETRY_STATUSES)
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
function normalizeExportPageSize(limit) {
|
|
1853
|
+
if (limit === void 0) {
|
|
1854
|
+
return DEFAULT_EXPORT_PAGE_SIZE;
|
|
1855
|
+
}
|
|
1856
|
+
return Math.max(1, Math.min(DEFAULT_EXPORT_PAGE_SIZE, Math.floor(limit)));
|
|
1857
|
+
}
|
|
1858
|
+
function buildStreamPath(slug, since) {
|
|
1859
|
+
const params = new URLSearchParams();
|
|
1860
|
+
if (since !== void 0) {
|
|
1861
|
+
params.set("since", String(Math.max(0, Math.floor(since))));
|
|
1862
|
+
}
|
|
1863
|
+
const query = params.toString();
|
|
1864
|
+
return `/api/stream/${slug}${query ? `?${query}` : ""}`;
|
|
1865
|
+
}
|
|
1866
|
+
function normalizeReconnectBackoff(value) {
|
|
1867
|
+
if (value === void 0) {
|
|
1868
|
+
return DEFAULT_RETRY_BACKOFF_MS;
|
|
1869
|
+
}
|
|
1870
|
+
return Math.max(0, parseDuration(value));
|
|
1871
|
+
}
|
|
1872
|
+
function shouldReconnectStreamError(error) {
|
|
1873
|
+
if (error instanceof UnauthorizedError || error instanceof NotFoundError) {
|
|
1874
|
+
return false;
|
|
1875
|
+
}
|
|
1876
|
+
if (error instanceof WebhooksCCError) {
|
|
1877
|
+
return error.statusCode === 429 || error.statusCode >= 500;
|
|
1878
|
+
}
|
|
1879
|
+
return error instanceof Error;
|
|
1880
|
+
}
|
|
1881
|
+
function parseStreamRequest(data) {
|
|
1882
|
+
try {
|
|
1883
|
+
const parsed = JSON.parse(data);
|
|
1884
|
+
if (typeof parsed.endpointId !== "string" || typeof parsed.method !== "string" || typeof parsed.receivedAt !== "number" || typeof parsed.headers !== "object" || parsed.headers === null) {
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
return {
|
|
1888
|
+
id: typeof parsed._id === "string" ? parsed._id : typeof parsed.id === "string" ? parsed.id : "",
|
|
1889
|
+
endpointId: parsed.endpointId,
|
|
1890
|
+
method: parsed.method,
|
|
1891
|
+
path: typeof parsed.path === "string" ? parsed.path : "/",
|
|
1892
|
+
headers: parsed.headers,
|
|
1893
|
+
body: typeof parsed.body === "string" ? parsed.body : void 0,
|
|
1894
|
+
queryParams: typeof parsed.queryParams === "object" && parsed.queryParams !== null ? parsed.queryParams : {},
|
|
1895
|
+
contentType: typeof parsed.contentType === "string" ? parsed.contentType : void 0,
|
|
1896
|
+
ip: typeof parsed.ip === "string" ? parsed.ip : "unknown",
|
|
1897
|
+
size: typeof parsed.size === "number" ? parsed.size : 0,
|
|
1898
|
+
receivedAt: parsed.receivedAt
|
|
1899
|
+
};
|
|
1900
|
+
} catch {
|
|
1901
|
+
return null;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
async function collectMatchingRequests(fetchRequests, options) {
|
|
1905
|
+
const timeout = parseDuration(options.timeout ?? 3e4);
|
|
1906
|
+
const rawPollInterval = parseDuration(options.pollInterval ?? 500);
|
|
1907
|
+
const safePollInterval = Math.max(
|
|
1908
|
+
MIN_POLL_INTERVAL,
|
|
1909
|
+
Math.min(MAX_POLL_INTERVAL, rawPollInterval)
|
|
1910
|
+
);
|
|
1911
|
+
const desiredCount = Math.max(1, Math.floor(options.count));
|
|
1912
|
+
const start = Date.now();
|
|
1913
|
+
let lastChecked = start - WAIT_FOR_LOOKBACK_MS;
|
|
1914
|
+
let iterations = 0;
|
|
1915
|
+
const MAX_ITERATIONS = 1e4;
|
|
1916
|
+
const collected = [];
|
|
1917
|
+
const seenRequestIds = /* @__PURE__ */ new Set();
|
|
1918
|
+
while (Date.now() - start < timeout && iterations < MAX_ITERATIONS) {
|
|
1919
|
+
iterations++;
|
|
1920
|
+
const checkTime = Date.now();
|
|
1921
|
+
try {
|
|
1922
|
+
const requests = (await fetchRequests(lastChecked)).slice().sort((left, right) => left.receivedAt - right.receivedAt);
|
|
1923
|
+
lastChecked = checkTime;
|
|
1924
|
+
for (const request of requests) {
|
|
1925
|
+
if (seenRequestIds.has(request.id)) {
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
seenRequestIds.add(request.id);
|
|
1929
|
+
if (options.match && !options.match(request)) {
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
collected.push(request);
|
|
1933
|
+
if (collected.length >= desiredCount) {
|
|
1934
|
+
return collected.slice(0, desiredCount);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
} catch (error) {
|
|
1938
|
+
if (error instanceof WebhooksCCError) {
|
|
1939
|
+
if (error instanceof UnauthorizedError || error instanceof NotFoundError) {
|
|
1940
|
+
throw error;
|
|
1941
|
+
}
|
|
1942
|
+
if (error.statusCode < 500 && !(error instanceof RateLimitError)) {
|
|
1943
|
+
throw error;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
await sleep(safePollInterval);
|
|
1948
|
+
}
|
|
1949
|
+
throw new TimeoutError(timeout);
|
|
1950
|
+
}
|
|
1951
|
+
function validateMockResponse(mockResponse, fieldName) {
|
|
1952
|
+
const { status, delay } = 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
|
+
if (delay !== void 0 && (!Number.isInteger(delay) || delay < 0 || delay > 3e4)) {
|
|
1957
|
+
throw new Error(`Invalid ${fieldName} delay: ${delay}. Must be an integer 0-30000.`);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
var WebhooksCC = class {
|
|
1961
|
+
constructor(options) {
|
|
1962
|
+
this.endpoints = {
|
|
1963
|
+
create: async (options = {}) => {
|
|
1964
|
+
if (options.mockResponse) {
|
|
1965
|
+
validateMockResponse(options.mockResponse, "mock response");
|
|
1966
|
+
}
|
|
1967
|
+
const body = {};
|
|
1968
|
+
if (options.name !== void 0) {
|
|
1969
|
+
body.name = options.name;
|
|
1970
|
+
}
|
|
1971
|
+
if (options.mockResponse !== void 0) {
|
|
1972
|
+
body.mockResponse = options.mockResponse;
|
|
1973
|
+
}
|
|
1974
|
+
const isEphemeral = options.ephemeral === true || options.expiresIn !== void 0;
|
|
1975
|
+
if (isEphemeral) {
|
|
1976
|
+
body.isEphemeral = true;
|
|
1977
|
+
}
|
|
1978
|
+
if (options.expiresIn !== void 0) {
|
|
1979
|
+
const durationMs = parseDuration(options.expiresIn);
|
|
1980
|
+
if (durationMs <= 0) {
|
|
1981
|
+
throw new Error("expiresIn must be greater than 0");
|
|
1982
|
+
}
|
|
1983
|
+
body.expiresAt = Date.now() + durationMs;
|
|
1984
|
+
}
|
|
1985
|
+
return this.request("POST", "/endpoints", body);
|
|
1986
|
+
},
|
|
1987
|
+
list: async () => {
|
|
1988
|
+
return this.request("GET", "/endpoints");
|
|
1989
|
+
},
|
|
1990
|
+
get: async (slug) => {
|
|
1991
|
+
validatePathSegment(slug, "slug");
|
|
1992
|
+
return this.request("GET", `/endpoints/${slug}`);
|
|
1993
|
+
},
|
|
1994
|
+
update: async (slug, options) => {
|
|
1995
|
+
validatePathSegment(slug, "slug");
|
|
1996
|
+
if (options.mockResponse && options.mockResponse !== null) {
|
|
1997
|
+
validateMockResponse(options.mockResponse, "mock response");
|
|
924
1998
|
}
|
|
925
1999
|
return this.request("PATCH", `/endpoints/${slug}`, options);
|
|
926
2000
|
},
|
|
@@ -963,6 +2037,20 @@ var WebhooksCC = class {
|
|
|
963
2037
|
return this.endpoints.send(slug, sendOptions);
|
|
964
2038
|
}
|
|
965
2039
|
};
|
|
2040
|
+
this.templates = {
|
|
2041
|
+
listProviders: () => {
|
|
2042
|
+
return [...TEMPLATE_PROVIDERS];
|
|
2043
|
+
},
|
|
2044
|
+
get: (provider) => {
|
|
2045
|
+
return TEMPLATE_METADATA[provider];
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
this.usage = async () => {
|
|
2049
|
+
return this.request("GET", "/usage");
|
|
2050
|
+
};
|
|
2051
|
+
this.flow = () => {
|
|
2052
|
+
return new WebhookFlowBuilder(this);
|
|
2053
|
+
};
|
|
966
2054
|
/**
|
|
967
2055
|
* Build a request without sending it. Returns the computed method, URL,
|
|
968
2056
|
* headers, and body — including any provider signatures. Useful for
|
|
@@ -1092,10 +2180,86 @@ var WebhooksCC = class {
|
|
|
1092
2180
|
`/endpoints/${endpointSlug}/requests${query ? `?${query}` : ""}`
|
|
1093
2181
|
);
|
|
1094
2182
|
},
|
|
2183
|
+
listPaginated: async (endpointSlug, options = {}) => {
|
|
2184
|
+
validatePathSegment(endpointSlug, "endpointSlug");
|
|
2185
|
+
return this.request(
|
|
2186
|
+
"GET",
|
|
2187
|
+
`/endpoints/${endpointSlug}/requests/paginated${buildPaginatedListQuery(options)}`
|
|
2188
|
+
);
|
|
2189
|
+
},
|
|
1095
2190
|
get: async (requestId) => {
|
|
1096
2191
|
validatePathSegment(requestId, "requestId");
|
|
1097
2192
|
return this.request("GET", `/requests/${requestId}`);
|
|
1098
2193
|
},
|
|
2194
|
+
waitForAll: async (endpointSlug, options) => {
|
|
2195
|
+
validatePathSegment(endpointSlug, "endpointSlug");
|
|
2196
|
+
const listLimit = Math.min(1e3, Math.max(100, Math.floor(options.count) * 2));
|
|
2197
|
+
return collectMatchingRequests(
|
|
2198
|
+
(since) => this.requests.list(endpointSlug, {
|
|
2199
|
+
since,
|
|
2200
|
+
limit: listLimit
|
|
2201
|
+
}),
|
|
2202
|
+
options
|
|
2203
|
+
);
|
|
2204
|
+
},
|
|
2205
|
+
search: async (filters = {}) => {
|
|
2206
|
+
return this.request(
|
|
2207
|
+
"GET",
|
|
2208
|
+
`/search/requests${buildSearchQuery(filters, true)}`
|
|
2209
|
+
);
|
|
2210
|
+
},
|
|
2211
|
+
count: async (filters = {}) => {
|
|
2212
|
+
const response = await this.request(
|
|
2213
|
+
"GET",
|
|
2214
|
+
`/search/requests/count${buildSearchQuery(filters, false)}`
|
|
2215
|
+
);
|
|
2216
|
+
return response.count;
|
|
2217
|
+
},
|
|
2218
|
+
clear: async (endpointSlug, options = {}) => {
|
|
2219
|
+
validatePathSegment(endpointSlug, "endpointSlug");
|
|
2220
|
+
await this.request(
|
|
2221
|
+
"DELETE",
|
|
2222
|
+
`/endpoints/${endpointSlug}/requests${buildClearQuery(options)}`
|
|
2223
|
+
);
|
|
2224
|
+
},
|
|
2225
|
+
export: async (endpointSlug, options) => {
|
|
2226
|
+
validatePathSegment(endpointSlug, "endpointSlug");
|
|
2227
|
+
const endpoint = await this.endpoints.get(endpointSlug);
|
|
2228
|
+
const endpointUrl = endpoint.url ?? `${this.webhookUrl}/w/${endpoint.slug}`;
|
|
2229
|
+
const requests = [];
|
|
2230
|
+
const pageSize = normalizeExportPageSize(options.limit);
|
|
2231
|
+
let cursor;
|
|
2232
|
+
while (true) {
|
|
2233
|
+
const remaining = options.limit !== void 0 ? Math.max(0, options.limit - requests.length) : pageSize;
|
|
2234
|
+
if (options.limit !== void 0 && remaining === 0) {
|
|
2235
|
+
break;
|
|
2236
|
+
}
|
|
2237
|
+
const page = await this.requests.listPaginated(endpointSlug, {
|
|
2238
|
+
limit: options.limit !== void 0 ? Math.min(pageSize, remaining) : pageSize,
|
|
2239
|
+
cursor
|
|
2240
|
+
});
|
|
2241
|
+
for (const request of page.items) {
|
|
2242
|
+
if (options.since !== void 0 && request.receivedAt <= options.since) {
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
requests.push(request);
|
|
2246
|
+
if (options.limit !== void 0 && requests.length >= options.limit) {
|
|
2247
|
+
break;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
if (!page.hasMore || !page.cursor) {
|
|
2251
|
+
break;
|
|
2252
|
+
}
|
|
2253
|
+
if (options.limit !== void 0 && requests.length >= options.limit) {
|
|
2254
|
+
break;
|
|
2255
|
+
}
|
|
2256
|
+
cursor = page.cursor;
|
|
2257
|
+
}
|
|
2258
|
+
if (options.format === "curl") {
|
|
2259
|
+
return buildCurlExport(endpointUrl, requests);
|
|
2260
|
+
}
|
|
2261
|
+
return buildHarExport(endpointUrl, requests, SDK_VERSION);
|
|
2262
|
+
},
|
|
1099
2263
|
/**
|
|
1100
2264
|
* Polls for incoming requests until one matches or timeout expires.
|
|
1101
2265
|
*
|
|
@@ -1109,47 +2273,11 @@ var WebhooksCC = class {
|
|
|
1109
2273
|
* @throws Error if timeout expires or max iterations (10000) reached
|
|
1110
2274
|
*/
|
|
1111
2275
|
waitFor: async (endpointSlug, options = {}) => {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
MIN_POLL_INTERVAL,
|
|
1118
|
-
Math.min(MAX_POLL_INTERVAL, rawPollInterval)
|
|
1119
|
-
);
|
|
1120
|
-
const start = Date.now();
|
|
1121
|
-
let lastChecked = start - 5 * 60 * 1e3;
|
|
1122
|
-
const MAX_ITERATIONS = 1e4;
|
|
1123
|
-
let iterations = 0;
|
|
1124
|
-
while (Date.now() - start < timeout && iterations < MAX_ITERATIONS) {
|
|
1125
|
-
iterations++;
|
|
1126
|
-
const checkTime = Date.now();
|
|
1127
|
-
try {
|
|
1128
|
-
const requests = await this.requests.list(endpointSlug, {
|
|
1129
|
-
since: lastChecked,
|
|
1130
|
-
limit: 100
|
|
1131
|
-
});
|
|
1132
|
-
lastChecked = checkTime;
|
|
1133
|
-
const matched = match ? requests.find(match) : requests[0];
|
|
1134
|
-
if (matched) {
|
|
1135
|
-
return matched;
|
|
1136
|
-
}
|
|
1137
|
-
} catch (error) {
|
|
1138
|
-
if (error instanceof WebhooksCCError) {
|
|
1139
|
-
if (error instanceof UnauthorizedError) {
|
|
1140
|
-
throw error;
|
|
1141
|
-
}
|
|
1142
|
-
if (error instanceof NotFoundError) {
|
|
1143
|
-
throw error;
|
|
1144
|
-
}
|
|
1145
|
-
if (error.statusCode < 500 && !(error instanceof RateLimitError)) {
|
|
1146
|
-
throw error;
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
await sleep(safePollInterval);
|
|
1151
|
-
}
|
|
1152
|
-
throw new TimeoutError(timeout);
|
|
2276
|
+
const [request] = await this.requests.waitForAll(endpointSlug, {
|
|
2277
|
+
...options,
|
|
2278
|
+
count: 1
|
|
2279
|
+
});
|
|
2280
|
+
return request;
|
|
1153
2281
|
},
|
|
1154
2282
|
/**
|
|
1155
2283
|
* Replay a captured request to a target URL.
|
|
@@ -1192,20 +2320,25 @@ var WebhooksCC = class {
|
|
|
1192
2320
|
* The connection is closed when the iterator is broken, the signal is aborted,
|
|
1193
2321
|
* or the timeout expires.
|
|
1194
2322
|
*
|
|
1195
|
-
*
|
|
2323
|
+
* Reconnection is opt-in and resumes from the last yielded request timestamp.
|
|
1196
2324
|
*/
|
|
1197
2325
|
subscribe: (slug, options = {}) => {
|
|
1198
2326
|
validatePathSegment(slug, "slug");
|
|
1199
|
-
const { signal, timeout } = options;
|
|
2327
|
+
const { signal, timeout, reconnect = false, onReconnect } = options;
|
|
1200
2328
|
const baseUrl = this.baseUrl;
|
|
1201
2329
|
const apiKey = this.apiKey;
|
|
1202
2330
|
const timeoutMs = timeout !== void 0 ? parseDuration(timeout) : void 0;
|
|
2331
|
+
const maxReconnectAttempts = Math.max(0, Math.floor(options.maxReconnectAttempts ?? 5));
|
|
2332
|
+
const reconnectBackoffMs = normalizeReconnectBackoff(options.reconnectBackoffMs);
|
|
1203
2333
|
return {
|
|
1204
2334
|
[Symbol.asyncIterator]() {
|
|
1205
2335
|
const controller = new AbortController();
|
|
1206
2336
|
let timeoutId;
|
|
1207
2337
|
let iterator = null;
|
|
1208
2338
|
let started = false;
|
|
2339
|
+
let reconnectAttempts = 0;
|
|
2340
|
+
let lastReceivedAt;
|
|
2341
|
+
const seenRequestIds = /* @__PURE__ */ new Set();
|
|
1209
2342
|
const onAbort = () => controller.abort();
|
|
1210
2343
|
if (signal) {
|
|
1211
2344
|
if (signal.aborted) {
|
|
@@ -1222,7 +2355,10 @@ var WebhooksCC = class {
|
|
|
1222
2355
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1223
2356
|
};
|
|
1224
2357
|
const start = async () => {
|
|
1225
|
-
const url = `${baseUrl}
|
|
2358
|
+
const url = `${baseUrl}${buildStreamPath(
|
|
2359
|
+
slug,
|
|
2360
|
+
lastReceivedAt !== void 0 ? lastReceivedAt - 1 : void 0
|
|
2361
|
+
)}`;
|
|
1226
2362
|
const connectController = new AbortController();
|
|
1227
2363
|
const connectTimeout = setTimeout(() => connectController.abort(), 3e4);
|
|
1228
2364
|
controller.signal.addEventListener("abort", () => connectController.abort(), {
|
|
@@ -1238,12 +2374,10 @@ var WebhooksCC = class {
|
|
|
1238
2374
|
clearTimeout(connectTimeout);
|
|
1239
2375
|
}
|
|
1240
2376
|
if (!response.ok) {
|
|
1241
|
-
cleanup();
|
|
1242
2377
|
const text = await response.text();
|
|
1243
2378
|
throw mapStatusToError(response.status, text, response);
|
|
1244
2379
|
}
|
|
1245
2380
|
if (!response.body) {
|
|
1246
|
-
cleanup();
|
|
1247
2381
|
throw new Error("SSE response has no body");
|
|
1248
2382
|
}
|
|
1249
2383
|
controller.signal.addEventListener(
|
|
@@ -1256,6 +2390,24 @@ var WebhooksCC = class {
|
|
|
1256
2390
|
);
|
|
1257
2391
|
return parseSSE(response.body);
|
|
1258
2392
|
};
|
|
2393
|
+
const reconnectStream = async () => {
|
|
2394
|
+
if (!reconnect || reconnectAttempts >= maxReconnectAttempts || controller.signal.aborted) {
|
|
2395
|
+
cleanup();
|
|
2396
|
+
return false;
|
|
2397
|
+
}
|
|
2398
|
+
reconnectAttempts++;
|
|
2399
|
+
try {
|
|
2400
|
+
onReconnect?.(reconnectAttempts);
|
|
2401
|
+
} catch {
|
|
2402
|
+
}
|
|
2403
|
+
await sleep(reconnectBackoffMs * 2 ** (reconnectAttempts - 1));
|
|
2404
|
+
if (controller.signal.aborted) {
|
|
2405
|
+
cleanup();
|
|
2406
|
+
return false;
|
|
2407
|
+
}
|
|
2408
|
+
iterator = await start();
|
|
2409
|
+
return true;
|
|
2410
|
+
};
|
|
1259
2411
|
return {
|
|
1260
2412
|
[Symbol.asyncIterator]() {
|
|
1261
2413
|
return this;
|
|
@@ -1269,32 +2421,26 @@ var WebhooksCC = class {
|
|
|
1269
2421
|
while (iterator) {
|
|
1270
2422
|
const { done, value } = await iterator.next();
|
|
1271
2423
|
if (done) {
|
|
1272
|
-
|
|
2424
|
+
iterator = null;
|
|
2425
|
+
if (await reconnectStream()) {
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
1273
2428
|
return { done: true, value: void 0 };
|
|
1274
2429
|
}
|
|
2430
|
+
reconnectAttempts = 0;
|
|
1275
2431
|
if (value.event === "request") {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const req = {
|
|
1282
|
-
id: data._id ?? data.id,
|
|
1283
|
-
endpointId: data.endpointId,
|
|
1284
|
-
method: data.method,
|
|
1285
|
-
path: data.path ?? "/",
|
|
1286
|
-
headers: data.headers,
|
|
1287
|
-
body: data.body ?? void 0,
|
|
1288
|
-
queryParams: data.queryParams ?? {},
|
|
1289
|
-
contentType: data.contentType ?? void 0,
|
|
1290
|
-
ip: data.ip ?? "unknown",
|
|
1291
|
-
size: data.size ?? 0,
|
|
1292
|
-
receivedAt: data.receivedAt
|
|
1293
|
-
};
|
|
1294
|
-
return { done: false, value: req };
|
|
1295
|
-
} catch {
|
|
2432
|
+
const req = parseStreamRequest(value.data);
|
|
2433
|
+
if (!req) {
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
if (req.id && seenRequestIds.has(req.id)) {
|
|
1296
2437
|
continue;
|
|
1297
2438
|
}
|
|
2439
|
+
if (req.id) {
|
|
2440
|
+
seenRequestIds.add(req.id);
|
|
2441
|
+
}
|
|
2442
|
+
lastReceivedAt = req.receivedAt;
|
|
2443
|
+
return { done: false, value: req };
|
|
1298
2444
|
}
|
|
1299
2445
|
if (value.event === "timeout" || value.event === "endpoint_deleted") {
|
|
1300
2446
|
cleanup();
|
|
@@ -1304,11 +2450,17 @@ var WebhooksCC = class {
|
|
|
1304
2450
|
cleanup();
|
|
1305
2451
|
return { done: true, value: void 0 };
|
|
1306
2452
|
} catch (error) {
|
|
1307
|
-
cleanup();
|
|
1308
|
-
controller.abort();
|
|
1309
2453
|
if (error instanceof Error && error.name === "AbortError") {
|
|
2454
|
+
cleanup();
|
|
2455
|
+
controller.abort();
|
|
1310
2456
|
return { done: true, value: void 0 };
|
|
1311
2457
|
}
|
|
2458
|
+
iterator = null;
|
|
2459
|
+
if (shouldReconnectStreamError(error) && await reconnectStream()) {
|
|
2460
|
+
return this.next();
|
|
2461
|
+
}
|
|
2462
|
+
cleanup();
|
|
2463
|
+
controller.abort();
|
|
1312
2464
|
throw error;
|
|
1313
2465
|
}
|
|
1314
2466
|
},
|
|
@@ -1332,67 +2484,100 @@ var WebhooksCC = class {
|
|
|
1332
2484
|
this.baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL);
|
|
1333
2485
|
this.webhookUrl = stripTrailingSlashes(options.webhookUrl ?? DEFAULT_WEBHOOK_URL);
|
|
1334
2486
|
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
2487
|
+
this.retry = normalizeRetryOptions(options.retry);
|
|
1335
2488
|
this.hooks = options.hooks ?? {};
|
|
1336
2489
|
}
|
|
1337
2490
|
async request(method, path, body) {
|
|
1338
2491
|
const url = `${this.baseUrl}/api${path}`;
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}
|
|
1346
|
-
try {
|
|
1347
|
-
const response = await fetch(url, {
|
|
1348
|
-
method,
|
|
1349
|
-
headers: {
|
|
1350
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
1351
|
-
"Content-Type": "application/json"
|
|
1352
|
-
},
|
|
1353
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
1354
|
-
signal: controller.signal
|
|
1355
|
-
});
|
|
1356
|
-
const durationMs = Date.now() - start;
|
|
1357
|
-
if (!response.ok) {
|
|
1358
|
-
const errorText = await response.text();
|
|
1359
|
-
const sanitizedError = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
|
|
1360
|
-
const error = mapStatusToError(response.status, sanitizedError, response);
|
|
1361
|
-
try {
|
|
1362
|
-
this.hooks.onError?.({ method, url, error, durationMs });
|
|
1363
|
-
} catch {
|
|
1364
|
-
}
|
|
1365
|
-
throw error;
|
|
1366
|
-
}
|
|
2492
|
+
let attempt = 0;
|
|
2493
|
+
while (true) {
|
|
2494
|
+
attempt++;
|
|
2495
|
+
const controller = new AbortController();
|
|
2496
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
2497
|
+
const start = Date.now();
|
|
1367
2498
|
try {
|
|
1368
|
-
this.hooks.
|
|
2499
|
+
this.hooks.onRequest?.({ method, url });
|
|
1369
2500
|
} catch {
|
|
1370
2501
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
const
|
|
2502
|
+
try {
|
|
2503
|
+
const response = await fetch(url, {
|
|
2504
|
+
method,
|
|
2505
|
+
headers: {
|
|
2506
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
2507
|
+
"Content-Type": "application/json"
|
|
2508
|
+
},
|
|
2509
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
2510
|
+
signal: controller.signal
|
|
2511
|
+
});
|
|
2512
|
+
const durationMs = Date.now() - start;
|
|
2513
|
+
if (!response.ok) {
|
|
2514
|
+
const errorText = await response.text();
|
|
2515
|
+
const sanitizedError = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
|
|
2516
|
+
const error = mapStatusToError(response.status, sanitizedError, response);
|
|
2517
|
+
try {
|
|
2518
|
+
this.hooks.onError?.({ method, url, error, durationMs });
|
|
2519
|
+
} catch {
|
|
2520
|
+
}
|
|
2521
|
+
if (attempt < this.retry.maxAttempts && this.retry.retryOn.has(response.status)) {
|
|
2522
|
+
const retryDelayMs = response.status === 429 && parseRetryAfterHeader(response) !== void 0 ? (parseRetryAfterHeader(response) ?? 0) * 1e3 : this.retry.backoffMs * 2 ** (attempt - 1);
|
|
2523
|
+
await sleep(retryDelayMs);
|
|
2524
|
+
continue;
|
|
2525
|
+
}
|
|
2526
|
+
throw error;
|
|
2527
|
+
}
|
|
1382
2528
|
try {
|
|
1383
|
-
this.hooks.
|
|
1384
|
-
method,
|
|
1385
|
-
url,
|
|
1386
|
-
error: timeoutError,
|
|
1387
|
-
durationMs: Date.now() - start
|
|
1388
|
-
});
|
|
2529
|
+
this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
|
|
1389
2530
|
} catch {
|
|
1390
2531
|
}
|
|
1391
|
-
|
|
2532
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
2533
|
+
return void 0;
|
|
2534
|
+
}
|
|
2535
|
+
const contentType = response.headers.get("content-type");
|
|
2536
|
+
if (contentType && !contentType.includes("application/json")) {
|
|
2537
|
+
throw new Error(`Unexpected content type: ${contentType}`);
|
|
2538
|
+
}
|
|
2539
|
+
return response.json();
|
|
2540
|
+
} catch (error) {
|
|
2541
|
+
if (error instanceof WebhooksCCError) {
|
|
2542
|
+
throw error;
|
|
2543
|
+
}
|
|
2544
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
2545
|
+
const timeoutError = new TimeoutError(this.timeout);
|
|
2546
|
+
try {
|
|
2547
|
+
this.hooks.onError?.({
|
|
2548
|
+
method,
|
|
2549
|
+
url,
|
|
2550
|
+
error: timeoutError,
|
|
2551
|
+
durationMs: Date.now() - start
|
|
2552
|
+
});
|
|
2553
|
+
} catch {
|
|
2554
|
+
}
|
|
2555
|
+
if (attempt < this.retry.maxAttempts) {
|
|
2556
|
+
await sleep(this.retry.backoffMs * 2 ** (attempt - 1));
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
2559
|
+
throw timeoutError;
|
|
2560
|
+
}
|
|
2561
|
+
const isNetworkError = error instanceof Error;
|
|
2562
|
+
if (isNetworkError) {
|
|
2563
|
+
try {
|
|
2564
|
+
this.hooks.onError?.({
|
|
2565
|
+
method,
|
|
2566
|
+
url,
|
|
2567
|
+
error,
|
|
2568
|
+
durationMs: Date.now() - start
|
|
2569
|
+
});
|
|
2570
|
+
} catch {
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
if (attempt < this.retry.maxAttempts && isNetworkError) {
|
|
2574
|
+
await sleep(this.retry.backoffMs * 2 ** (attempt - 1));
|
|
2575
|
+
continue;
|
|
2576
|
+
}
|
|
2577
|
+
throw error;
|
|
2578
|
+
} finally {
|
|
2579
|
+
clearTimeout(timeoutId);
|
|
1392
2580
|
}
|
|
1393
|
-
throw error;
|
|
1394
|
-
} finally {
|
|
1395
|
-
clearTimeout(timeoutId);
|
|
1396
2581
|
}
|
|
1397
2582
|
}
|
|
1398
2583
|
/** Returns a static description of all SDK operations (no API call). */
|
|
@@ -1402,7 +2587,12 @@ var WebhooksCC = class {
|
|
|
1402
2587
|
endpoints: {
|
|
1403
2588
|
create: {
|
|
1404
2589
|
description: "Create a webhook endpoint",
|
|
1405
|
-
params: {
|
|
2590
|
+
params: {
|
|
2591
|
+
name: "string?",
|
|
2592
|
+
ephemeral: "boolean?",
|
|
2593
|
+
expiresIn: "number|string?",
|
|
2594
|
+
mockResponse: "object?"
|
|
2595
|
+
}
|
|
1406
2596
|
},
|
|
1407
2597
|
list: {
|
|
1408
2598
|
description: "List all endpoints",
|
|
@@ -1428,18 +2618,38 @@ var WebhooksCC = class {
|
|
|
1428
2618
|
description: "Send a provider template webhook with signed headers",
|
|
1429
2619
|
params: {
|
|
1430
2620
|
slug: "string",
|
|
1431
|
-
provider:
|
|
2621
|
+
provider: PROVIDER_PARAM_DESCRIPTION,
|
|
1432
2622
|
template: "string?",
|
|
1433
2623
|
secret: "string",
|
|
1434
2624
|
event: "string?"
|
|
1435
2625
|
}
|
|
1436
2626
|
}
|
|
1437
2627
|
},
|
|
2628
|
+
templates: {
|
|
2629
|
+
listProviders: {
|
|
2630
|
+
description: "List supported template providers",
|
|
2631
|
+
params: {}
|
|
2632
|
+
},
|
|
2633
|
+
get: {
|
|
2634
|
+
description: "Get static metadata for a template provider",
|
|
2635
|
+
params: {
|
|
2636
|
+
provider: PROVIDER_PARAM_DESCRIPTION
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
},
|
|
2640
|
+
usage: {
|
|
2641
|
+
description: "Get current request usage and remaining quota",
|
|
2642
|
+
params: {}
|
|
2643
|
+
},
|
|
2644
|
+
flow: {
|
|
2645
|
+
description: "Create a fluent webhook flow builder for common capture/verify/replay flows",
|
|
2646
|
+
params: {}
|
|
2647
|
+
},
|
|
1438
2648
|
sendTo: {
|
|
1439
2649
|
description: "Send a webhook directly to any URL with optional provider signing",
|
|
1440
2650
|
params: {
|
|
1441
2651
|
url: "string",
|
|
1442
|
-
provider:
|
|
2652
|
+
provider: `${PROVIDER_PARAM_DESCRIPTION}?`,
|
|
1443
2653
|
secret: "string?",
|
|
1444
2654
|
body: "unknown?",
|
|
1445
2655
|
headers: "Record<string, string>?"
|
|
@@ -1449,7 +2659,7 @@ var WebhooksCC = class {
|
|
|
1449
2659
|
description: "Build a request without sending it \u2014 returns computed method, URL, headers, and body including provider signatures",
|
|
1450
2660
|
params: {
|
|
1451
2661
|
url: "string",
|
|
1452
|
-
provider:
|
|
2662
|
+
provider: `${PROVIDER_PARAM_DESCRIPTION}?`,
|
|
1453
2663
|
secret: "string?",
|
|
1454
2664
|
body: "unknown?",
|
|
1455
2665
|
headers: "Record<string, string>?"
|
|
@@ -1460,10 +2670,24 @@ var WebhooksCC = class {
|
|
|
1460
2670
|
description: "List captured requests",
|
|
1461
2671
|
params: { endpointSlug: "string", limit: "number?", since: "number?" }
|
|
1462
2672
|
},
|
|
2673
|
+
listPaginated: {
|
|
2674
|
+
description: "List captured requests with cursor-based pagination",
|
|
2675
|
+
params: { endpointSlug: "string", limit: "number?", cursor: "string?" }
|
|
2676
|
+
},
|
|
1463
2677
|
get: {
|
|
1464
2678
|
description: "Get request by ID",
|
|
1465
2679
|
params: { requestId: "string" }
|
|
1466
2680
|
},
|
|
2681
|
+
waitForAll: {
|
|
2682
|
+
description: "Poll until multiple matching requests arrive",
|
|
2683
|
+
params: {
|
|
2684
|
+
endpointSlug: "string",
|
|
2685
|
+
count: "number",
|
|
2686
|
+
timeout: "number|string?",
|
|
2687
|
+
pollInterval: "number|string?",
|
|
2688
|
+
match: "function?"
|
|
2689
|
+
}
|
|
2690
|
+
},
|
|
1467
2691
|
waitFor: {
|
|
1468
2692
|
description: "Poll until a matching request arrives",
|
|
1469
2693
|
params: {
|
|
@@ -1474,11 +2698,58 @@ var WebhooksCC = class {
|
|
|
1474
2698
|
},
|
|
1475
2699
|
subscribe: {
|
|
1476
2700
|
description: "Stream requests via SSE",
|
|
1477
|
-
params: {
|
|
2701
|
+
params: {
|
|
2702
|
+
slug: "string",
|
|
2703
|
+
signal: "AbortSignal?",
|
|
2704
|
+
timeout: "number|string?",
|
|
2705
|
+
reconnect: "boolean?",
|
|
2706
|
+
maxReconnectAttempts: "number?",
|
|
2707
|
+
reconnectBackoffMs: "number|string?",
|
|
2708
|
+
onReconnect: "function?"
|
|
2709
|
+
}
|
|
1478
2710
|
},
|
|
1479
2711
|
replay: {
|
|
1480
2712
|
description: "Replay a captured request to a URL",
|
|
1481
2713
|
params: { requestId: "string", targetUrl: "string" }
|
|
2714
|
+
},
|
|
2715
|
+
export: {
|
|
2716
|
+
description: "Export captured requests as HAR or cURL commands",
|
|
2717
|
+
params: {
|
|
2718
|
+
endpointSlug: "string",
|
|
2719
|
+
format: '"har"|"curl"',
|
|
2720
|
+
limit: "number?",
|
|
2721
|
+
since: "number?"
|
|
2722
|
+
}
|
|
2723
|
+
},
|
|
2724
|
+
search: {
|
|
2725
|
+
description: "Search retained requests across path, body, and headers",
|
|
2726
|
+
params: {
|
|
2727
|
+
slug: "string?",
|
|
2728
|
+
method: "string?",
|
|
2729
|
+
q: "string?",
|
|
2730
|
+
from: "number|string?",
|
|
2731
|
+
to: "number|string?",
|
|
2732
|
+
limit: "number?",
|
|
2733
|
+
offset: "number?",
|
|
2734
|
+
order: '"asc"|"desc"?'
|
|
2735
|
+
}
|
|
2736
|
+
},
|
|
2737
|
+
count: {
|
|
2738
|
+
description: "Count retained requests matching search filters",
|
|
2739
|
+
params: {
|
|
2740
|
+
slug: "string?",
|
|
2741
|
+
method: "string?",
|
|
2742
|
+
q: "string?",
|
|
2743
|
+
from: "number|string?",
|
|
2744
|
+
to: "number|string?"
|
|
2745
|
+
}
|
|
2746
|
+
},
|
|
2747
|
+
clear: {
|
|
2748
|
+
description: "Delete captured requests for an endpoint",
|
|
2749
|
+
params: {
|
|
2750
|
+
endpointSlug: "string",
|
|
2751
|
+
before: "number|string?"
|
|
2752
|
+
}
|
|
1482
2753
|
}
|
|
1483
2754
|
}
|
|
1484
2755
|
};
|
|
@@ -1494,6 +2765,49 @@ function stripTrailingSlashes(url) {
|
|
|
1494
2765
|
}
|
|
1495
2766
|
|
|
1496
2767
|
// src/helpers.ts
|
|
2768
|
+
function getHeaderValue(headers, name) {
|
|
2769
|
+
const target = name.toLowerCase();
|
|
2770
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
2771
|
+
if (key.toLowerCase() === target) {
|
|
2772
|
+
return value;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
return void 0;
|
|
2776
|
+
}
|
|
2777
|
+
function getContentType(request) {
|
|
2778
|
+
return request.contentType ?? getHeaderValue(request.headers, "content-type");
|
|
2779
|
+
}
|
|
2780
|
+
function normalizeContentType(value) {
|
|
2781
|
+
if (!value) {
|
|
2782
|
+
return void 0;
|
|
2783
|
+
}
|
|
2784
|
+
return value.split(";", 1)[0]?.trim().toLowerCase();
|
|
2785
|
+
}
|
|
2786
|
+
function getJsonPathValue(body, path) {
|
|
2787
|
+
const parts = path.split(".");
|
|
2788
|
+
let current = body;
|
|
2789
|
+
for (const part of parts) {
|
|
2790
|
+
if (current === null || current === void 0) {
|
|
2791
|
+
return void 0;
|
|
2792
|
+
}
|
|
2793
|
+
if (Array.isArray(current)) {
|
|
2794
|
+
const index = Number(part);
|
|
2795
|
+
if (!Number.isInteger(index) || index < 0 || index >= current.length) {
|
|
2796
|
+
return void 0;
|
|
2797
|
+
}
|
|
2798
|
+
current = current[index];
|
|
2799
|
+
continue;
|
|
2800
|
+
}
|
|
2801
|
+
if (typeof current !== "object") {
|
|
2802
|
+
return void 0;
|
|
2803
|
+
}
|
|
2804
|
+
if (!Object.prototype.hasOwnProperty.call(current, part)) {
|
|
2805
|
+
return void 0;
|
|
2806
|
+
}
|
|
2807
|
+
current = current[part];
|
|
2808
|
+
}
|
|
2809
|
+
return current;
|
|
2810
|
+
}
|
|
1497
2811
|
function parseJsonBody(request) {
|
|
1498
2812
|
if (!request.body) return void 0;
|
|
1499
2813
|
try {
|
|
@@ -1516,6 +2830,50 @@ function matchJsonField(field, value) {
|
|
|
1516
2830
|
return body[field] === value;
|
|
1517
2831
|
};
|
|
1518
2832
|
}
|
|
2833
|
+
function parseFormBody(request) {
|
|
2834
|
+
if (!request.body) {
|
|
2835
|
+
return void 0;
|
|
2836
|
+
}
|
|
2837
|
+
const contentType = normalizeContentType(getContentType(request));
|
|
2838
|
+
if (contentType !== "application/x-www-form-urlencoded") {
|
|
2839
|
+
return void 0;
|
|
2840
|
+
}
|
|
2841
|
+
const parsed = {};
|
|
2842
|
+
for (const [key, value] of new URLSearchParams(request.body).entries()) {
|
|
2843
|
+
const existing = parsed[key];
|
|
2844
|
+
if (existing === void 0) {
|
|
2845
|
+
parsed[key] = value;
|
|
2846
|
+
continue;
|
|
2847
|
+
}
|
|
2848
|
+
if (Array.isArray(existing)) {
|
|
2849
|
+
existing.push(value);
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
parsed[key] = [existing, value];
|
|
2853
|
+
}
|
|
2854
|
+
return parsed;
|
|
2855
|
+
}
|
|
2856
|
+
function parseBody(request) {
|
|
2857
|
+
if (!request.body) {
|
|
2858
|
+
return void 0;
|
|
2859
|
+
}
|
|
2860
|
+
const contentType = normalizeContentType(getContentType(request));
|
|
2861
|
+
if (contentType === "application/json" || contentType?.endsWith("+json")) {
|
|
2862
|
+
const parsed = parseJsonBody(request);
|
|
2863
|
+
return parsed === void 0 ? request.body : parsed;
|
|
2864
|
+
}
|
|
2865
|
+
if (contentType === "application/x-www-form-urlencoded") {
|
|
2866
|
+
return parseFormBody(request);
|
|
2867
|
+
}
|
|
2868
|
+
return request.body;
|
|
2869
|
+
}
|
|
2870
|
+
function extractJsonField(request, path) {
|
|
2871
|
+
const body = parseJsonBody(request);
|
|
2872
|
+
if (body === void 0) {
|
|
2873
|
+
return void 0;
|
|
2874
|
+
}
|
|
2875
|
+
return getJsonPathValue(body, path);
|
|
2876
|
+
}
|
|
1519
2877
|
function isShopifyWebhook(request) {
|
|
1520
2878
|
return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-shopify-hmac-sha256");
|
|
1521
2879
|
}
|
|
@@ -1531,23 +2889,85 @@ function isPaddleWebhook(request) {
|
|
|
1531
2889
|
function isLinearWebhook(request) {
|
|
1532
2890
|
return Object.keys(request.headers).some((k) => k.toLowerCase() === "linear-signature");
|
|
1533
2891
|
}
|
|
2892
|
+
function isDiscordWebhook(request) {
|
|
2893
|
+
const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
|
|
2894
|
+
return keys.includes("x-signature-ed25519") && keys.includes("x-signature-timestamp");
|
|
2895
|
+
}
|
|
1534
2896
|
function isStandardWebhook(request) {
|
|
1535
2897
|
const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
|
|
1536
2898
|
return keys.includes("webhook-id") && keys.includes("webhook-timestamp") && keys.includes("webhook-signature");
|
|
1537
2899
|
}
|
|
1538
2900
|
|
|
1539
2901
|
// src/matchers.ts
|
|
2902
|
+
function getHeaderValue2(headers, name) {
|
|
2903
|
+
const lowerName = name.toLowerCase();
|
|
2904
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
2905
|
+
if (key.toLowerCase() === lowerName) {
|
|
2906
|
+
return value;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
return void 0;
|
|
2910
|
+
}
|
|
2911
|
+
function globToRegExp(pattern) {
|
|
2912
|
+
let source = "^";
|
|
2913
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
2914
|
+
const char = pattern[index];
|
|
2915
|
+
if (char === "*") {
|
|
2916
|
+
if (pattern[index + 1] === "*") {
|
|
2917
|
+
source += ".*";
|
|
2918
|
+
index++;
|
|
2919
|
+
} else {
|
|
2920
|
+
source += "[^/]*";
|
|
2921
|
+
}
|
|
2922
|
+
continue;
|
|
2923
|
+
}
|
|
2924
|
+
source += /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
|
|
2925
|
+
}
|
|
2926
|
+
source += "$";
|
|
2927
|
+
return new RegExp(source);
|
|
2928
|
+
}
|
|
2929
|
+
function isDeepSubset(expected, actual) {
|
|
2930
|
+
if (Array.isArray(expected)) {
|
|
2931
|
+
return Array.isArray(actual) && expected.every((value, index) => isDeepSubset(value, actual[index]));
|
|
2932
|
+
}
|
|
2933
|
+
if (expected && typeof expected === "object") {
|
|
2934
|
+
if (!actual || typeof actual !== "object" || Array.isArray(actual)) {
|
|
2935
|
+
return false;
|
|
2936
|
+
}
|
|
2937
|
+
return Object.entries(expected).every(([key, value]) => {
|
|
2938
|
+
if (!Object.prototype.hasOwnProperty.call(actual, key)) {
|
|
2939
|
+
return false;
|
|
2940
|
+
}
|
|
2941
|
+
return isDeepSubset(value, actual[key]);
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
return Object.is(expected, actual);
|
|
2945
|
+
}
|
|
1540
2946
|
function matchMethod(method) {
|
|
1541
2947
|
const upper = method.toUpperCase();
|
|
1542
2948
|
return (request) => request.method.toUpperCase() === upper;
|
|
1543
2949
|
}
|
|
1544
2950
|
function matchHeader(name, value) {
|
|
1545
|
-
const lowerName = name.toLowerCase();
|
|
1546
2951
|
return (request) => {
|
|
1547
|
-
const
|
|
1548
|
-
if (
|
|
2952
|
+
const headerValue = getHeaderValue2(request.headers, name);
|
|
2953
|
+
if (headerValue === void 0) return false;
|
|
1549
2954
|
if (value === void 0) return true;
|
|
1550
|
-
return
|
|
2955
|
+
return headerValue === value;
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
function matchPath(pattern) {
|
|
2959
|
+
const regex = globToRegExp(pattern);
|
|
2960
|
+
return (request) => regex.test(request.path);
|
|
2961
|
+
}
|
|
2962
|
+
function matchQueryParam(key, value) {
|
|
2963
|
+
return (request) => {
|
|
2964
|
+
if (!Object.prototype.hasOwnProperty.call(request.queryParams, key)) {
|
|
2965
|
+
return false;
|
|
2966
|
+
}
|
|
2967
|
+
if (value === void 0) {
|
|
2968
|
+
return true;
|
|
2969
|
+
}
|
|
2970
|
+
return request.queryParams[key] === value;
|
|
1551
2971
|
};
|
|
1552
2972
|
}
|
|
1553
2973
|
function matchBodyPath(path, value) {
|
|
@@ -1572,6 +2992,23 @@ function matchBodyPath(path, value) {
|
|
|
1572
2992
|
return current === value;
|
|
1573
2993
|
};
|
|
1574
2994
|
}
|
|
2995
|
+
function matchBodySubset(subset) {
|
|
2996
|
+
return (request) => {
|
|
2997
|
+
const body = parseJsonBody(request);
|
|
2998
|
+
return isDeepSubset(subset, body);
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
function matchContentType(type) {
|
|
3002
|
+
const expected = type.trim().toLowerCase();
|
|
3003
|
+
return (request) => {
|
|
3004
|
+
const raw = request.contentType ?? getHeaderValue2(request.headers, "content-type");
|
|
3005
|
+
if (!raw) {
|
|
3006
|
+
return false;
|
|
3007
|
+
}
|
|
3008
|
+
const normalized = raw.trim().toLowerCase();
|
|
3009
|
+
return normalized === expected || normalized.startsWith(`${expected};`);
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
1575
3012
|
function matchAll(first, ...rest) {
|
|
1576
3013
|
const matchers = [first, ...rest];
|
|
1577
3014
|
return (request) => matchers.every((m) => m(request));
|
|
@@ -1580,15 +3017,173 @@ function matchAny(first, ...rest) {
|
|
|
1580
3017
|
const matchers = [first, ...rest];
|
|
1581
3018
|
return (request) => matchers.some((m) => m(request));
|
|
1582
3019
|
}
|
|
3020
|
+
|
|
3021
|
+
// src/diff.ts
|
|
3022
|
+
function isPlainObject(value) {
|
|
3023
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3024
|
+
}
|
|
3025
|
+
function isJsonBody(body) {
|
|
3026
|
+
if (body.length === 0) {
|
|
3027
|
+
return { valid: false };
|
|
3028
|
+
}
|
|
3029
|
+
try {
|
|
3030
|
+
return { valid: true, value: JSON.parse(body) };
|
|
3031
|
+
} catch {
|
|
3032
|
+
return { valid: false };
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
function areEqual(left, right) {
|
|
3036
|
+
if (left === right) {
|
|
3037
|
+
return true;
|
|
3038
|
+
}
|
|
3039
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
3040
|
+
return left.length === right.length && left.every((value, index) => areEqual(value, right[index]));
|
|
3041
|
+
}
|
|
3042
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
3043
|
+
const leftKeys = Object.keys(left);
|
|
3044
|
+
const rightKeys = Object.keys(right);
|
|
3045
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => areEqual(left[key], right[key]));
|
|
3046
|
+
}
|
|
3047
|
+
return Number.isNaN(left) && Number.isNaN(right);
|
|
3048
|
+
}
|
|
3049
|
+
function compareJsonValues(left, right, path, changes) {
|
|
3050
|
+
if (areEqual(left, right)) {
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
3054
|
+
const maxLength = Math.max(left.length, right.length);
|
|
3055
|
+
for (let index = 0; index < maxLength; index++) {
|
|
3056
|
+
const nextPath = path ? `${path}.${index}` : String(index);
|
|
3057
|
+
compareJsonValues(left[index], right[index], nextPath, changes);
|
|
3058
|
+
}
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
3061
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
3062
|
+
const keys = [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].sort();
|
|
3063
|
+
for (const key of keys) {
|
|
3064
|
+
const nextPath = path ? `${path}.${key}` : key;
|
|
3065
|
+
compareJsonValues(left[key], right[key], nextPath, changes);
|
|
3066
|
+
}
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
changes[path || "$"] = { left, right };
|
|
3070
|
+
}
|
|
3071
|
+
function formatJsonDiff(changes) {
|
|
3072
|
+
return Object.entries(changes).map(
|
|
3073
|
+
([path, difference]) => `${path}: ${JSON.stringify(difference.left)} -> ${JSON.stringify(difference.right)}`
|
|
3074
|
+
).join("\n");
|
|
3075
|
+
}
|
|
3076
|
+
function formatTextDiff(left, right) {
|
|
3077
|
+
const leftLines = left.split("\n");
|
|
3078
|
+
const rightLines = right.split("\n");
|
|
3079
|
+
const maxLength = Math.max(leftLines.length, rightLines.length);
|
|
3080
|
+
const lines = [];
|
|
3081
|
+
for (let index = 0; index < maxLength; index++) {
|
|
3082
|
+
const leftLine = leftLines[index];
|
|
3083
|
+
const rightLine = rightLines[index];
|
|
3084
|
+
if (leftLine === rightLine) {
|
|
3085
|
+
continue;
|
|
3086
|
+
}
|
|
3087
|
+
if (leftLine !== void 0) {
|
|
3088
|
+
lines.push(`- ${leftLine}`);
|
|
3089
|
+
}
|
|
3090
|
+
if (rightLine !== void 0) {
|
|
3091
|
+
lines.push(`+ ${rightLine}`);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
return lines.join("\n");
|
|
3095
|
+
}
|
|
3096
|
+
function normalizeHeaders(headers, ignoredHeaders) {
|
|
3097
|
+
const normalized = {};
|
|
3098
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
3099
|
+
const lowerKey = key.toLowerCase();
|
|
3100
|
+
if (!ignoredHeaders.has(lowerKey)) {
|
|
3101
|
+
normalized[lowerKey] = value;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
return normalized;
|
|
3105
|
+
}
|
|
3106
|
+
function diffHeaders(leftHeaders, rightHeaders, options) {
|
|
3107
|
+
const ignoredHeaders = new Set(
|
|
3108
|
+
(options.ignoreHeaders ?? []).map((header) => header.toLowerCase())
|
|
3109
|
+
);
|
|
3110
|
+
const left = normalizeHeaders(leftHeaders, ignoredHeaders);
|
|
3111
|
+
const right = normalizeHeaders(rightHeaders, ignoredHeaders);
|
|
3112
|
+
const leftKeys = new Set(Object.keys(left));
|
|
3113
|
+
const rightKeys = new Set(Object.keys(right));
|
|
3114
|
+
const added = [...rightKeys].filter((key) => !leftKeys.has(key)).sort();
|
|
3115
|
+
const removed = [...leftKeys].filter((key) => !rightKeys.has(key)).sort();
|
|
3116
|
+
const changed = {};
|
|
3117
|
+
for (const key of [...leftKeys].filter((header) => rightKeys.has(header)).sort()) {
|
|
3118
|
+
if (left[key] !== right[key]) {
|
|
3119
|
+
changed[key] = { left: left[key], right: right[key] };
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
if (added.length === 0 && removed.length === 0 && Object.keys(changed).length === 0) {
|
|
3123
|
+
return void 0;
|
|
3124
|
+
}
|
|
3125
|
+
return { added, removed, changed };
|
|
3126
|
+
}
|
|
3127
|
+
function diffBodies(leftBody, rightBody) {
|
|
3128
|
+
const left = leftBody ?? "";
|
|
3129
|
+
const right = rightBody ?? "";
|
|
3130
|
+
if (left === right) {
|
|
3131
|
+
return void 0;
|
|
3132
|
+
}
|
|
3133
|
+
const leftJson = isJsonBody(left);
|
|
3134
|
+
const rightJson = isJsonBody(right);
|
|
3135
|
+
if (leftJson.valid && rightJson.valid) {
|
|
3136
|
+
const changed = {};
|
|
3137
|
+
compareJsonValues(leftJson.value, rightJson.value, "", changed);
|
|
3138
|
+
if (Object.keys(changed).length === 0) {
|
|
3139
|
+
return void 0;
|
|
3140
|
+
}
|
|
3141
|
+
return {
|
|
3142
|
+
type: "json",
|
|
3143
|
+
changed,
|
|
3144
|
+
diff: formatJsonDiff(changed)
|
|
3145
|
+
};
|
|
3146
|
+
}
|
|
3147
|
+
return {
|
|
3148
|
+
type: "text",
|
|
3149
|
+
diff: formatTextDiff(left, right)
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
function diffRequests(left, right, options = {}) {
|
|
3153
|
+
const differences = {};
|
|
3154
|
+
if (left.method !== right.method) {
|
|
3155
|
+
differences.method = { left: left.method, right: right.method };
|
|
3156
|
+
}
|
|
3157
|
+
if (left.path !== right.path) {
|
|
3158
|
+
differences.path = { left: left.path, right: right.path };
|
|
3159
|
+
}
|
|
3160
|
+
const headerDiff = diffHeaders(left.headers, right.headers, options);
|
|
3161
|
+
if (headerDiff) {
|
|
3162
|
+
differences.headers = headerDiff;
|
|
3163
|
+
}
|
|
3164
|
+
const bodyDiff = diffBodies(left.body, right.body);
|
|
3165
|
+
if (bodyDiff) {
|
|
3166
|
+
differences.body = bodyDiff;
|
|
3167
|
+
}
|
|
3168
|
+
return {
|
|
3169
|
+
matches: Object.keys(differences).length === 0,
|
|
3170
|
+
differences
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
1583
3173
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1584
3174
|
0 && (module.exports = {
|
|
1585
3175
|
ApiError,
|
|
1586
3176
|
NotFoundError,
|
|
1587
3177
|
RateLimitError,
|
|
3178
|
+
TEMPLATE_METADATA,
|
|
1588
3179
|
TimeoutError,
|
|
1589
3180
|
UnauthorizedError,
|
|
3181
|
+
WebhookFlowBuilder,
|
|
1590
3182
|
WebhooksCC,
|
|
1591
3183
|
WebhooksCCError,
|
|
3184
|
+
diffRequests,
|
|
3185
|
+
extractJsonField,
|
|
3186
|
+
isDiscordWebhook,
|
|
1592
3187
|
isGitHubWebhook,
|
|
1593
3188
|
isLinearWebhook,
|
|
1594
3189
|
isPaddleWebhook,
|
|
@@ -1600,10 +3195,26 @@ function matchAny(first, ...rest) {
|
|
|
1600
3195
|
matchAll,
|
|
1601
3196
|
matchAny,
|
|
1602
3197
|
matchBodyPath,
|
|
3198
|
+
matchBodySubset,
|
|
3199
|
+
matchContentType,
|
|
1603
3200
|
matchHeader,
|
|
1604
3201
|
matchJsonField,
|
|
1605
3202
|
matchMethod,
|
|
3203
|
+
matchPath,
|
|
3204
|
+
matchQueryParam,
|
|
3205
|
+
parseBody,
|
|
1606
3206
|
parseDuration,
|
|
3207
|
+
parseFormBody,
|
|
1607
3208
|
parseJsonBody,
|
|
1608
|
-
parseSSE
|
|
3209
|
+
parseSSE,
|
|
3210
|
+
verifyDiscordSignature,
|
|
3211
|
+
verifyGitHubSignature,
|
|
3212
|
+
verifyLinearSignature,
|
|
3213
|
+
verifyPaddleSignature,
|
|
3214
|
+
verifyShopifySignature,
|
|
3215
|
+
verifySignature,
|
|
3216
|
+
verifySlackSignature,
|
|
3217
|
+
verifyStandardWebhookSignature,
|
|
3218
|
+
verifyStripeSignature,
|
|
3219
|
+
verifyTwilioSignature
|
|
1609
3220
|
});
|