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