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