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