@webhooks-cc/sdk 0.3.1 → 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.d.mts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +615 -1
- package/dist/index.mjs +615 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -73,6 +73,28 @@ 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";
|
|
77
|
+
/**
|
|
78
|
+
* Options for sending a provider template webhook with signed headers.
|
|
79
|
+
*/
|
|
80
|
+
interface SendTemplateOptions {
|
|
81
|
+
/** Provider template to use */
|
|
82
|
+
provider: TemplateProvider;
|
|
83
|
+
/** Provider-specific template preset (uses provider default if omitted) */
|
|
84
|
+
template?: string;
|
|
85
|
+
/** Shared secret used for provider signature generation */
|
|
86
|
+
secret: string;
|
|
87
|
+
/** Provider event/topic name (provider default used if omitted) */
|
|
88
|
+
event?: string;
|
|
89
|
+
/** HTTP method override (default: "POST") */
|
|
90
|
+
method?: string;
|
|
91
|
+
/** Additional headers merged after template headers */
|
|
92
|
+
headers?: Record<string, string>;
|
|
93
|
+
/** Body override; if omitted a provider-specific template body is generated */
|
|
94
|
+
body?: unknown;
|
|
95
|
+
/** Unix timestamp (seconds) override for deterministic signatures in tests */
|
|
96
|
+
timestamp?: number;
|
|
97
|
+
}
|
|
76
98
|
/**
|
|
77
99
|
* Options for listing captured requests.
|
|
78
100
|
*/
|
|
@@ -228,6 +250,7 @@ declare class WebhooksCC {
|
|
|
228
250
|
update: (slug: string, options: UpdateEndpointOptions) => Promise<Endpoint>;
|
|
229
251
|
delete: (slug: string) => Promise<void>;
|
|
230
252
|
send: (slug: string, options?: SendOptions) => Promise<Response>;
|
|
253
|
+
sendTemplate: (slug: string, options: SendTemplateOptions) => Promise<Response>;
|
|
231
254
|
};
|
|
232
255
|
requests: {
|
|
233
256
|
list: (endpointSlug: string, options?: ListRequestsOptions) => Promise<Request[]>;
|
|
@@ -353,4 +376,4 @@ interface SSEFrame {
|
|
|
353
376
|
*/
|
|
354
377
|
declare function parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<SSEFrame, void, undefined>;
|
|
355
378
|
|
|
356
|
-
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 SubscribeOptions, TimeoutError, UnauthorizedError, type UpdateEndpointOptions, type WaitForOptions, WebhooksCC, WebhooksCCError, isGitHubWebhook, isLinearWebhook, isPaddleWebhook, isShopifyWebhook, isSlackWebhook, isStripeWebhook, isTwilioWebhook, matchAll, matchAny, matchBodyPath, matchHeader, matchJsonField, matchMethod, parseDuration, parseJsonBody, parseSSE };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -73,6 +73,28 @@ 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";
|
|
77
|
+
/**
|
|
78
|
+
* Options for sending a provider template webhook with signed headers.
|
|
79
|
+
*/
|
|
80
|
+
interface SendTemplateOptions {
|
|
81
|
+
/** Provider template to use */
|
|
82
|
+
provider: TemplateProvider;
|
|
83
|
+
/** Provider-specific template preset (uses provider default if omitted) */
|
|
84
|
+
template?: string;
|
|
85
|
+
/** Shared secret used for provider signature generation */
|
|
86
|
+
secret: string;
|
|
87
|
+
/** Provider event/topic name (provider default used if omitted) */
|
|
88
|
+
event?: string;
|
|
89
|
+
/** HTTP method override (default: "POST") */
|
|
90
|
+
method?: string;
|
|
91
|
+
/** Additional headers merged after template headers */
|
|
92
|
+
headers?: Record<string, string>;
|
|
93
|
+
/** Body override; if omitted a provider-specific template body is generated */
|
|
94
|
+
body?: unknown;
|
|
95
|
+
/** Unix timestamp (seconds) override for deterministic signatures in tests */
|
|
96
|
+
timestamp?: number;
|
|
97
|
+
}
|
|
76
98
|
/**
|
|
77
99
|
* Options for listing captured requests.
|
|
78
100
|
*/
|
|
@@ -228,6 +250,7 @@ declare class WebhooksCC {
|
|
|
228
250
|
update: (slug: string, options: UpdateEndpointOptions) => Promise<Endpoint>;
|
|
229
251
|
delete: (slug: string) => Promise<void>;
|
|
230
252
|
send: (slug: string, options?: SendOptions) => Promise<Response>;
|
|
253
|
+
sendTemplate: (slug: string, options: SendTemplateOptions) => Promise<Response>;
|
|
231
254
|
};
|
|
232
255
|
requests: {
|
|
233
256
|
list: (endpointSlug: string, options?: ListRequestsOptions) => Promise<Request[]>;
|
|
@@ -353,4 +376,4 @@ interface SSEFrame {
|
|
|
353
376
|
*/
|
|
354
377
|
declare function parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<SSEFrame, void, undefined>;
|
|
355
378
|
|
|
356
|
-
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 SubscribeOptions, TimeoutError, UnauthorizedError, type UpdateEndpointOptions, type WaitForOptions, WebhooksCC, WebhooksCCError, isGitHubWebhook, isLinearWebhook, isPaddleWebhook, isShopifyWebhook, isSlackWebhook, isStripeWebhook, isTwilioWebhook, matchAll, matchAny, matchBodyPath, matchHeader, matchJsonField, matchMethod, parseDuration, parseJsonBody, parseSSE };
|
|
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 };
|
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
|
}
|
|
@@ -641,6 +1245,16 @@ var WebhooksCC = class {
|
|
|
641
1245
|
send: {
|
|
642
1246
|
description: "Send a test webhook to endpoint",
|
|
643
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
|
+
}
|
|
644
1258
|
}
|
|
645
1259
|
},
|
|
646
1260
|
requests: {
|
package/dist/index.mjs
CHANGED
|
@@ -145,6 +145,588 @@ 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 buildTwilioSignaturePayload(endpointUrl, params) {
|
|
677
|
+
const sortedParams = params.map(([key, value], index) => ({ key, value, index })).sort(
|
|
678
|
+
(a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : a.value < b.value ? -1 : a.value > b.value ? 1 : 0
|
|
679
|
+
);
|
|
680
|
+
let payload = endpointUrl;
|
|
681
|
+
for (const { key, value } of sortedParams) {
|
|
682
|
+
payload += `${key}${value}`;
|
|
683
|
+
}
|
|
684
|
+
return payload;
|
|
685
|
+
}
|
|
686
|
+
async function buildTemplateSendOptions(endpointUrl, options) {
|
|
687
|
+
const method = (options.method ?? "POST").toUpperCase();
|
|
688
|
+
const template = ensureTemplate(options.provider, options.template);
|
|
689
|
+
const event = options.event ?? defaultEvent(options.provider, template);
|
|
690
|
+
const now = /* @__PURE__ */ new Date();
|
|
691
|
+
const built = buildTemplatePayload(options.provider, template, event, now, options.body);
|
|
692
|
+
const headers = {
|
|
693
|
+
"content-type": built.contentType,
|
|
694
|
+
"x-webhooks-cc-template-provider": options.provider,
|
|
695
|
+
"x-webhooks-cc-template-template": template,
|
|
696
|
+
"x-webhooks-cc-template-event": event,
|
|
697
|
+
...built.headers
|
|
698
|
+
};
|
|
699
|
+
if (options.provider === "stripe") {
|
|
700
|
+
const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
|
|
701
|
+
const signature = await hmacSign("SHA-256", options.secret, `${timestamp}.${built.body}`);
|
|
702
|
+
headers["stripe-signature"] = `t=${timestamp},v1=${toHex(signature)}`;
|
|
703
|
+
}
|
|
704
|
+
if (options.provider === "github") {
|
|
705
|
+
headers["x-github-event"] = event;
|
|
706
|
+
headers["x-github-delivery"] = randomUuid();
|
|
707
|
+
const signature = await hmacSign("SHA-256", options.secret, built.body);
|
|
708
|
+
headers["x-hub-signature-256"] = `sha256=${toHex(signature)}`;
|
|
709
|
+
}
|
|
710
|
+
if (options.provider === "shopify") {
|
|
711
|
+
headers["x-shopify-topic"] = event;
|
|
712
|
+
const signature = await hmacSign("SHA-256", options.secret, built.body);
|
|
713
|
+
headers["x-shopify-hmac-sha256"] = toBase64(signature);
|
|
714
|
+
}
|
|
715
|
+
if (options.provider === "twilio") {
|
|
716
|
+
const signaturePayload = built.twilioParams ? buildTwilioSignaturePayload(endpointUrl, built.twilioParams) : `${endpointUrl}${built.body}`;
|
|
717
|
+
const signature = await hmacSign("SHA-1", options.secret, signaturePayload);
|
|
718
|
+
headers["x-twilio-signature"] = toBase64(signature);
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
method,
|
|
722
|
+
headers: {
|
|
723
|
+
...headers,
|
|
724
|
+
...options.headers ?? {}
|
|
725
|
+
},
|
|
726
|
+
body: built.body
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
148
730
|
// src/client.ts
|
|
149
731
|
var DEFAULT_BASE_URL = "https://webhooks.cc";
|
|
150
732
|
var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
|
|
@@ -164,6 +746,19 @@ var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
|
164
746
|
"upgrade"
|
|
165
747
|
]);
|
|
166
748
|
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "proxy-authorization", "set-cookie"]);
|
|
749
|
+
var PROXY_HEADERS = /* @__PURE__ */ new Set([
|
|
750
|
+
"cdn-loop",
|
|
751
|
+
"cf-connecting-ip",
|
|
752
|
+
"cf-ipcountry",
|
|
753
|
+
"cf-ray",
|
|
754
|
+
"cf-visitor",
|
|
755
|
+
"via",
|
|
756
|
+
"x-forwarded-for",
|
|
757
|
+
"x-forwarded-host",
|
|
758
|
+
"x-forwarded-proto",
|
|
759
|
+
"x-real-ip",
|
|
760
|
+
"true-client-ip"
|
|
761
|
+
]);
|
|
167
762
|
var ApiError = WebhooksCCError;
|
|
168
763
|
function mapStatusToError(status, message, response) {
|
|
169
764
|
const isGeneric = message.length < 30;
|
|
@@ -248,6 +843,15 @@ var WebhooksCC = class {
|
|
|
248
843
|
body: body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : void 0,
|
|
249
844
|
signal: AbortSignal.timeout(this.timeout)
|
|
250
845
|
});
|
|
846
|
+
},
|
|
847
|
+
sendTemplate: async (slug, options) => {
|
|
848
|
+
validatePathSegment(slug, "slug");
|
|
849
|
+
if (!options.secret || typeof options.secret !== "string") {
|
|
850
|
+
throw new Error("sendTemplate requires a non-empty secret");
|
|
851
|
+
}
|
|
852
|
+
const endpointUrl = `${this.webhookUrl}/w/${slug}`;
|
|
853
|
+
const sendOptions = await buildTemplateSendOptions(endpointUrl, options);
|
|
854
|
+
return this.endpoints.send(slug, sendOptions);
|
|
251
855
|
}
|
|
252
856
|
};
|
|
253
857
|
this.requests = {
|
|
@@ -342,7 +946,7 @@ var WebhooksCC = class {
|
|
|
342
946
|
const headers = {};
|
|
343
947
|
for (const [key, val] of Object.entries(captured.headers)) {
|
|
344
948
|
const lower = key.toLowerCase();
|
|
345
|
-
if (!HOP_BY_HOP_HEADERS.has(lower) && !SENSITIVE_HEADERS.has(lower)) {
|
|
949
|
+
if (!HOP_BY_HOP_HEADERS.has(lower) && !SENSITIVE_HEADERS.has(lower) && !PROXY_HEADERS.has(lower)) {
|
|
346
950
|
headers[key] = val;
|
|
347
951
|
}
|
|
348
952
|
}
|
|
@@ -593,6 +1197,16 @@ var WebhooksCC = class {
|
|
|
593
1197
|
send: {
|
|
594
1198
|
description: "Send a test webhook to endpoint",
|
|
595
1199
|
params: { slug: "string", method: "string?", headers: "object?", body: "unknown?" }
|
|
1200
|
+
},
|
|
1201
|
+
sendTemplate: {
|
|
1202
|
+
description: "Send a provider template webhook with signed headers",
|
|
1203
|
+
params: {
|
|
1204
|
+
slug: "string",
|
|
1205
|
+
provider: '"stripe"|"github"|"shopify"|"twilio"',
|
|
1206
|
+
template: "string?",
|
|
1207
|
+
secret: "string",
|
|
1208
|
+
event: "string?"
|
|
1209
|
+
}
|
|
596
1210
|
}
|
|
597
1211
|
},
|
|
598
1212
|
requests: {
|