@webhooks-cc/sdk 0.4.0 → 0.5.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 +41 -2
- package/dist/index.d.ts +41 -2
- package/dist/index.js +138 -10
- package/dist/index.mjs +137 -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,33 @@ 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;
|
|
179
202
|
requests: Record<string, OperationDescription>;
|
|
180
203
|
}
|
|
181
204
|
|
|
@@ -252,6 +275,16 @@ declare class WebhooksCC {
|
|
|
252
275
|
send: (slug: string, options?: SendOptions) => Promise<Response>;
|
|
253
276
|
sendTemplate: (slug: string, options: SendTemplateOptions) => Promise<Response>;
|
|
254
277
|
};
|
|
278
|
+
/**
|
|
279
|
+
* Send a webhook directly to any URL with optional provider signing.
|
|
280
|
+
* Use this for local integration testing — send properly signed webhooks
|
|
281
|
+
* to localhost handlers without routing through webhooks.cc infrastructure.
|
|
282
|
+
*
|
|
283
|
+
* @param url - Target URL to send the webhook to (http or https)
|
|
284
|
+
* @param options - Method, headers, body, and optional provider signing
|
|
285
|
+
* @returns Raw fetch Response from the target
|
|
286
|
+
*/
|
|
287
|
+
sendTo: (url: string, options?: SendToOptions) => Promise<Response>;
|
|
255
288
|
requests: {
|
|
256
289
|
list: (endpointSlug: string, options?: ListRequestsOptions) => Promise<Request[]>;
|
|
257
290
|
get: (requestId: string) => Promise<Request>;
|
|
@@ -325,6 +358,12 @@ declare function isTwilioWebhook(request: Request): boolean;
|
|
|
325
358
|
declare function isPaddleWebhook(request: Request): boolean;
|
|
326
359
|
/** Check if a request looks like a Linear webhook. */
|
|
327
360
|
declare function isLinearWebhook(request: Request): boolean;
|
|
361
|
+
/**
|
|
362
|
+
* Check if a request looks like a Standard Webhooks request.
|
|
363
|
+
* Matches on the presence of all three Standard Webhooks headers:
|
|
364
|
+
* webhook-id, webhook-timestamp, and webhook-signature.
|
|
365
|
+
*/
|
|
366
|
+
declare function isStandardWebhook(request: Request): boolean;
|
|
328
367
|
|
|
329
368
|
/** Match requests by HTTP method (case-insensitive). */
|
|
330
369
|
declare function matchMethod(method: string): (request: Request) => boolean;
|
|
@@ -376,4 +415,4 @@ interface SSEFrame {
|
|
|
376
415
|
*/
|
|
377
416
|
declare function parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<SSEFrame, void, undefined>;
|
|
378
417
|
|
|
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 };
|
|
418
|
+
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,33 @@ 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;
|
|
179
202
|
requests: Record<string, OperationDescription>;
|
|
180
203
|
}
|
|
181
204
|
|
|
@@ -252,6 +275,16 @@ declare class WebhooksCC {
|
|
|
252
275
|
send: (slug: string, options?: SendOptions) => Promise<Response>;
|
|
253
276
|
sendTemplate: (slug: string, options: SendTemplateOptions) => Promise<Response>;
|
|
254
277
|
};
|
|
278
|
+
/**
|
|
279
|
+
* Send a webhook directly to any URL with optional provider signing.
|
|
280
|
+
* Use this for local integration testing — send properly signed webhooks
|
|
281
|
+
* to localhost handlers without routing through webhooks.cc infrastructure.
|
|
282
|
+
*
|
|
283
|
+
* @param url - Target URL to send the webhook to (http or https)
|
|
284
|
+
* @param options - Method, headers, body, and optional provider signing
|
|
285
|
+
* @returns Raw fetch Response from the target
|
|
286
|
+
*/
|
|
287
|
+
sendTo: (url: string, options?: SendToOptions) => Promise<Response>;
|
|
255
288
|
requests: {
|
|
256
289
|
list: (endpointSlug: string, options?: ListRequestsOptions) => Promise<Request[]>;
|
|
257
290
|
get: (requestId: string) => Promise<Request>;
|
|
@@ -325,6 +358,12 @@ declare function isTwilioWebhook(request: Request): boolean;
|
|
|
325
358
|
declare function isPaddleWebhook(request: Request): boolean;
|
|
326
359
|
/** Check if a request looks like a Linear webhook. */
|
|
327
360
|
declare function isLinearWebhook(request: Request): boolean;
|
|
361
|
+
/**
|
|
362
|
+
* Check if a request looks like a Standard Webhooks request.
|
|
363
|
+
* Matches on the presence of all three Standard Webhooks headers:
|
|
364
|
+
* webhook-id, webhook-timestamp, and webhook-signature.
|
|
365
|
+
*/
|
|
366
|
+
declare function isStandardWebhook(request: Request): boolean;
|
|
328
367
|
|
|
329
368
|
/** Match requests by HTTP method (case-insensitive). */
|
|
330
369
|
declare function matchMethod(method: string): (request: Request) => boolean;
|
|
@@ -376,4 +415,4 @@ interface SSEFrame {
|
|
|
376
415
|
*/
|
|
377
416
|
declare function parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<SSEFrame, void, undefined>;
|
|
378
417
|
|
|
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 };
|
|
418
|
+
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,61 @@ 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
|
+
if (rawSecret.startsWith("whsec_")) {
|
|
772
|
+
rawSecret = rawSecret.slice(6);
|
|
773
|
+
}
|
|
774
|
+
const secretBytes = fromBase64(rawSecret);
|
|
775
|
+
const signature = await hmacSignRaw("SHA-256", secretBytes, signingInput);
|
|
776
|
+
return {
|
|
777
|
+
method: method2,
|
|
778
|
+
headers: {
|
|
779
|
+
"content-type": "application/json",
|
|
780
|
+
"webhook-id": msgId,
|
|
781
|
+
"webhook-timestamp": String(timestamp),
|
|
782
|
+
"webhook-signature": `v1,${toBase64(signature)}`,
|
|
783
|
+
...options.headers ?? {}
|
|
784
|
+
},
|
|
785
|
+
body
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
const provider = options.provider;
|
|
735
789
|
const method = (options.method ?? "POST").toUpperCase();
|
|
736
|
-
const template = ensureTemplate(
|
|
737
|
-
const event = options.event ?? defaultEvent(
|
|
790
|
+
const template = ensureTemplate(provider, options.template);
|
|
791
|
+
const event = options.event ?? defaultEvent(provider, template);
|
|
738
792
|
const now = /* @__PURE__ */ new Date();
|
|
739
|
-
const built = buildTemplatePayload(
|
|
793
|
+
const built = buildTemplatePayload(provider, template, event, now, options.body);
|
|
740
794
|
const headers = {
|
|
741
795
|
"content-type": built.contentType,
|
|
742
|
-
"x-webhooks-cc-template-provider":
|
|
796
|
+
"x-webhooks-cc-template-provider": provider,
|
|
743
797
|
"x-webhooks-cc-template-template": template,
|
|
744
798
|
"x-webhooks-cc-template-event": event,
|
|
745
799
|
...built.headers
|
|
746
800
|
};
|
|
747
|
-
if (
|
|
801
|
+
if (provider === "stripe") {
|
|
748
802
|
const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
749
803
|
const signature = await hmacSign("SHA-256", options.secret, `${timestamp}.${built.body}`);
|
|
750
804
|
headers["stripe-signature"] = `t=${timestamp},v1=${toHex(signature)}`;
|
|
751
805
|
}
|
|
752
|
-
if (
|
|
806
|
+
if (provider === "github") {
|
|
753
807
|
headers["x-github-event"] = event;
|
|
754
808
|
headers["x-github-delivery"] = randomUuid();
|
|
755
809
|
const signature = await hmacSign("SHA-256", options.secret, built.body);
|
|
756
810
|
headers["x-hub-signature-256"] = `sha256=${toHex(signature)}`;
|
|
757
811
|
}
|
|
758
|
-
if (
|
|
812
|
+
if (provider === "shopify") {
|
|
759
813
|
headers["x-shopify-topic"] = event;
|
|
760
814
|
const signature = await hmacSign("SHA-256", options.secret, built.body);
|
|
761
815
|
headers["x-shopify-hmac-sha256"] = toBase64(signature);
|
|
762
816
|
}
|
|
763
|
-
if (
|
|
817
|
+
if (provider === "twilio") {
|
|
764
818
|
const signaturePayload = built.twilioParams ? buildTwilioSignaturePayload(endpointUrl, built.twilioParams) : `${endpointUrl}${built.body}`;
|
|
765
819
|
const signature = await hmacSign("SHA-1", options.secret, signaturePayload);
|
|
766
820
|
headers["x-twilio-signature"] = toBase64(signature);
|
|
@@ -779,7 +833,7 @@ async function buildTemplateSendOptions(endpointUrl, options) {
|
|
|
779
833
|
var DEFAULT_BASE_URL = "https://webhooks.cc";
|
|
780
834
|
var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
|
|
781
835
|
var DEFAULT_TIMEOUT = 3e4;
|
|
782
|
-
var SDK_VERSION = "0.
|
|
836
|
+
var SDK_VERSION = "0.5.0";
|
|
783
837
|
var MIN_POLL_INTERVAL = 10;
|
|
784
838
|
var MAX_POLL_INTERVAL = 6e4;
|
|
785
839
|
var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
|
|
@@ -902,6 +956,65 @@ var WebhooksCC = class {
|
|
|
902
956
|
return this.endpoints.send(slug, sendOptions);
|
|
903
957
|
}
|
|
904
958
|
};
|
|
959
|
+
/**
|
|
960
|
+
* Send a webhook directly to any URL with optional provider signing.
|
|
961
|
+
* Use this for local integration testing — send properly signed webhooks
|
|
962
|
+
* to localhost handlers without routing through webhooks.cc infrastructure.
|
|
963
|
+
*
|
|
964
|
+
* @param url - Target URL to send the webhook to (http or https)
|
|
965
|
+
* @param options - Method, headers, body, and optional provider signing
|
|
966
|
+
* @returns Raw fetch Response from the target
|
|
967
|
+
*/
|
|
968
|
+
this.sendTo = async (url, options = {}) => {
|
|
969
|
+
let parsed;
|
|
970
|
+
try {
|
|
971
|
+
parsed = new URL(url);
|
|
972
|
+
} catch {
|
|
973
|
+
throw new Error(`Invalid URL: "${url}" is not a valid URL`);
|
|
974
|
+
}
|
|
975
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
976
|
+
throw new Error("Invalid URL: only http and https protocols are supported");
|
|
977
|
+
}
|
|
978
|
+
if (options.provider) {
|
|
979
|
+
if (!options.secret || typeof options.secret !== "string") {
|
|
980
|
+
throw new Error("sendTo with a provider requires a non-empty secret");
|
|
981
|
+
}
|
|
982
|
+
const sendOptions = await buildTemplateSendOptions(url, {
|
|
983
|
+
provider: options.provider,
|
|
984
|
+
template: options.template,
|
|
985
|
+
secret: options.secret,
|
|
986
|
+
event: options.event,
|
|
987
|
+
body: options.body,
|
|
988
|
+
method: options.method,
|
|
989
|
+
headers: options.headers,
|
|
990
|
+
timestamp: options.timestamp
|
|
991
|
+
});
|
|
992
|
+
const method2 = (sendOptions.method ?? "POST").toUpperCase();
|
|
993
|
+
return fetch(url, {
|
|
994
|
+
method: method2,
|
|
995
|
+
headers: sendOptions.headers ?? {},
|
|
996
|
+
body: sendOptions.body !== void 0 ? typeof sendOptions.body === "string" ? sendOptions.body : JSON.stringify(sendOptions.body) : void 0,
|
|
997
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
const method = (options.method ?? "POST").toUpperCase();
|
|
1001
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
`Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
const headers = { ...options.headers ?? {} };
|
|
1007
|
+
const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
|
|
1008
|
+
if (options.body !== void 0 && !hasContentType) {
|
|
1009
|
+
headers["Content-Type"] = "application/json";
|
|
1010
|
+
}
|
|
1011
|
+
return fetch(url, {
|
|
1012
|
+
method,
|
|
1013
|
+
headers,
|
|
1014
|
+
body: options.body !== void 0 ? typeof options.body === "string" ? options.body : JSON.stringify(options.body) : void 0,
|
|
1015
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1016
|
+
});
|
|
1017
|
+
};
|
|
905
1018
|
this.requests = {
|
|
906
1019
|
list: async (endpointSlug, options = {}) => {
|
|
907
1020
|
validatePathSegment(endpointSlug, "endpointSlug");
|
|
@@ -1250,13 +1363,23 @@ var WebhooksCC = class {
|
|
|
1250
1363
|
description: "Send a provider template webhook with signed headers",
|
|
1251
1364
|
params: {
|
|
1252
1365
|
slug: "string",
|
|
1253
|
-
provider: '"stripe"|"github"|"shopify"|"twilio"',
|
|
1366
|
+
provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"',
|
|
1254
1367
|
template: "string?",
|
|
1255
1368
|
secret: "string",
|
|
1256
1369
|
event: "string?"
|
|
1257
1370
|
}
|
|
1258
1371
|
}
|
|
1259
1372
|
},
|
|
1373
|
+
sendTo: {
|
|
1374
|
+
description: "Send a webhook directly to any URL with optional provider signing",
|
|
1375
|
+
params: {
|
|
1376
|
+
url: "string",
|
|
1377
|
+
provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
|
|
1378
|
+
secret: "string?",
|
|
1379
|
+
body: "unknown?",
|
|
1380
|
+
headers: "Record<string, string>?"
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1260
1383
|
requests: {
|
|
1261
1384
|
list: {
|
|
1262
1385
|
description: "List captured requests",
|
|
@@ -1333,6 +1456,10 @@ function isPaddleWebhook(request) {
|
|
|
1333
1456
|
function isLinearWebhook(request) {
|
|
1334
1457
|
return Object.keys(request.headers).some((k) => k.toLowerCase() === "linear-signature");
|
|
1335
1458
|
}
|
|
1459
|
+
function isStandardWebhook(request) {
|
|
1460
|
+
const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
|
|
1461
|
+
return keys.includes("webhook-id") && keys.includes("webhook-timestamp") && keys.includes("webhook-signature");
|
|
1462
|
+
}
|
|
1336
1463
|
|
|
1337
1464
|
// src/matchers.ts
|
|
1338
1465
|
function matchMethod(method) {
|
|
@@ -1392,6 +1519,7 @@ function matchAny(first, ...rest) {
|
|
|
1392
1519
|
isPaddleWebhook,
|
|
1393
1520
|
isShopifyWebhook,
|
|
1394
1521
|
isSlackWebhook,
|
|
1522
|
+
isStandardWebhook,
|
|
1395
1523
|
isStripeWebhook,
|
|
1396
1524
|
isTwilioWebhook,
|
|
1397
1525
|
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,61 @@ 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
|
+
if (rawSecret.startsWith("whsec_")) {
|
|
723
|
+
rawSecret = rawSecret.slice(6);
|
|
724
|
+
}
|
|
725
|
+
const secretBytes = fromBase64(rawSecret);
|
|
726
|
+
const signature = await hmacSignRaw("SHA-256", secretBytes, signingInput);
|
|
727
|
+
return {
|
|
728
|
+
method: method2,
|
|
729
|
+
headers: {
|
|
730
|
+
"content-type": "application/json",
|
|
731
|
+
"webhook-id": msgId,
|
|
732
|
+
"webhook-timestamp": String(timestamp),
|
|
733
|
+
"webhook-signature": `v1,${toBase64(signature)}`,
|
|
734
|
+
...options.headers ?? {}
|
|
735
|
+
},
|
|
736
|
+
body
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
const provider = options.provider;
|
|
687
740
|
const method = (options.method ?? "POST").toUpperCase();
|
|
688
|
-
const template = ensureTemplate(
|
|
689
|
-
const event = options.event ?? defaultEvent(
|
|
741
|
+
const template = ensureTemplate(provider, options.template);
|
|
742
|
+
const event = options.event ?? defaultEvent(provider, template);
|
|
690
743
|
const now = /* @__PURE__ */ new Date();
|
|
691
|
-
const built = buildTemplatePayload(
|
|
744
|
+
const built = buildTemplatePayload(provider, template, event, now, options.body);
|
|
692
745
|
const headers = {
|
|
693
746
|
"content-type": built.contentType,
|
|
694
|
-
"x-webhooks-cc-template-provider":
|
|
747
|
+
"x-webhooks-cc-template-provider": provider,
|
|
695
748
|
"x-webhooks-cc-template-template": template,
|
|
696
749
|
"x-webhooks-cc-template-event": event,
|
|
697
750
|
...built.headers
|
|
698
751
|
};
|
|
699
|
-
if (
|
|
752
|
+
if (provider === "stripe") {
|
|
700
753
|
const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
701
754
|
const signature = await hmacSign("SHA-256", options.secret, `${timestamp}.${built.body}`);
|
|
702
755
|
headers["stripe-signature"] = `t=${timestamp},v1=${toHex(signature)}`;
|
|
703
756
|
}
|
|
704
|
-
if (
|
|
757
|
+
if (provider === "github") {
|
|
705
758
|
headers["x-github-event"] = event;
|
|
706
759
|
headers["x-github-delivery"] = randomUuid();
|
|
707
760
|
const signature = await hmacSign("SHA-256", options.secret, built.body);
|
|
708
761
|
headers["x-hub-signature-256"] = `sha256=${toHex(signature)}`;
|
|
709
762
|
}
|
|
710
|
-
if (
|
|
763
|
+
if (provider === "shopify") {
|
|
711
764
|
headers["x-shopify-topic"] = event;
|
|
712
765
|
const signature = await hmacSign("SHA-256", options.secret, built.body);
|
|
713
766
|
headers["x-shopify-hmac-sha256"] = toBase64(signature);
|
|
714
767
|
}
|
|
715
|
-
if (
|
|
768
|
+
if (provider === "twilio") {
|
|
716
769
|
const signaturePayload = built.twilioParams ? buildTwilioSignaturePayload(endpointUrl, built.twilioParams) : `${endpointUrl}${built.body}`;
|
|
717
770
|
const signature = await hmacSign("SHA-1", options.secret, signaturePayload);
|
|
718
771
|
headers["x-twilio-signature"] = toBase64(signature);
|
|
@@ -731,7 +784,7 @@ async function buildTemplateSendOptions(endpointUrl, options) {
|
|
|
731
784
|
var DEFAULT_BASE_URL = "https://webhooks.cc";
|
|
732
785
|
var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
|
|
733
786
|
var DEFAULT_TIMEOUT = 3e4;
|
|
734
|
-
var SDK_VERSION = "0.
|
|
787
|
+
var SDK_VERSION = "0.5.0";
|
|
735
788
|
var MIN_POLL_INTERVAL = 10;
|
|
736
789
|
var MAX_POLL_INTERVAL = 6e4;
|
|
737
790
|
var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
|
|
@@ -854,6 +907,65 @@ var WebhooksCC = class {
|
|
|
854
907
|
return this.endpoints.send(slug, sendOptions);
|
|
855
908
|
}
|
|
856
909
|
};
|
|
910
|
+
/**
|
|
911
|
+
* Send a webhook directly to any URL with optional provider signing.
|
|
912
|
+
* Use this for local integration testing — send properly signed webhooks
|
|
913
|
+
* to localhost handlers without routing through webhooks.cc infrastructure.
|
|
914
|
+
*
|
|
915
|
+
* @param url - Target URL to send the webhook to (http or https)
|
|
916
|
+
* @param options - Method, headers, body, and optional provider signing
|
|
917
|
+
* @returns Raw fetch Response from the target
|
|
918
|
+
*/
|
|
919
|
+
this.sendTo = async (url, options = {}) => {
|
|
920
|
+
let parsed;
|
|
921
|
+
try {
|
|
922
|
+
parsed = new URL(url);
|
|
923
|
+
} catch {
|
|
924
|
+
throw new Error(`Invalid URL: "${url}" is not a valid URL`);
|
|
925
|
+
}
|
|
926
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
927
|
+
throw new Error("Invalid URL: only http and https protocols are supported");
|
|
928
|
+
}
|
|
929
|
+
if (options.provider) {
|
|
930
|
+
if (!options.secret || typeof options.secret !== "string") {
|
|
931
|
+
throw new Error("sendTo with a provider requires a non-empty secret");
|
|
932
|
+
}
|
|
933
|
+
const sendOptions = await buildTemplateSendOptions(url, {
|
|
934
|
+
provider: options.provider,
|
|
935
|
+
template: options.template,
|
|
936
|
+
secret: options.secret,
|
|
937
|
+
event: options.event,
|
|
938
|
+
body: options.body,
|
|
939
|
+
method: options.method,
|
|
940
|
+
headers: options.headers,
|
|
941
|
+
timestamp: options.timestamp
|
|
942
|
+
});
|
|
943
|
+
const method2 = (sendOptions.method ?? "POST").toUpperCase();
|
|
944
|
+
return fetch(url, {
|
|
945
|
+
method: method2,
|
|
946
|
+
headers: sendOptions.headers ?? {},
|
|
947
|
+
body: sendOptions.body !== void 0 ? typeof sendOptions.body === "string" ? sendOptions.body : JSON.stringify(sendOptions.body) : void 0,
|
|
948
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
const method = (options.method ?? "POST").toUpperCase();
|
|
952
|
+
if (!ALLOWED_METHODS.has(method)) {
|
|
953
|
+
throw new Error(
|
|
954
|
+
`Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
const headers = { ...options.headers ?? {} };
|
|
958
|
+
const hasContentType = Object.keys(headers).some((k) => k.toLowerCase() === "content-type");
|
|
959
|
+
if (options.body !== void 0 && !hasContentType) {
|
|
960
|
+
headers["Content-Type"] = "application/json";
|
|
961
|
+
}
|
|
962
|
+
return fetch(url, {
|
|
963
|
+
method,
|
|
964
|
+
headers,
|
|
965
|
+
body: options.body !== void 0 ? typeof options.body === "string" ? options.body : JSON.stringify(options.body) : void 0,
|
|
966
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
967
|
+
});
|
|
968
|
+
};
|
|
857
969
|
this.requests = {
|
|
858
970
|
list: async (endpointSlug, options = {}) => {
|
|
859
971
|
validatePathSegment(endpointSlug, "endpointSlug");
|
|
@@ -1202,13 +1314,23 @@ var WebhooksCC = class {
|
|
|
1202
1314
|
description: "Send a provider template webhook with signed headers",
|
|
1203
1315
|
params: {
|
|
1204
1316
|
slug: "string",
|
|
1205
|
-
provider: '"stripe"|"github"|"shopify"|"twilio"',
|
|
1317
|
+
provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"',
|
|
1206
1318
|
template: "string?",
|
|
1207
1319
|
secret: "string",
|
|
1208
1320
|
event: "string?"
|
|
1209
1321
|
}
|
|
1210
1322
|
}
|
|
1211
1323
|
},
|
|
1324
|
+
sendTo: {
|
|
1325
|
+
description: "Send a webhook directly to any URL with optional provider signing",
|
|
1326
|
+
params: {
|
|
1327
|
+
url: "string",
|
|
1328
|
+
provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"?',
|
|
1329
|
+
secret: "string?",
|
|
1330
|
+
body: "unknown?",
|
|
1331
|
+
headers: "Record<string, string>?"
|
|
1332
|
+
}
|
|
1333
|
+
},
|
|
1212
1334
|
requests: {
|
|
1213
1335
|
list: {
|
|
1214
1336
|
description: "List captured requests",
|
|
@@ -1285,6 +1407,10 @@ function isPaddleWebhook(request) {
|
|
|
1285
1407
|
function isLinearWebhook(request) {
|
|
1286
1408
|
return Object.keys(request.headers).some((k) => k.toLowerCase() === "linear-signature");
|
|
1287
1409
|
}
|
|
1410
|
+
function isStandardWebhook(request) {
|
|
1411
|
+
const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
|
|
1412
|
+
return keys.includes("webhook-id") && keys.includes("webhook-timestamp") && keys.includes("webhook-signature");
|
|
1413
|
+
}
|
|
1288
1414
|
|
|
1289
1415
|
// src/matchers.ts
|
|
1290
1416
|
function matchMethod(method) {
|
|
@@ -1343,6 +1469,7 @@ export {
|
|
|
1343
1469
|
isPaddleWebhook,
|
|
1344
1470
|
isShopifyWebhook,
|
|
1345
1471
|
isSlackWebhook,
|
|
1472
|
+
isStandardWebhook,
|
|
1346
1473
|
isStripeWebhook,
|
|
1347
1474
|
isTwilioWebhook,
|
|
1348
1475
|
matchAll,
|