@webhooks-cc/sdk 0.3.1 → 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.mjs CHANGED
@@ -145,11 +145,646 @@ async function* parseSSE(stream) {
145
145
  }
146
146
  }
147
147
 
148
+ // src/templates.ts
149
+ var DEFAULT_TEMPLATE_BY_PROVIDER = {
150
+ stripe: "payment_intent.succeeded",
151
+ github: "push",
152
+ shopify: "orders/create",
153
+ twilio: "messaging.inbound"
154
+ };
155
+ var PROVIDER_TEMPLATES = {
156
+ stripe: ["payment_intent.succeeded", "checkout.session.completed", "invoice.paid"],
157
+ github: ["push", "pull_request.opened", "ping"],
158
+ shopify: ["orders/create", "orders/paid", "products/update", "app/uninstalled"],
159
+ twilio: ["messaging.inbound", "messaging.status_callback", "voice.incoming_call"]
160
+ };
161
+ function randomHex(length) {
162
+ const bytes = new Uint8Array(Math.ceil(length / 2));
163
+ globalThis.crypto.getRandomValues(bytes);
164
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
165
+ }
166
+ function randomToken(prefix) {
167
+ return `${prefix}_${randomHex(8)}`;
168
+ }
169
+ function randomDigits(length) {
170
+ const bytes = new Uint8Array(length);
171
+ globalThis.crypto.getRandomValues(bytes);
172
+ return Array.from(bytes, (b) => (b % 10).toString()).join("");
173
+ }
174
+ function randomSid(prefix) {
175
+ return `${prefix}${randomHex(32)}`;
176
+ }
177
+ function randomUuid() {
178
+ if (typeof globalThis.crypto?.randomUUID === "function") {
179
+ return globalThis.crypto.randomUUID();
180
+ }
181
+ return `${randomHex(8)}-${randomHex(4)}-${randomHex(4)}-${randomHex(4)}-${randomHex(12)}`;
182
+ }
183
+ function repositoryPayload() {
184
+ return {
185
+ id: Number(randomDigits(9)),
186
+ name: "demo-repo",
187
+ full_name: "webhooks-cc/demo-repo",
188
+ private: false,
189
+ default_branch: "main",
190
+ html_url: "https://github.com/webhooks-cc/demo-repo"
191
+ };
192
+ }
193
+ function githubSender() {
194
+ return {
195
+ login: "webhooks-cc-bot",
196
+ id: 987654,
197
+ type: "Bot"
198
+ };
199
+ }
200
+ function ensureTemplate(provider, template) {
201
+ const resolved = template ?? DEFAULT_TEMPLATE_BY_PROVIDER[provider];
202
+ const supported = PROVIDER_TEMPLATES[provider];
203
+ if (!supported.some((item) => item === resolved)) {
204
+ throw new Error(
205
+ `Unsupported template "${resolved}" for provider "${provider}". Supported templates: ${supported.join(", ")}`
206
+ );
207
+ }
208
+ return resolved;
209
+ }
210
+ function defaultEvent(provider, template) {
211
+ if (provider === "github" && template === "pull_request.opened") {
212
+ return "pull_request";
213
+ }
214
+ return template;
215
+ }
216
+ function formEncode(params) {
217
+ const form = new URLSearchParams();
218
+ for (const [key, value] of Object.entries(params)) {
219
+ form.append(key, value);
220
+ }
221
+ return form.toString();
222
+ }
223
+ function asStringRecord(value) {
224
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
225
+ return null;
226
+ }
227
+ const out = {};
228
+ for (const [k, v] of Object.entries(value)) {
229
+ if (v == null) {
230
+ out[k] = "";
231
+ continue;
232
+ }
233
+ out[k] = typeof v === "string" ? v : String(v);
234
+ }
235
+ return out;
236
+ }
237
+ function buildTemplatePayload(provider, template, event, now, bodyOverride) {
238
+ const nowSec = Math.floor(now.getTime() / 1e3);
239
+ const nowIso = now.toISOString();
240
+ if (provider === "stripe") {
241
+ const paymentIntentId = randomToken("pi");
242
+ const checkoutSessionId = `cs_test_${randomHex(24)}`;
243
+ const payloadByTemplate = {
244
+ "payment_intent.succeeded": {
245
+ id: randomToken("evt"),
246
+ object: "event",
247
+ api_version: "2025-01-27.acacia",
248
+ created: nowSec,
249
+ data: {
250
+ object: {
251
+ id: paymentIntentId,
252
+ object: "payment_intent",
253
+ amount: 2e3,
254
+ amount_received: 2e3,
255
+ currency: "usd",
256
+ status: "succeeded",
257
+ created: nowSec,
258
+ metadata: {
259
+ order_id: randomToken("order")
260
+ }
261
+ }
262
+ },
263
+ livemode: false,
264
+ pending_webhooks: 1,
265
+ request: {
266
+ id: `req_${randomHex(24)}`,
267
+ idempotency_key: null
268
+ },
269
+ type: event
270
+ },
271
+ "checkout.session.completed": {
272
+ id: randomToken("evt"),
273
+ object: "event",
274
+ api_version: "2025-01-27.acacia",
275
+ created: nowSec,
276
+ data: {
277
+ object: {
278
+ id: checkoutSessionId,
279
+ object: "checkout.session",
280
+ mode: "payment",
281
+ payment_status: "paid",
282
+ amount_total: 2e3,
283
+ amount_subtotal: 2e3,
284
+ currency: "usd",
285
+ customer: `cus_${randomHex(14)}`,
286
+ payment_intent: paymentIntentId,
287
+ status: "complete",
288
+ success_url: "https://example.com/success",
289
+ cancel_url: "https://example.com/cancel",
290
+ created: nowSec
291
+ }
292
+ },
293
+ livemode: false,
294
+ pending_webhooks: 1,
295
+ request: {
296
+ id: `req_${randomHex(24)}`,
297
+ idempotency_key: null
298
+ },
299
+ type: event
300
+ },
301
+ "invoice.paid": {
302
+ id: randomToken("evt"),
303
+ object: "event",
304
+ api_version: "2025-01-27.acacia",
305
+ created: nowSec,
306
+ data: {
307
+ object: {
308
+ id: `in_${randomHex(14)}`,
309
+ object: "invoice",
310
+ account_country: "US",
311
+ account_name: "webhooks.cc demo",
312
+ amount_due: 2e3,
313
+ amount_paid: 2e3,
314
+ amount_remaining: 0,
315
+ billing_reason: "subscription_cycle",
316
+ currency: "usd",
317
+ customer: `cus_${randomHex(14)}`,
318
+ paid: true,
319
+ status: "paid",
320
+ hosted_invoice_url: "https://invoice.stripe.com/demo",
321
+ created: nowSec
322
+ }
323
+ },
324
+ livemode: false,
325
+ pending_webhooks: 1,
326
+ request: {
327
+ id: `req_${randomHex(24)}`,
328
+ idempotency_key: null
329
+ },
330
+ type: event
331
+ }
332
+ };
333
+ const payload = bodyOverride ?? payloadByTemplate[template];
334
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
335
+ return {
336
+ body,
337
+ contentType: "application/json",
338
+ headers: {
339
+ "user-agent": "Stripe/1.0 (+https://stripe.com/docs/webhooks)"
340
+ }
341
+ };
342
+ }
343
+ if (provider === "github") {
344
+ const before = randomHex(40);
345
+ const after = randomHex(40);
346
+ const baseRepo = repositoryPayload();
347
+ const payloadByTemplate = {
348
+ push: {
349
+ ref: "refs/heads/main",
350
+ before,
351
+ after,
352
+ repository: baseRepo,
353
+ pusher: {
354
+ name: "webhooks-cc-bot",
355
+ email: "bot@webhooks.cc"
356
+ },
357
+ sender: githubSender(),
358
+ created: false,
359
+ deleted: false,
360
+ forced: false,
361
+ compare: `https://github.com/${baseRepo.full_name}/compare/${before}...${after}`,
362
+ commits: [
363
+ {
364
+ id: after,
365
+ message: "Update webhook integration tests",
366
+ timestamp: nowIso,
367
+ url: `https://github.com/${baseRepo.full_name}/commit/${after}`,
368
+ author: {
369
+ name: "webhooks-cc-bot",
370
+ email: "bot@webhooks.cc"
371
+ },
372
+ committer: {
373
+ name: "webhooks-cc-bot",
374
+ email: "bot@webhooks.cc"
375
+ },
376
+ added: [],
377
+ removed: [],
378
+ modified: ["src/webhooks.ts"]
379
+ }
380
+ ],
381
+ head_commit: {
382
+ id: after,
383
+ message: "Update webhook integration tests",
384
+ timestamp: nowIso,
385
+ url: `https://github.com/${baseRepo.full_name}/commit/${after}`,
386
+ author: {
387
+ name: "webhooks-cc-bot",
388
+ email: "bot@webhooks.cc"
389
+ },
390
+ committer: {
391
+ name: "webhooks-cc-bot",
392
+ email: "bot@webhooks.cc"
393
+ },
394
+ added: [],
395
+ removed: [],
396
+ modified: ["src/webhooks.ts"]
397
+ }
398
+ },
399
+ "pull_request.opened": {
400
+ action: "opened",
401
+ number: 42,
402
+ pull_request: {
403
+ id: Number(randomDigits(9)),
404
+ number: 42,
405
+ state: "open",
406
+ title: "Add webhook retry logic",
407
+ body: "This PR improves retry handling for inbound webhooks.",
408
+ created_at: nowIso,
409
+ updated_at: nowIso,
410
+ html_url: `https://github.com/${baseRepo.full_name}/pull/42`,
411
+ user: {
412
+ login: "webhooks-cc-bot",
413
+ id: 987654,
414
+ type: "Bot"
415
+ },
416
+ draft: false,
417
+ head: {
418
+ label: "webhooks-cc:feature/webhook-retries",
419
+ ref: "feature/webhook-retries",
420
+ sha: randomHex(40),
421
+ repo: baseRepo
422
+ },
423
+ base: {
424
+ label: "webhooks-cc:main",
425
+ ref: "main",
426
+ sha: randomHex(40),
427
+ repo: baseRepo
428
+ }
429
+ },
430
+ repository: baseRepo,
431
+ sender: githubSender()
432
+ },
433
+ ping: {
434
+ zen: "Keep it logically awesome.",
435
+ hook_id: Number(randomDigits(7)),
436
+ hook: {
437
+ type: "Repository",
438
+ id: Number(randomDigits(7)),
439
+ name: "web",
440
+ active: true,
441
+ events: ["push", "pull_request"],
442
+ config: {
443
+ content_type: "json",
444
+ insecure_ssl: "0",
445
+ url: "https://go.webhooks.cc/w/demo"
446
+ }
447
+ },
448
+ repository: baseRepo,
449
+ sender: githubSender()
450
+ }
451
+ };
452
+ const payload = bodyOverride ?? payloadByTemplate[template];
453
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
454
+ return {
455
+ body,
456
+ contentType: "application/json",
457
+ headers: {
458
+ "user-agent": "GitHub-Hookshot/8f03f6d"
459
+ }
460
+ };
461
+ }
462
+ if (provider === "shopify") {
463
+ const payloadByTemplate = {
464
+ "orders/create": {
465
+ id: Number(randomDigits(10)),
466
+ admin_graphql_api_id: `gid://shopify/Order/${randomDigits(10)}`,
467
+ email: "customer@example.com",
468
+ created_at: nowIso,
469
+ updated_at: nowIso,
470
+ currency: "USD",
471
+ financial_status: "pending",
472
+ fulfillment_status: null,
473
+ total_price: "19.99",
474
+ subtotal_price: "19.99",
475
+ total_tax: "0.00",
476
+ line_items: [
477
+ {
478
+ id: Number(randomDigits(10)),
479
+ admin_graphql_api_id: `gid://shopify/LineItem/${randomDigits(10)}`,
480
+ title: "Demo Item",
481
+ quantity: 1,
482
+ sku: "DEMO-001",
483
+ price: "19.99"
484
+ }
485
+ ]
486
+ },
487
+ "orders/paid": {
488
+ id: Number(randomDigits(10)),
489
+ admin_graphql_api_id: `gid://shopify/Order/${randomDigits(10)}`,
490
+ email: "customer@example.com",
491
+ created_at: nowIso,
492
+ updated_at: nowIso,
493
+ currency: "USD",
494
+ financial_status: "paid",
495
+ fulfillment_status: null,
496
+ total_price: "49.00",
497
+ subtotal_price: "49.00",
498
+ total_tax: "0.00",
499
+ line_items: [
500
+ {
501
+ id: Number(randomDigits(10)),
502
+ admin_graphql_api_id: `gid://shopify/LineItem/${randomDigits(10)}`,
503
+ title: "Webhook Pro Plan",
504
+ quantity: 1,
505
+ sku: "WHK-PRO",
506
+ price: "49.00"
507
+ }
508
+ ]
509
+ },
510
+ "products/update": {
511
+ id: Number(randomDigits(10)),
512
+ admin_graphql_api_id: `gid://shopify/Product/${randomDigits(10)}`,
513
+ title: "Webhook Tester Hoodie",
514
+ body_html: "<strong>Updated product details</strong>",
515
+ vendor: "webhooks.cc",
516
+ product_type: "Apparel",
517
+ handle: "webhook-tester-hoodie",
518
+ status: "active",
519
+ created_at: nowIso,
520
+ updated_at: nowIso,
521
+ variants: [
522
+ {
523
+ id: Number(randomDigits(10)),
524
+ product_id: Number(randomDigits(10)),
525
+ title: "Default Title",
526
+ price: "39.00",
527
+ sku: "WHK-HOODIE",
528
+ position: 1,
529
+ inventory_policy: "deny",
530
+ fulfillment_service: "manual",
531
+ inventory_management: "shopify"
532
+ }
533
+ ]
534
+ },
535
+ "app/uninstalled": {
536
+ id: Number(randomDigits(10)),
537
+ name: "Demo Shop",
538
+ email: "owner@example.com",
539
+ domain: "demo-shop.myshopify.com",
540
+ myshopify_domain: "demo-shop.myshopify.com",
541
+ country_name: "United States",
542
+ currency: "USD",
543
+ plan_name: "basic",
544
+ created_at: nowIso,
545
+ updated_at: nowIso
546
+ }
547
+ };
548
+ const payload = bodyOverride ?? payloadByTemplate[template];
549
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
550
+ return {
551
+ body,
552
+ contentType: "application/json",
553
+ headers: {
554
+ "x-shopify-shop-domain": "demo-shop.myshopify.com",
555
+ "x-shopify-api-version": "2025-10",
556
+ "x-shopify-webhook-id": randomUuid(),
557
+ "x-shopify-event-id": randomUuid(),
558
+ "x-shopify-triggered-at": nowIso
559
+ }
560
+ };
561
+ }
562
+ if (provider !== "twilio") {
563
+ throw new Error(`Unsupported provider: ${provider}`);
564
+ }
565
+ const defaultTwilioParamsByTemplate = {
566
+ "messaging.inbound": {
567
+ AccountSid: randomSid("AC"),
568
+ ApiVersion: "2010-04-01",
569
+ MessageSid: randomSid("SM"),
570
+ SmsSid: randomSid("SM"),
571
+ SmsMessageSid: randomSid("SM"),
572
+ From: "+14155550123",
573
+ To: "+14155559876",
574
+ Body: "Hello from webhooks.cc",
575
+ NumMedia: "0",
576
+ NumSegments: "1",
577
+ MessageStatus: "received",
578
+ SmsStatus: "received",
579
+ FromCity: "SAN FRANCISCO",
580
+ FromState: "CA",
581
+ FromCountry: "US",
582
+ FromZip: "94105",
583
+ ToCity: "",
584
+ ToState: "",
585
+ ToCountry: "US",
586
+ ToZip: ""
587
+ },
588
+ "messaging.status_callback": {
589
+ AccountSid: randomSid("AC"),
590
+ ApiVersion: "2010-04-01",
591
+ MessageSid: randomSid("SM"),
592
+ SmsSid: randomSid("SM"),
593
+ MessageStatus: "delivered",
594
+ SmsStatus: "delivered",
595
+ To: "+14155559876",
596
+ From: "+14155550123",
597
+ ErrorCode: ""
598
+ },
599
+ "voice.incoming_call": {
600
+ AccountSid: randomSid("AC"),
601
+ ApiVersion: "2010-04-01",
602
+ CallSid: randomSid("CA"),
603
+ CallStatus: "ringing",
604
+ Direction: "inbound",
605
+ From: "+14155550123",
606
+ To: "+14155559876",
607
+ CallerCity: "SAN FRANCISCO",
608
+ CallerState: "CA",
609
+ CallerCountry: "US",
610
+ CallerZip: "94105",
611
+ CalledCity: "",
612
+ CalledState: "",
613
+ CalledCountry: "US",
614
+ CalledZip: ""
615
+ }
616
+ };
617
+ let twilioParams;
618
+ if (bodyOverride !== void 0) {
619
+ if (typeof bodyOverride === "string") {
620
+ const entries = Array.from(new URLSearchParams(bodyOverride).entries());
621
+ return {
622
+ body: bodyOverride,
623
+ contentType: "application/x-www-form-urlencoded",
624
+ headers: {
625
+ "user-agent": "TwilioProxy/1.1"
626
+ },
627
+ twilioParams: entries
628
+ };
629
+ }
630
+ const overrideParams = asStringRecord(bodyOverride);
631
+ if (!overrideParams) {
632
+ throw new Error("Twilio template body override must be a string or an object");
633
+ }
634
+ twilioParams = overrideParams;
635
+ } else {
636
+ twilioParams = defaultTwilioParamsByTemplate[template];
637
+ }
638
+ return {
639
+ body: formEncode(twilioParams),
640
+ contentType: "application/x-www-form-urlencoded",
641
+ headers: {
642
+ "user-agent": "TwilioProxy/1.1"
643
+ },
644
+ twilioParams: Object.entries(twilioParams)
645
+ };
646
+ }
647
+ async function hmacSign(algorithm, secret, payload) {
648
+ if (!globalThis.crypto?.subtle) {
649
+ throw new Error("crypto.subtle is required for template signature generation");
650
+ }
651
+ const key = await globalThis.crypto.subtle.importKey(
652
+ "raw",
653
+ new TextEncoder().encode(secret),
654
+ { name: "HMAC", hash: algorithm },
655
+ false,
656
+ ["sign"]
657
+ );
658
+ const signature = await globalThis.crypto.subtle.sign(
659
+ "HMAC",
660
+ key,
661
+ new TextEncoder().encode(payload)
662
+ );
663
+ return new Uint8Array(signature);
664
+ }
665
+ function toHex(bytes) {
666
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
667
+ }
668
+ function toBase64(bytes) {
669
+ if (typeof btoa !== "function") {
670
+ return Buffer.from(bytes).toString("base64");
671
+ }
672
+ let binary = "";
673
+ for (const byte of bytes) binary += String.fromCharCode(byte);
674
+ return btoa(binary);
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
+ }
703
+ function buildTwilioSignaturePayload(endpointUrl, params) {
704
+ const sortedParams = params.map(([key, value], index) => ({ key, value, index })).sort(
705
+ (a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : a.value < b.value ? -1 : a.value > b.value ? 1 : 0
706
+ );
707
+ let payload = endpointUrl;
708
+ for (const { key, value } of sortedParams) {
709
+ payload += `${key}${value}`;
710
+ }
711
+ return payload;
712
+ }
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;
740
+ const method = (options.method ?? "POST").toUpperCase();
741
+ const template = ensureTemplate(provider, options.template);
742
+ const event = options.event ?? defaultEvent(provider, template);
743
+ const now = /* @__PURE__ */ new Date();
744
+ const built = buildTemplatePayload(provider, template, event, now, options.body);
745
+ const headers = {
746
+ "content-type": built.contentType,
747
+ "x-webhooks-cc-template-provider": provider,
748
+ "x-webhooks-cc-template-template": template,
749
+ "x-webhooks-cc-template-event": event,
750
+ ...built.headers
751
+ };
752
+ if (provider === "stripe") {
753
+ const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
754
+ const signature = await hmacSign("SHA-256", options.secret, `${timestamp}.${built.body}`);
755
+ headers["stripe-signature"] = `t=${timestamp},v1=${toHex(signature)}`;
756
+ }
757
+ if (provider === "github") {
758
+ headers["x-github-event"] = event;
759
+ headers["x-github-delivery"] = randomUuid();
760
+ const signature = await hmacSign("SHA-256", options.secret, built.body);
761
+ headers["x-hub-signature-256"] = `sha256=${toHex(signature)}`;
762
+ }
763
+ if (provider === "shopify") {
764
+ headers["x-shopify-topic"] = event;
765
+ const signature = await hmacSign("SHA-256", options.secret, built.body);
766
+ headers["x-shopify-hmac-sha256"] = toBase64(signature);
767
+ }
768
+ if (provider === "twilio") {
769
+ const signaturePayload = built.twilioParams ? buildTwilioSignaturePayload(endpointUrl, built.twilioParams) : `${endpointUrl}${built.body}`;
770
+ const signature = await hmacSign("SHA-1", options.secret, signaturePayload);
771
+ headers["x-twilio-signature"] = toBase64(signature);
772
+ }
773
+ return {
774
+ method,
775
+ headers: {
776
+ ...headers,
777
+ ...options.headers ?? {}
778
+ },
779
+ body: built.body
780
+ };
781
+ }
782
+
148
783
  // src/client.ts
149
784
  var DEFAULT_BASE_URL = "https://webhooks.cc";
150
785
  var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
151
786
  var DEFAULT_TIMEOUT = 3e4;
152
- var SDK_VERSION = "0.3.0";
787
+ var SDK_VERSION = "0.5.0";
153
788
  var MIN_POLL_INTERVAL = 10;
154
789
  var MAX_POLL_INTERVAL = 6e4;
155
790
  var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
@@ -164,6 +799,19 @@ var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
164
799
  "upgrade"
165
800
  ]);
166
801
  var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "proxy-authorization", "set-cookie"]);
802
+ var PROXY_HEADERS = /* @__PURE__ */ new Set([
803
+ "cdn-loop",
804
+ "cf-connecting-ip",
805
+ "cf-ipcountry",
806
+ "cf-ray",
807
+ "cf-visitor",
808
+ "via",
809
+ "x-forwarded-for",
810
+ "x-forwarded-host",
811
+ "x-forwarded-proto",
812
+ "x-real-ip",
813
+ "true-client-ip"
814
+ ]);
167
815
  var ApiError = WebhooksCCError;
168
816
  function mapStatusToError(status, message, response) {
169
817
  const isGeneric = message.length < 30;
@@ -248,7 +896,75 @@ var WebhooksCC = class {
248
896
  body: body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : void 0,
249
897
  signal: AbortSignal.timeout(this.timeout)
250
898
  });
899
+ },
900
+ sendTemplate: async (slug, options) => {
901
+ validatePathSegment(slug, "slug");
902
+ if (!options.secret || typeof options.secret !== "string") {
903
+ throw new Error("sendTemplate requires a non-empty secret");
904
+ }
905
+ const endpointUrl = `${this.webhookUrl}/w/${slug}`;
906
+ const sendOptions = await buildTemplateSendOptions(endpointUrl, options);
907
+ return this.endpoints.send(slug, sendOptions);
908
+ }
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";
251
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
+ });
252
968
  };
253
969
  this.requests = {
254
970
  list: async (endpointSlug, options = {}) => {
@@ -342,7 +1058,7 @@ var WebhooksCC = class {
342
1058
  const headers = {};
343
1059
  for (const [key, val] of Object.entries(captured.headers)) {
344
1060
  const lower = key.toLowerCase();
345
- if (!HOP_BY_HOP_HEADERS.has(lower) && !SENSITIVE_HEADERS.has(lower)) {
1061
+ if (!HOP_BY_HOP_HEADERS.has(lower) && !SENSITIVE_HEADERS.has(lower) && !PROXY_HEADERS.has(lower)) {
346
1062
  headers[key] = val;
347
1063
  }
348
1064
  }
@@ -593,6 +1309,26 @@ var WebhooksCC = class {
593
1309
  send: {
594
1310
  description: "Send a test webhook to endpoint",
595
1311
  params: { slug: "string", method: "string?", headers: "object?", body: "unknown?" }
1312
+ },
1313
+ sendTemplate: {
1314
+ description: "Send a provider template webhook with signed headers",
1315
+ params: {
1316
+ slug: "string",
1317
+ provider: '"stripe"|"github"|"shopify"|"twilio"|"standard-webhooks"',
1318
+ template: "string?",
1319
+ secret: "string",
1320
+ event: "string?"
1321
+ }
1322
+ }
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>?"
596
1332
  }
597
1333
  },
598
1334
  requests: {
@@ -671,6 +1407,10 @@ function isPaddleWebhook(request) {
671
1407
  function isLinearWebhook(request) {
672
1408
  return Object.keys(request.headers).some((k) => k.toLowerCase() === "linear-signature");
673
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
+ }
674
1414
 
675
1415
  // src/matchers.ts
676
1416
  function matchMethod(method) {
@@ -729,6 +1469,7 @@ export {
729
1469
  isPaddleWebhook,
730
1470
  isShopifyWebhook,
731
1471
  isSlackWebhook,
1472
+ isStandardWebhook,
732
1473
  isStripeWebhook,
733
1474
  isTwilioWebhook,
734
1475
  matchAll,