@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 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(options.provider, options.template);
737
- const event = options.event ?? defaultEvent(options.provider, template);
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(options.provider, template, event, now, options.body);
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": options.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 (options.provider === "stripe") {
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 (options.provider === "github") {
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 (options.provider === "shopify") {
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 (options.provider === "twilio") {
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.3.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(options.provider, options.template);
689
- const event = options.event ?? defaultEvent(options.provider, template);
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(options.provider, template, event, now, options.body);
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": options.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 (options.provider === "stripe") {
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 (options.provider === "github") {
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 (options.provider === "shopify") {
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 (options.provider === "twilio") {
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.3.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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webhooks-cc/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "TypeScript SDK for webhooks.cc — create endpoints, capture requests, assert in tests",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",