@webhooks-cc/sdk 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +57 -2
- package/dist/index.d.ts +57 -2
- package/dist/index.js +213 -10
- package/dist/index.mjs +212 -10
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -73,7 +73,7 @@ interface SendOptions {
|
|
|
73
73
|
/** Request body (will be JSON-serialized if not a string) */
|
|
74
74
|
body?: unknown;
|
|
75
75
|
}
|
|
76
|
-
type TemplateProvider = "stripe" | "github" | "shopify" | "twilio";
|
|
76
|
+
type TemplateProvider = "stripe" | "github" | "shopify" | "twilio" | "standard-webhooks";
|
|
77
77
|
/**
|
|
78
78
|
* Options for sending a provider template webhook with signed headers.
|
|
79
79
|
*/
|
|
@@ -172,10 +172,34 @@ interface OperationDescription {
|
|
|
172
172
|
description: string;
|
|
173
173
|
params: Record<string, string>;
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Options for sending a webhook directly to an arbitrary URL.
|
|
177
|
+
* Supports optional provider signing (Standard Webhooks, Stripe, etc.).
|
|
178
|
+
*/
|
|
179
|
+
interface SendToOptions {
|
|
180
|
+
/** Provider template for signing (optional). When set, secret is required. */
|
|
181
|
+
provider?: TemplateProvider;
|
|
182
|
+
/** Provider-specific template preset (e.g. "checkout.session.completed" for Stripe) */
|
|
183
|
+
template?: string;
|
|
184
|
+
/** Secret for provider signature generation (required when provider is set) */
|
|
185
|
+
secret?: string;
|
|
186
|
+
/** Event name for provider headers */
|
|
187
|
+
event?: string;
|
|
188
|
+
/** HTTP method (default: "POST") */
|
|
189
|
+
method?: string;
|
|
190
|
+
/** HTTP headers to include */
|
|
191
|
+
headers?: Record<string, string>;
|
|
192
|
+
/** Request body (will be JSON-serialized if not a string) */
|
|
193
|
+
body?: unknown;
|
|
194
|
+
/** Unix timestamp (seconds) override for deterministic signatures in tests */
|
|
195
|
+
timestamp?: number;
|
|
196
|
+
}
|
|
175
197
|
/** Self-describing schema returned by client.describe(). */
|
|
176
198
|
interface SDKDescription {
|
|
177
199
|
version: string;
|
|
178
200
|
endpoints: Record<string, OperationDescription>;
|
|
201
|
+
sendTo: OperationDescription;
|
|
202
|
+
buildRequest: OperationDescription;
|
|
179
203
|
requests: Record<string, OperationDescription>;
|
|
180
204
|
}
|
|
181
205
|
|
|
@@ -252,6 +276,31 @@ declare class WebhooksCC {
|
|
|
252
276
|
send: (slug: string, options?: SendOptions) => Promise<Response>;
|
|
253
277
|
sendTemplate: (slug: string, options: SendTemplateOptions) => Promise<Response>;
|
|
254
278
|
};
|
|
279
|
+
/**
|
|
280
|
+
* Build a request without sending it. Returns the computed method, URL,
|
|
281
|
+
* headers, and body — including any provider signatures. Useful for
|
|
282
|
+
* debugging what sendTo would actually send.
|
|
283
|
+
*
|
|
284
|
+
* @param url - Target URL (http or https)
|
|
285
|
+
* @param options - Same options as sendTo
|
|
286
|
+
* @returns The computed request details
|
|
287
|
+
*/
|
|
288
|
+
buildRequest: (url: string, options?: SendToOptions) => Promise<{
|
|
289
|
+
url: string;
|
|
290
|
+
method: string;
|
|
291
|
+
headers: Record<string, string>;
|
|
292
|
+
body?: string;
|
|
293
|
+
}>;
|
|
294
|
+
/**
|
|
295
|
+
* Send a webhook directly to any URL with optional provider signing.
|
|
296
|
+
* Use this for local integration testing — send properly signed webhooks
|
|
297
|
+
* to localhost handlers without routing through webhooks.cc infrastructure.
|
|
298
|
+
*
|
|
299
|
+
* @param url - Target URL to send the webhook to (http or https)
|
|
300
|
+
* @param options - Method, headers, body, and optional provider signing
|
|
301
|
+
* @returns Raw fetch Response from the target
|
|
302
|
+
*/
|
|
303
|
+
sendTo: (url: string, options?: SendToOptions) => Promise<Response>;
|
|
255
304
|
requests: {
|
|
256
305
|
list: (endpointSlug: string, options?: ListRequestsOptions) => Promise<Request[]>;
|
|
257
306
|
get: (requestId: string) => Promise<Request>;
|
|
@@ -325,6 +374,12 @@ declare function isTwilioWebhook(request: Request): boolean;
|
|
|
325
374
|
declare function isPaddleWebhook(request: Request): boolean;
|
|
326
375
|
/** Check if a request looks like a Linear webhook. */
|
|
327
376
|
declare function isLinearWebhook(request: Request): boolean;
|
|
377
|
+
/**
|
|
378
|
+
* Check if a request looks like a Standard Webhooks request.
|
|
379
|
+
* Matches on the presence of all three Standard Webhooks headers:
|
|
380
|
+
* webhook-id, webhook-timestamp, and webhook-signature.
|
|
381
|
+
*/
|
|
382
|
+
declare function isStandardWebhook(request: Request): boolean;
|
|
328
383
|
|
|
329
384
|
/** Match requests by HTTP method (case-insensitive). */
|
|
330
385
|
declare function matchMethod(method: string): (request: Request) => boolean;
|
|
@@ -376,4 +431,4 @@ interface SSEFrame {
|
|
|
376
431
|
*/
|
|
377
432
|
declare function parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<SSEFrame, void, undefined>;
|
|
378
433
|
|
|
379
|
-
export { ApiError, type ClientHooks, type ClientOptions, type CreateEndpointOptions, type Endpoint, type ErrorHookInfo, type ListRequestsOptions, NotFoundError, type OperationDescription, RateLimitError, type Request, type RequestHookInfo, type ResponseHookInfo, type SDKDescription, type SSEFrame, type SendOptions, type SendTemplateOptions, type SubscribeOptions, type TemplateProvider, TimeoutError, UnauthorizedError, type UpdateEndpointOptions, type WaitForOptions, WebhooksCC, WebhooksCCError, isGitHubWebhook, isLinearWebhook, isPaddleWebhook, isShopifyWebhook, isSlackWebhook, isStripeWebhook, isTwilioWebhook, matchAll, matchAny, matchBodyPath, matchHeader, matchJsonField, matchMethod, parseDuration, parseJsonBody, parseSSE };
|
|
434
|
+
export { ApiError, type ClientHooks, type ClientOptions, type CreateEndpointOptions, type Endpoint, type ErrorHookInfo, type ListRequestsOptions, NotFoundError, type OperationDescription, RateLimitError, type Request, type RequestHookInfo, type ResponseHookInfo, type SDKDescription, type SSEFrame, type SendOptions, type SendTemplateOptions, type SendToOptions, type SubscribeOptions, type TemplateProvider, TimeoutError, UnauthorizedError, type UpdateEndpointOptions, type WaitForOptions, WebhooksCC, WebhooksCCError, isGitHubWebhook, isLinearWebhook, isPaddleWebhook, isShopifyWebhook, isSlackWebhook, isStandardWebhook, isStripeWebhook, isTwilioWebhook, matchAll, matchAny, matchBodyPath, matchHeader, matchJsonField, matchMethod, parseDuration, parseJsonBody, parseSSE };
|
package/dist/index.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ interface SendOptions {
|
|
|
73
73
|
/** Request body (will be JSON-serialized if not a string) */
|
|
74
74
|
body?: unknown;
|
|
75
75
|
}
|
|
76
|
-
type TemplateProvider = "stripe" | "github" | "shopify" | "twilio";
|
|
76
|
+
type TemplateProvider = "stripe" | "github" | "shopify" | "twilio" | "standard-webhooks";
|
|
77
77
|
/**
|
|
78
78
|
* Options for sending a provider template webhook with signed headers.
|
|
79
79
|
*/
|
|
@@ -172,10 +172,34 @@ interface OperationDescription {
|
|
|
172
172
|
description: string;
|
|
173
173
|
params: Record<string, string>;
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Options for sending a webhook directly to an arbitrary URL.
|
|
177
|
+
* Supports optional provider signing (Standard Webhooks, Stripe, etc.).
|
|
178
|
+
*/
|
|
179
|
+
interface SendToOptions {
|
|
180
|
+
/** Provider template for signing (optional). When set, secret is required. */
|
|
181
|
+
provider?: TemplateProvider;
|
|
182
|
+
/** Provider-specific template preset (e.g. "checkout.session.completed" for Stripe) */
|
|
183
|
+
template?: string;
|
|
184
|
+
/** Secret for provider signature generation (required when provider is set) */
|
|
185
|
+
secret?: string;
|
|
186
|
+
/** Event name for provider headers */
|
|
187
|
+
event?: string;
|
|
188
|
+
/** HTTP method (default: "POST") */
|
|
189
|
+
method?: string;
|
|
190
|
+
/** HTTP headers to include */
|
|
191
|
+
headers?: Record<string, string>;
|
|
192
|
+
/** Request body (will be JSON-serialized if not a string) */
|
|
193
|
+
body?: unknown;
|
|
194
|
+
/** Unix timestamp (seconds) override for deterministic signatures in tests */
|
|
195
|
+
timestamp?: number;
|
|
196
|
+
}
|
|
175
197
|
/** Self-describing schema returned by client.describe(). */
|
|
176
198
|
interface SDKDescription {
|
|
177
199
|
version: string;
|
|
178
200
|
endpoints: Record<string, OperationDescription>;
|
|
201
|
+
sendTo: OperationDescription;
|
|
202
|
+
buildRequest: OperationDescription;
|
|
179
203
|
requests: Record<string, OperationDescription>;
|
|
180
204
|
}
|
|
181
205
|
|
|
@@ -252,6 +276,31 @@ declare class WebhooksCC {
|
|
|
252
276
|
send: (slug: string, options?: SendOptions) => Promise<Response>;
|
|
253
277
|
sendTemplate: (slug: string, options: SendTemplateOptions) => Promise<Response>;
|
|
254
278
|
};
|
|
279
|
+
/**
|
|
280
|
+
* Build a request without sending it. Returns the computed method, URL,
|
|
281
|
+
* headers, and body — including any provider signatures. Useful for
|
|
282
|
+
* debugging what sendTo would actually send.
|
|
283
|
+
*
|
|
284
|
+
* @param url - Target URL (http or https)
|
|
285
|
+
* @param options - Same options as sendTo
|
|
286
|
+
* @returns The computed request details
|
|
287
|
+
*/
|
|
288
|
+
buildRequest: (url: string, options?: SendToOptions) => Promise<{
|
|
289
|
+
url: string;
|
|
290
|
+
method: string;
|
|
291
|
+
headers: Record<string, string>;
|
|
292
|
+
body?: string;
|
|
293
|
+
}>;
|
|
294
|
+
/**
|
|
295
|
+
* Send a webhook directly to any URL with optional provider signing.
|
|
296
|
+
* Use this for local integration testing — send properly signed webhooks
|
|
297
|
+
* to localhost handlers without routing through webhooks.cc infrastructure.
|
|
298
|
+
*
|
|
299
|
+
* @param url - Target URL to send the webhook to (http or https)
|
|
300
|
+
* @param options - Method, headers, body, and optional provider signing
|
|
301
|
+
* @returns Raw fetch Response from the target
|
|
302
|
+
*/
|
|
303
|
+
sendTo: (url: string, options?: SendToOptions) => Promise<Response>;
|
|
255
304
|
requests: {
|
|
256
305
|
list: (endpointSlug: string, options?: ListRequestsOptions) => Promise<Request[]>;
|
|
257
306
|
get: (requestId: string) => Promise<Request>;
|
|
@@ -325,6 +374,12 @@ declare function isTwilioWebhook(request: Request): boolean;
|
|
|
325
374
|
declare function isPaddleWebhook(request: Request): boolean;
|
|
326
375
|
/** Check if a request looks like a Linear webhook. */
|
|
327
376
|
declare function isLinearWebhook(request: Request): boolean;
|
|
377
|
+
/**
|
|
378
|
+
* Check if a request looks like a Standard Webhooks request.
|
|
379
|
+
* Matches on the presence of all three Standard Webhooks headers:
|
|
380
|
+
* webhook-id, webhook-timestamp, and webhook-signature.
|
|
381
|
+
*/
|
|
382
|
+
declare function isStandardWebhook(request: Request): boolean;
|
|
328
383
|
|
|
329
384
|
/** Match requests by HTTP method (case-insensitive). */
|
|
330
385
|
declare function matchMethod(method: string): (request: Request) => boolean;
|
|
@@ -376,4 +431,4 @@ interface SSEFrame {
|
|
|
376
431
|
*/
|
|
377
432
|
declare function parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<SSEFrame, void, undefined>;
|
|
378
433
|
|
|
379
|
-
export { ApiError, type ClientHooks, type ClientOptions, type CreateEndpointOptions, type Endpoint, type ErrorHookInfo, type ListRequestsOptions, NotFoundError, type OperationDescription, RateLimitError, type Request, type RequestHookInfo, type ResponseHookInfo, type SDKDescription, type SSEFrame, type SendOptions, type SendTemplateOptions, type SubscribeOptions, type TemplateProvider, TimeoutError, UnauthorizedError, type UpdateEndpointOptions, type WaitForOptions, WebhooksCC, WebhooksCCError, isGitHubWebhook, isLinearWebhook, isPaddleWebhook, isShopifyWebhook, isSlackWebhook, isStripeWebhook, isTwilioWebhook, matchAll, matchAny, matchBodyPath, matchHeader, matchJsonField, matchMethod, parseDuration, parseJsonBody, parseSSE };
|
|
434
|
+
export { ApiError, type ClientHooks, type ClientOptions, type CreateEndpointOptions, type Endpoint, type ErrorHookInfo, type ListRequestsOptions, NotFoundError, type OperationDescription, RateLimitError, type Request, type RequestHookInfo, type ResponseHookInfo, type SDKDescription, type SSEFrame, type SendOptions, type SendTemplateOptions, type SendToOptions, type SubscribeOptions, type TemplateProvider, TimeoutError, UnauthorizedError, type UpdateEndpointOptions, type WaitForOptions, WebhooksCC, WebhooksCCError, isGitHubWebhook, isLinearWebhook, isPaddleWebhook, isShopifyWebhook, isSlackWebhook, isStandardWebhook, isStripeWebhook, isTwilioWebhook, matchAll, matchAny, matchBodyPath, matchHeader, matchJsonField, matchMethod, parseDuration, parseJsonBody, parseSSE };
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ __export(index_exports, {
|
|
|
32
32
|
isPaddleWebhook: () => isPaddleWebhook,
|
|
33
33
|
isShopifyWebhook: () => isShopifyWebhook,
|
|
34
34
|
isSlackWebhook: () => isSlackWebhook,
|
|
35
|
+
isStandardWebhook: () => isStandardWebhook,
|
|
35
36
|
isStripeWebhook: () => isStripeWebhook,
|
|
36
37
|
isTwilioWebhook: () => isTwilioWebhook,
|
|
37
38
|
matchAll: () => matchAll,
|
|
@@ -721,6 +722,33 @@ function toBase64(bytes) {
|
|
|
721
722
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
722
723
|
return btoa(binary);
|
|
723
724
|
}
|
|
725
|
+
function fromBase64(str) {
|
|
726
|
+
if (typeof atob !== "function") {
|
|
727
|
+
return new Uint8Array(Buffer.from(str, "base64"));
|
|
728
|
+
}
|
|
729
|
+
const binary = atob(str);
|
|
730
|
+
const bytes = new Uint8Array(binary.length);
|
|
731
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
732
|
+
return bytes;
|
|
733
|
+
}
|
|
734
|
+
async function hmacSignRaw(algorithm, keyBytes, payload) {
|
|
735
|
+
if (!globalThis.crypto?.subtle) {
|
|
736
|
+
throw new Error("crypto.subtle is required for signature generation");
|
|
737
|
+
}
|
|
738
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
739
|
+
"raw",
|
|
740
|
+
keyBytes.buffer,
|
|
741
|
+
{ name: "HMAC", hash: algorithm },
|
|
742
|
+
false,
|
|
743
|
+
["sign"]
|
|
744
|
+
);
|
|
745
|
+
const signature = await globalThis.crypto.subtle.sign(
|
|
746
|
+
"HMAC",
|
|
747
|
+
key,
|
|
748
|
+
new TextEncoder().encode(payload)
|
|
749
|
+
);
|
|
750
|
+
return new Uint8Array(signature);
|
|
751
|
+
}
|
|
724
752
|
function buildTwilioSignaturePayload(endpointUrl, params) {
|
|
725
753
|
const sortedParams = params.map(([key, value], index) => ({ key, value, index })).sort(
|
|
726
754
|
(a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : a.value < b.value ? -1 : a.value > b.value ? 1 : 0
|
|
@@ -732,35 +760,68 @@ function buildTwilioSignaturePayload(endpointUrl, params) {
|
|
|
732
760
|
return payload;
|
|
733
761
|
}
|
|
734
762
|
async function buildTemplateSendOptions(endpointUrl, options) {
|
|
763
|
+
if (options.provider === "standard-webhooks") {
|
|
764
|
+
const method2 = (options.method ?? "POST").toUpperCase();
|
|
765
|
+
const payload = options.body ?? {};
|
|
766
|
+
const body = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
767
|
+
const msgId = options.event ? `msg_${options.event}_${randomHex(8)}` : `msg_${randomHex(16)}`;
|
|
768
|
+
const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
769
|
+
const signingInput = `${msgId}.${timestamp}.${body}`;
|
|
770
|
+
let rawSecret = options.secret;
|
|
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
|
+
}
|
|
782
|
+
const signature = await hmacSignRaw("SHA-256", secretBytes, signingInput);
|
|
783
|
+
return {
|
|
784
|
+
method: method2,
|
|
785
|
+
headers: {
|
|
786
|
+
"content-type": "application/json",
|
|
787
|
+
"webhook-id": msgId,
|
|
788
|
+
"webhook-timestamp": String(timestamp),
|
|
789
|
+
"webhook-signature": `v1,${toBase64(signature)}`,
|
|
790
|
+
...options.headers ?? {}
|
|
791
|
+
},
|
|
792
|
+
body
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
const provider = options.provider;
|
|
735
796
|
const method = (options.method ?? "POST").toUpperCase();
|
|
736
|
-
const template = ensureTemplate(
|
|
737
|
-
const event = options.event ?? defaultEvent(
|
|
797
|
+
const template = ensureTemplate(provider, options.template);
|
|
798
|
+
const event = options.event ?? defaultEvent(provider, template);
|
|
738
799
|
const now = /* @__PURE__ */ new Date();
|
|
739
|
-
const built = buildTemplatePayload(
|
|
800
|
+
const built = buildTemplatePayload(provider, template, event, now, options.body);
|
|
740
801
|
const headers = {
|
|
741
802
|
"content-type": built.contentType,
|
|
742
|
-
"x-webhooks-cc-template-provider":
|
|
803
|
+
"x-webhooks-cc-template-provider": provider,
|
|
743
804
|
"x-webhooks-cc-template-template": template,
|
|
744
805
|
"x-webhooks-cc-template-event": event,
|
|
745
806
|
...built.headers
|
|
746
807
|
};
|
|
747
|
-
if (
|
|
808
|
+
if (provider === "stripe") {
|
|
748
809
|
const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
749
810
|
const signature = await hmacSign("SHA-256", options.secret, `${timestamp}.${built.body}`);
|
|
750
811
|
headers["stripe-signature"] = `t=${timestamp},v1=${toHex(signature)}`;
|
|
751
812
|
}
|
|
752
|
-
if (
|
|
813
|
+
if (provider === "github") {
|
|
753
814
|
headers["x-github-event"] = event;
|
|
754
815
|
headers["x-github-delivery"] = randomUuid();
|
|
755
816
|
const signature = await hmacSign("SHA-256", options.secret, built.body);
|
|
756
817
|
headers["x-hub-signature-256"] = `sha256=${toHex(signature)}`;
|
|
757
818
|
}
|
|
758
|
-
if (
|
|
819
|
+
if (provider === "shopify") {
|
|
759
820
|
headers["x-shopify-topic"] = event;
|
|
760
821
|
const signature = await hmacSign("SHA-256", options.secret, built.body);
|
|
761
822
|
headers["x-shopify-hmac-sha256"] = toBase64(signature);
|
|
762
823
|
}
|
|
763
|
-
if (
|
|
824
|
+
if (provider === "twilio") {
|
|
764
825
|
const signaturePayload = built.twilioParams ? buildTwilioSignaturePayload(endpointUrl, built.twilioParams) : `${endpointUrl}${built.body}`;
|
|
765
826
|
const signature = await hmacSign("SHA-1", options.secret, signaturePayload);
|
|
766
827
|
headers["x-twilio-signature"] = toBase64(signature);
|
|
@@ -779,7 +840,7 @@ async function buildTemplateSendOptions(endpointUrl, options) {
|
|
|
779
840
|
var DEFAULT_BASE_URL = "https://webhooks.cc";
|
|
780
841
|
var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
|
|
781
842
|
var DEFAULT_TIMEOUT = 3e4;
|
|
782
|
-
var SDK_VERSION = "0.
|
|
843
|
+
var SDK_VERSION = "0.6.0";
|
|
783
844
|
var MIN_POLL_INTERVAL = 10;
|
|
784
845
|
var MAX_POLL_INTERVAL = 6e4;
|
|
785
846
|
var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
|
|
@@ -902,6 +963,123 @@ var WebhooksCC = class {
|
|
|
902
963
|
return this.endpoints.send(slug, sendOptions);
|
|
903
964
|
}
|
|
904
965
|
};
|
|
966
|
+
/**
|
|
967
|
+
* Build a request without sending it. Returns the computed method, URL,
|
|
968
|
+
* headers, and body — including any provider signatures. Useful for
|
|
969
|
+
* debugging what sendTo would actually send.
|
|
970
|
+
*
|
|
971
|
+
* @param url - Target URL (http or https)
|
|
972
|
+
* @param options - Same options as sendTo
|
|
973
|
+
* @returns The computed request details
|
|
974
|
+
*/
|
|
975
|
+
this.buildRequest = async (url, options = {}) => {
|
|
976
|
+
let parsed;
|
|
977
|
+
try {
|
|
978
|
+
parsed = new URL(url);
|
|
979
|
+
} catch {
|
|
980
|
+
throw new Error(`Invalid URL: "${url}" is not a valid URL`);
|
|
981
|
+
}
|
|
982
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
983
|
+
throw new Error("Invalid URL: only http and https protocols are supported");
|
|
984
|
+
}
|
|
985
|
+
if (options.provider) {
|
|
986
|
+
if (!options.secret || typeof options.secret !== "string") {
|
|
987
|
+
throw new Error("buildRequest with a provider requires a non-empty secret");
|
|
988
|
+
}
|
|
989
|
+
const sendOptions = await buildTemplateSendOptions(url, {
|
|
990
|
+
provider: options.provider,
|
|
991
|
+
template: options.template,
|
|
992
|
+
secret: options.secret,
|
|
993
|
+
event: options.event,
|
|
994
|
+
body: options.body,
|
|
995
|
+
method: options.method,
|
|
996
|
+
headers: options.headers,
|
|
997
|
+
timestamp: options.timestamp
|
|
998
|
+
});
|
|
999
|
+
return {
|
|
1000
|
+
url,
|
|
1001
|
+
method: (sendOptions.method ?? "POST").toUpperCase(),
|
|
1002
|
+
headers: sendOptions.headers ?? {},
|
|
1003
|
+
body: sendOptions.body !== void 0 ? typeof sendOptions.body === "string" ? sendOptions.body : JSON.stringify(sendOptions.body) : void 0
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
const method = (options.method ?? "POST").toUpperCase();
|
|
1007
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
const headers = { ...options.headers ?? {} };
|
|
1013
|
+
const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
|
|
1014
|
+
if (options.body !== void 0 && !hasContentType) {
|
|
1015
|
+
headers["Content-Type"] = "application/json";
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
url,
|
|
1019
|
+
method,
|
|
1020
|
+
headers,
|
|
1021
|
+
body: options.body !== void 0 ? typeof options.body === "string" ? options.body : JSON.stringify(options.body) : void 0
|
|
1022
|
+
};
|
|
1023
|
+
};
|
|
1024
|
+
/**
|
|
1025
|
+
* Send a webhook directly to any URL with optional provider signing.
|
|
1026
|
+
* Use this for local integration testing — send properly signed webhooks
|
|
1027
|
+
* to localhost handlers without routing through webhooks.cc infrastructure.
|
|
1028
|
+
*
|
|
1029
|
+
* @param url - Target URL to send the webhook to (http or https)
|
|
1030
|
+
* @param options - Method, headers, body, and optional provider signing
|
|
1031
|
+
* @returns Raw fetch Response from the target
|
|
1032
|
+
*/
|
|
1033
|
+
this.sendTo = async (url, options = {}) => {
|
|
1034
|
+
let parsed;
|
|
1035
|
+
try {
|
|
1036
|
+
parsed = new URL(url);
|
|
1037
|
+
} catch {
|
|
1038
|
+
throw new Error(`Invalid URL: "${url}" is not a valid URL`);
|
|
1039
|
+
}
|
|
1040
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1041
|
+
throw new Error("Invalid URL: only http and https protocols are supported");
|
|
1042
|
+
}
|
|
1043
|
+
if (options.provider) {
|
|
1044
|
+
if (!options.secret || typeof options.secret !== "string") {
|
|
1045
|
+
throw new Error("sendTo with a provider requires a non-empty secret");
|
|
1046
|
+
}
|
|
1047
|
+
const sendOptions = await buildTemplateSendOptions(url, {
|
|
1048
|
+
provider: options.provider,
|
|
1049
|
+
template: options.template,
|
|
1050
|
+
secret: options.secret,
|
|
1051
|
+
event: options.event,
|
|
1052
|
+
body: options.body,
|
|
1053
|
+
method: options.method,
|
|
1054
|
+
headers: options.headers,
|
|
1055
|
+
timestamp: options.timestamp
|
|
1056
|
+
});
|
|
1057
|
+
const method2 = (sendOptions.method ?? "POST").toUpperCase();
|
|
1058
|
+
return fetch(url, {
|
|
1059
|
+
method: method2,
|
|
1060
|
+
headers: sendOptions.headers ?? {},
|
|
1061
|
+
body: sendOptions.body !== void 0 ? typeof sendOptions.body === "string" ? sendOptions.body : JSON.stringify(sendOptions.body) : void 0,
|
|
1062
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
const method = (options.method ?? "POST").toUpperCase();
|
|
1066
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
1067
|
+
throw new Error(
|
|
1068
|
+
`Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
const headers = { ...options.headers ?? {} };
|
|
1072
|
+
const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
|
|
1073
|
+
if (options.body !== void 0 && !hasContentType) {
|
|
1074
|
+
headers["Content-Type"] = "application/json";
|
|
1075
|
+
}
|
|
1076
|
+
return fetch(url, {
|
|
1077
|
+
method,
|
|
1078
|
+
headers,
|
|
1079
|
+
body: options.body !== void 0 ? typeof options.body === "string" ? options.body : JSON.stringify(options.body) : void 0,
|
|
1080
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1081
|
+
});
|
|
1082
|
+
};
|
|
905
1083
|
this.requests = {
|
|
906
1084
|
list: async (endpointSlug, options = {}) => {
|
|
907
1085
|
validatePathSegment(endpointSlug, "endpointSlug");
|
|
@@ -1250,13 +1428,33 @@ var WebhooksCC = class {
|
|
|
1250
1428
|
description: "Send a provider template webhook with signed headers",
|
|
1251
1429
|
params: {
|
|
1252
1430
|
slug: "string",
|
|
1253
|
-
provider: '"stripe"|"github"|"shopify"|"twilio"',
|
|
1431
|
+
provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"',
|
|
1254
1432
|
template: "string?",
|
|
1255
1433
|
secret: "string",
|
|
1256
1434
|
event: "string?"
|
|
1257
1435
|
}
|
|
1258
1436
|
}
|
|
1259
1437
|
},
|
|
1438
|
+
sendTo: {
|
|
1439
|
+
description: "Send a webhook directly to any URL with optional provider signing",
|
|
1440
|
+
params: {
|
|
1441
|
+
url: "string",
|
|
1442
|
+
provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
|
|
1443
|
+
secret: "string?",
|
|
1444
|
+
body: "unknown?",
|
|
1445
|
+
headers: "Record<string, string>?"
|
|
1446
|
+
}
|
|
1447
|
+
},
|
|
1448
|
+
buildRequest: {
|
|
1449
|
+
description: "Build a request without sending it \u2014 returns computed method, URL, headers, and body including provider signatures",
|
|
1450
|
+
params: {
|
|
1451
|
+
url: "string",
|
|
1452
|
+
provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
|
|
1453
|
+
secret: "string?",
|
|
1454
|
+
body: "unknown?",
|
|
1455
|
+
headers: "Record<string, string>?"
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1260
1458
|
requests: {
|
|
1261
1459
|
list: {
|
|
1262
1460
|
description: "List captured requests",
|
|
@@ -1333,6 +1531,10 @@ function isPaddleWebhook(request) {
|
|
|
1333
1531
|
function isLinearWebhook(request) {
|
|
1334
1532
|
return Object.keys(request.headers).some((k) => k.toLowerCase() === "linear-signature");
|
|
1335
1533
|
}
|
|
1534
|
+
function isStandardWebhook(request) {
|
|
1535
|
+
const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
|
|
1536
|
+
return keys.includes("webhook-id") && keys.includes("webhook-timestamp") && keys.includes("webhook-signature");
|
|
1537
|
+
}
|
|
1336
1538
|
|
|
1337
1539
|
// src/matchers.ts
|
|
1338
1540
|
function matchMethod(method) {
|
|
@@ -1392,6 +1594,7 @@ function matchAny(first, ...rest) {
|
|
|
1392
1594
|
isPaddleWebhook,
|
|
1393
1595
|
isShopifyWebhook,
|
|
1394
1596
|
isSlackWebhook,
|
|
1597
|
+
isStandardWebhook,
|
|
1395
1598
|
isStripeWebhook,
|
|
1396
1599
|
isTwilioWebhook,
|
|
1397
1600
|
matchAll,
|
package/dist/index.mjs
CHANGED
|
@@ -673,6 +673,33 @@ function toBase64(bytes) {
|
|
|
673
673
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
674
674
|
return btoa(binary);
|
|
675
675
|
}
|
|
676
|
+
function fromBase64(str) {
|
|
677
|
+
if (typeof atob !== "function") {
|
|
678
|
+
return new Uint8Array(Buffer.from(str, "base64"));
|
|
679
|
+
}
|
|
680
|
+
const binary = atob(str);
|
|
681
|
+
const bytes = new Uint8Array(binary.length);
|
|
682
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
683
|
+
return bytes;
|
|
684
|
+
}
|
|
685
|
+
async function hmacSignRaw(algorithm, keyBytes, payload) {
|
|
686
|
+
if (!globalThis.crypto?.subtle) {
|
|
687
|
+
throw new Error("crypto.subtle is required for signature generation");
|
|
688
|
+
}
|
|
689
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
690
|
+
"raw",
|
|
691
|
+
keyBytes.buffer,
|
|
692
|
+
{ name: "HMAC", hash: algorithm },
|
|
693
|
+
false,
|
|
694
|
+
["sign"]
|
|
695
|
+
);
|
|
696
|
+
const signature = await globalThis.crypto.subtle.sign(
|
|
697
|
+
"HMAC",
|
|
698
|
+
key,
|
|
699
|
+
new TextEncoder().encode(payload)
|
|
700
|
+
);
|
|
701
|
+
return new Uint8Array(signature);
|
|
702
|
+
}
|
|
676
703
|
function buildTwilioSignaturePayload(endpointUrl, params) {
|
|
677
704
|
const sortedParams = params.map(([key, value], index) => ({ key, value, index })).sort(
|
|
678
705
|
(a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : a.value < b.value ? -1 : a.value > b.value ? 1 : 0
|
|
@@ -684,35 +711,68 @@ function buildTwilioSignaturePayload(endpointUrl, params) {
|
|
|
684
711
|
return payload;
|
|
685
712
|
}
|
|
686
713
|
async function buildTemplateSendOptions(endpointUrl, options) {
|
|
714
|
+
if (options.provider === "standard-webhooks") {
|
|
715
|
+
const method2 = (options.method ?? "POST").toUpperCase();
|
|
716
|
+
const payload = options.body ?? {};
|
|
717
|
+
const body = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
718
|
+
const msgId = options.event ? `msg_${options.event}_${randomHex(8)}` : `msg_${randomHex(16)}`;
|
|
719
|
+
const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
720
|
+
const signingInput = `${msgId}.${timestamp}.${body}`;
|
|
721
|
+
let rawSecret = options.secret;
|
|
722
|
+
const hadPrefix = rawSecret.startsWith("whsec_");
|
|
723
|
+
if (hadPrefix) {
|
|
724
|
+
rawSecret = rawSecret.slice(6);
|
|
725
|
+
}
|
|
726
|
+
let secretBytes;
|
|
727
|
+
try {
|
|
728
|
+
secretBytes = fromBase64(rawSecret);
|
|
729
|
+
} catch {
|
|
730
|
+
const raw = hadPrefix ? options.secret : rawSecret;
|
|
731
|
+
secretBytes = new TextEncoder().encode(raw);
|
|
732
|
+
}
|
|
733
|
+
const signature = await hmacSignRaw("SHA-256", secretBytes, signingInput);
|
|
734
|
+
return {
|
|
735
|
+
method: method2,
|
|
736
|
+
headers: {
|
|
737
|
+
"content-type": "application/json",
|
|
738
|
+
"webhook-id": msgId,
|
|
739
|
+
"webhook-timestamp": String(timestamp),
|
|
740
|
+
"webhook-signature": `v1,${toBase64(signature)}`,
|
|
741
|
+
...options.headers ?? {}
|
|
742
|
+
},
|
|
743
|
+
body
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const provider = options.provider;
|
|
687
747
|
const method = (options.method ?? "POST").toUpperCase();
|
|
688
|
-
const template = ensureTemplate(
|
|
689
|
-
const event = options.event ?? defaultEvent(
|
|
748
|
+
const template = ensureTemplate(provider, options.template);
|
|
749
|
+
const event = options.event ?? defaultEvent(provider, template);
|
|
690
750
|
const now = /* @__PURE__ */ new Date();
|
|
691
|
-
const built = buildTemplatePayload(
|
|
751
|
+
const built = buildTemplatePayload(provider, template, event, now, options.body);
|
|
692
752
|
const headers = {
|
|
693
753
|
"content-type": built.contentType,
|
|
694
|
-
"x-webhooks-cc-template-provider":
|
|
754
|
+
"x-webhooks-cc-template-provider": provider,
|
|
695
755
|
"x-webhooks-cc-template-template": template,
|
|
696
756
|
"x-webhooks-cc-template-event": event,
|
|
697
757
|
...built.headers
|
|
698
758
|
};
|
|
699
|
-
if (
|
|
759
|
+
if (provider === "stripe") {
|
|
700
760
|
const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
701
761
|
const signature = await hmacSign("SHA-256", options.secret, `${timestamp}.${built.body}`);
|
|
702
762
|
headers["stripe-signature"] = `t=${timestamp},v1=${toHex(signature)}`;
|
|
703
763
|
}
|
|
704
|
-
if (
|
|
764
|
+
if (provider === "github") {
|
|
705
765
|
headers["x-github-event"] = event;
|
|
706
766
|
headers["x-github-delivery"] = randomUuid();
|
|
707
767
|
const signature = await hmacSign("SHA-256", options.secret, built.body);
|
|
708
768
|
headers["x-hub-signature-256"] = `sha256=${toHex(signature)}`;
|
|
709
769
|
}
|
|
710
|
-
if (
|
|
770
|
+
if (provider === "shopify") {
|
|
711
771
|
headers["x-shopify-topic"] = event;
|
|
712
772
|
const signature = await hmacSign("SHA-256", options.secret, built.body);
|
|
713
773
|
headers["x-shopify-hmac-sha256"] = toBase64(signature);
|
|
714
774
|
}
|
|
715
|
-
if (
|
|
775
|
+
if (provider === "twilio") {
|
|
716
776
|
const signaturePayload = built.twilioParams ? buildTwilioSignaturePayload(endpointUrl, built.twilioParams) : `${endpointUrl}${built.body}`;
|
|
717
777
|
const signature = await hmacSign("SHA-1", options.secret, signaturePayload);
|
|
718
778
|
headers["x-twilio-signature"] = toBase64(signature);
|
|
@@ -731,7 +791,7 @@ async function buildTemplateSendOptions(endpointUrl, options) {
|
|
|
731
791
|
var DEFAULT_BASE_URL = "https://webhooks.cc";
|
|
732
792
|
var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
|
|
733
793
|
var DEFAULT_TIMEOUT = 3e4;
|
|
734
|
-
var SDK_VERSION = "0.
|
|
794
|
+
var SDK_VERSION = "0.6.0";
|
|
735
795
|
var MIN_POLL_INTERVAL = 10;
|
|
736
796
|
var MAX_POLL_INTERVAL = 6e4;
|
|
737
797
|
var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
|
|
@@ -854,6 +914,123 @@ var WebhooksCC = class {
|
|
|
854
914
|
return this.endpoints.send(slug, sendOptions);
|
|
855
915
|
}
|
|
856
916
|
};
|
|
917
|
+
/**
|
|
918
|
+
* Build a request without sending it. Returns the computed method, URL,
|
|
919
|
+
* headers, and body — including any provider signatures. Useful for
|
|
920
|
+
* debugging what sendTo would actually send.
|
|
921
|
+
*
|
|
922
|
+
* @param url - Target URL (http or https)
|
|
923
|
+
* @param options - Same options as sendTo
|
|
924
|
+
* @returns The computed request details
|
|
925
|
+
*/
|
|
926
|
+
this.buildRequest = async (url, options = {}) => {
|
|
927
|
+
let parsed;
|
|
928
|
+
try {
|
|
929
|
+
parsed = new URL(url);
|
|
930
|
+
} catch {
|
|
931
|
+
throw new Error(`Invalid URL: "${url}" is not a valid URL`);
|
|
932
|
+
}
|
|
933
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
934
|
+
throw new Error("Invalid URL: only http and https protocols are supported");
|
|
935
|
+
}
|
|
936
|
+
if (options.provider) {
|
|
937
|
+
if (!options.secret || typeof options.secret !== "string") {
|
|
938
|
+
throw new Error("buildRequest with a provider requires a non-empty secret");
|
|
939
|
+
}
|
|
940
|
+
const sendOptions = await buildTemplateSendOptions(url, {
|
|
941
|
+
provider: options.provider,
|
|
942
|
+
template: options.template,
|
|
943
|
+
secret: options.secret,
|
|
944
|
+
event: options.event,
|
|
945
|
+
body: options.body,
|
|
946
|
+
method: options.method,
|
|
947
|
+
headers: options.headers,
|
|
948
|
+
timestamp: options.timestamp
|
|
949
|
+
});
|
|
950
|
+
return {
|
|
951
|
+
url,
|
|
952
|
+
method: (sendOptions.method ?? "POST").toUpperCase(),
|
|
953
|
+
headers: sendOptions.headers ?? {},
|
|
954
|
+
body: sendOptions.body !== void 0 ? typeof sendOptions.body === "string" ? sendOptions.body : JSON.stringify(sendOptions.body) : void 0
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
const method = (options.method ?? "POST").toUpperCase();
|
|
958
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
959
|
+
throw new Error(
|
|
960
|
+
`Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
const headers = { ...options.headers ?? {} };
|
|
964
|
+
const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
|
|
965
|
+
if (options.body !== void 0 && !hasContentType) {
|
|
966
|
+
headers["Content-Type"] = "application/json";
|
|
967
|
+
}
|
|
968
|
+
return {
|
|
969
|
+
url,
|
|
970
|
+
method,
|
|
971
|
+
headers,
|
|
972
|
+
body: options.body !== void 0 ? typeof options.body === "string" ? options.body : JSON.stringify(options.body) : void 0
|
|
973
|
+
};
|
|
974
|
+
};
|
|
975
|
+
/**
|
|
976
|
+
* Send a webhook directly to any URL with optional provider signing.
|
|
977
|
+
* Use this for local integration testing — send properly signed webhooks
|
|
978
|
+
* to localhost handlers without routing through webhooks.cc infrastructure.
|
|
979
|
+
*
|
|
980
|
+
* @param url - Target URL to send the webhook to (http or https)
|
|
981
|
+
* @param options - Method, headers, body, and optional provider signing
|
|
982
|
+
* @returns Raw fetch Response from the target
|
|
983
|
+
*/
|
|
984
|
+
this.sendTo = async (url, options = {}) => {
|
|
985
|
+
let parsed;
|
|
986
|
+
try {
|
|
987
|
+
parsed = new URL(url);
|
|
988
|
+
} catch {
|
|
989
|
+
throw new Error(`Invalid URL: "${url}" is not a valid URL`);
|
|
990
|
+
}
|
|
991
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
992
|
+
throw new Error("Invalid URL: only http and https protocols are supported");
|
|
993
|
+
}
|
|
994
|
+
if (options.provider) {
|
|
995
|
+
if (!options.secret || typeof options.secret !== "string") {
|
|
996
|
+
throw new Error("sendTo with a provider requires a non-empty secret");
|
|
997
|
+
}
|
|
998
|
+
const sendOptions = await buildTemplateSendOptions(url, {
|
|
999
|
+
provider: options.provider,
|
|
1000
|
+
template: options.template,
|
|
1001
|
+
secret: options.secret,
|
|
1002
|
+
event: options.event,
|
|
1003
|
+
body: options.body,
|
|
1004
|
+
method: options.method,
|
|
1005
|
+
headers: options.headers,
|
|
1006
|
+
timestamp: options.timestamp
|
|
1007
|
+
});
|
|
1008
|
+
const method2 = (sendOptions.method ?? "POST").toUpperCase();
|
|
1009
|
+
return fetch(url, {
|
|
1010
|
+
method: method2,
|
|
1011
|
+
headers: sendOptions.headers ?? {},
|
|
1012
|
+
body: sendOptions.body !== void 0 ? typeof sendOptions.body === "string" ? sendOptions.body : JSON.stringify(sendOptions.body) : void 0,
|
|
1013
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
const method = (options.method ?? "POST").toUpperCase();
|
|
1017
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
`Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
const headers = { ...options.headers ?? {} };
|
|
1023
|
+
const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
|
|
1024
|
+
if (options.body !== void 0 && !hasContentType) {
|
|
1025
|
+
headers["Content-Type"] = "application/json";
|
|
1026
|
+
}
|
|
1027
|
+
return fetch(url, {
|
|
1028
|
+
method,
|
|
1029
|
+
headers,
|
|
1030
|
+
body: options.body !== void 0 ? typeof options.body === "string" ? options.body : JSON.stringify(options.body) : void 0,
|
|
1031
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1032
|
+
});
|
|
1033
|
+
};
|
|
857
1034
|
this.requests = {
|
|
858
1035
|
list: async (endpointSlug, options = {}) => {
|
|
859
1036
|
validatePathSegment(endpointSlug, "endpointSlug");
|
|
@@ -1202,13 +1379,33 @@ var WebhooksCC = class {
|
|
|
1202
1379
|
description: "Send a provider template webhook with signed headers",
|
|
1203
1380
|
params: {
|
|
1204
1381
|
slug: "string",
|
|
1205
|
-
provider: '"stripe"|"github"|"shopify"|"twilio"',
|
|
1382
|
+
provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"',
|
|
1206
1383
|
template: "string?",
|
|
1207
1384
|
secret: "string",
|
|
1208
1385
|
event: "string?"
|
|
1209
1386
|
}
|
|
1210
1387
|
}
|
|
1211
1388
|
},
|
|
1389
|
+
sendTo: {
|
|
1390
|
+
description: "Send a webhook directly to any URL with optional provider signing",
|
|
1391
|
+
params: {
|
|
1392
|
+
url: "string",
|
|
1393
|
+
provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
|
|
1394
|
+
secret: "string?",
|
|
1395
|
+
body: "unknown?",
|
|
1396
|
+
headers: "Record<string, string>?"
|
|
1397
|
+
}
|
|
1398
|
+
},
|
|
1399
|
+
buildRequest: {
|
|
1400
|
+
description: "Build a request without sending it \u2014 returns computed method, URL, headers, and body including provider signatures",
|
|
1401
|
+
params: {
|
|
1402
|
+
url: "string",
|
|
1403
|
+
provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
|
|
1404
|
+
secret: "string?",
|
|
1405
|
+
body: "unknown?",
|
|
1406
|
+
headers: "Record<string, string>?"
|
|
1407
|
+
}
|
|
1408
|
+
},
|
|
1212
1409
|
requests: {
|
|
1213
1410
|
list: {
|
|
1214
1411
|
description: "List captured requests",
|
|
@@ -1285,6 +1482,10 @@ function isPaddleWebhook(request) {
|
|
|
1285
1482
|
function isLinearWebhook(request) {
|
|
1286
1483
|
return Object.keys(request.headers).some((k) => k.toLowerCase() === "linear-signature");
|
|
1287
1484
|
}
|
|
1485
|
+
function isStandardWebhook(request) {
|
|
1486
|
+
const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
|
|
1487
|
+
return keys.includes("webhook-id") && keys.includes("webhook-timestamp") && keys.includes("webhook-signature");
|
|
1488
|
+
}
|
|
1288
1489
|
|
|
1289
1490
|
// src/matchers.ts
|
|
1290
1491
|
function matchMethod(method) {
|
|
@@ -1343,6 +1544,7 @@ export {
|
|
|
1343
1544
|
isPaddleWebhook,
|
|
1344
1545
|
isShopifyWebhook,
|
|
1345
1546
|
isSlackWebhook,
|
|
1547
|
+
isStandardWebhook,
|
|
1346
1548
|
isStripeWebhook,
|
|
1347
1549
|
isTwilioWebhook,
|
|
1348
1550
|
matchAll,
|