@webhooks-cc/sdk 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,15 +1,194 @@
1
+ // src/errors.ts
2
+ var WebhooksCCError = class extends Error {
3
+ constructor(statusCode, message) {
4
+ super(message);
5
+ this.statusCode = statusCode;
6
+ this.name = "WebhooksCCError";
7
+ Object.setPrototypeOf(this, new.target.prototype);
8
+ }
9
+ };
10
+ var UnauthorizedError = class extends WebhooksCCError {
11
+ constructor(message = "Invalid or missing API key") {
12
+ super(401, message);
13
+ this.name = "UnauthorizedError";
14
+ }
15
+ };
16
+ var NotFoundError = class extends WebhooksCCError {
17
+ constructor(message = "Resource not found") {
18
+ super(404, message);
19
+ this.name = "NotFoundError";
20
+ }
21
+ };
22
+ var TimeoutError = class extends WebhooksCCError {
23
+ constructor(timeoutMs) {
24
+ super(0, `Request timed out after ${timeoutMs}ms`);
25
+ this.name = "TimeoutError";
26
+ }
27
+ };
28
+ var RateLimitError = class extends WebhooksCCError {
29
+ constructor(retryAfter) {
30
+ const message = retryAfter ? `Rate limited, retry after ${retryAfter}s` : "Rate limited";
31
+ super(429, message);
32
+ this.name = "RateLimitError";
33
+ this.retryAfter = retryAfter;
34
+ }
35
+ };
36
+
37
+ // src/utils.ts
38
+ var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)$/;
39
+ function parseDuration(input) {
40
+ if (typeof input === "number") {
41
+ if (!Number.isFinite(input) || input < 0) {
42
+ throw new Error(`Invalid duration: must be a finite non-negative number, got ${input}`);
43
+ }
44
+ return input;
45
+ }
46
+ const trimmed = input.trim();
47
+ const asNumber = Number(trimmed);
48
+ if (!isNaN(asNumber) && trimmed.length > 0) {
49
+ if (!Number.isFinite(asNumber) || asNumber < 0) {
50
+ throw new Error(`Invalid duration: must be a finite non-negative number, got "${input}"`);
51
+ }
52
+ return asNumber;
53
+ }
54
+ const match = DURATION_REGEX.exec(trimmed);
55
+ if (!match) {
56
+ throw new Error(`Invalid duration: "${input}"`);
57
+ }
58
+ const value = parseFloat(match[1]);
59
+ switch (match[2]) {
60
+ case "ms":
61
+ return value;
62
+ case "s":
63
+ return value * 1e3;
64
+ case "m":
65
+ return value * 6e4;
66
+ case "h":
67
+ return value * 36e5;
68
+ default:
69
+ throw new Error(`Invalid duration: "${input}"`);
70
+ }
71
+ }
72
+
73
+ // src/sse.ts
74
+ async function* parseSSE(stream) {
75
+ const reader = stream.getReader();
76
+ const decoder = new TextDecoder();
77
+ let buffer = "";
78
+ let currentEvent = "message";
79
+ let dataLines = [];
80
+ try {
81
+ while (true) {
82
+ const { done, value } = await reader.read();
83
+ if (done) break;
84
+ buffer += decoder.decode(value, { stream: true });
85
+ const lines = buffer.split("\n");
86
+ buffer = lines.pop();
87
+ for (const line of lines) {
88
+ if (line === "" || line === "\r") {
89
+ if (dataLines.length > 0) {
90
+ yield { event: currentEvent, data: dataLines.join("\n") };
91
+ dataLines = [];
92
+ currentEvent = "message";
93
+ }
94
+ continue;
95
+ }
96
+ const trimmedLine = line.endsWith("\r") ? line.slice(0, -1) : line;
97
+ if (trimmedLine.startsWith(":")) {
98
+ const rawComment = trimmedLine.slice(1);
99
+ yield {
100
+ event: "comment",
101
+ data: rawComment.startsWith(" ") ? rawComment.slice(1) : rawComment
102
+ };
103
+ continue;
104
+ }
105
+ const colonIdx = trimmedLine.indexOf(":");
106
+ if (colonIdx === -1) continue;
107
+ const field = trimmedLine.slice(0, colonIdx);
108
+ const rawVal = trimmedLine.slice(colonIdx + 1);
109
+ const val = rawVal.startsWith(" ") ? rawVal.slice(1) : rawVal;
110
+ switch (field) {
111
+ case "event":
112
+ currentEvent = val;
113
+ break;
114
+ case "data":
115
+ dataLines.push(val);
116
+ break;
117
+ }
118
+ }
119
+ }
120
+ if (buffer.length > 0) {
121
+ const trimmedLine = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
122
+ if (trimmedLine.startsWith(":")) {
123
+ const rawComment = trimmedLine.slice(1);
124
+ yield {
125
+ event: "comment",
126
+ data: rawComment.startsWith(" ") ? rawComment.slice(1) : rawComment
127
+ };
128
+ } else {
129
+ const colonIdx = trimmedLine.indexOf(":");
130
+ if (colonIdx !== -1) {
131
+ const field = trimmedLine.slice(0, colonIdx);
132
+ const rawVal = trimmedLine.slice(colonIdx + 1);
133
+ const val = rawVal.startsWith(" ") ? rawVal.slice(1) : rawVal;
134
+ if (field === "event") currentEvent = val;
135
+ else if (field === "data") dataLines.push(val);
136
+ }
137
+ }
138
+ }
139
+ if (dataLines.length > 0) {
140
+ yield { event: currentEvent, data: dataLines.join("\n") };
141
+ }
142
+ } finally {
143
+ await reader.cancel();
144
+ reader.releaseLock();
145
+ }
146
+ }
147
+
1
148
  // src/client.ts
2
149
  var DEFAULT_BASE_URL = "https://webhooks.cc";
150
+ var DEFAULT_WEBHOOK_URL = "https://go.webhooks.cc";
3
151
  var DEFAULT_TIMEOUT = 3e4;
152
+ var SDK_VERSION = "0.3.0";
4
153
  var MIN_POLL_INTERVAL = 10;
5
154
  var MAX_POLL_INTERVAL = 6e4;
6
- var ApiError = class extends Error {
7
- constructor(statusCode, message) {
8
- super(`API error (${statusCode}): ${message}`);
9
- this.statusCode = statusCode;
10
- this.name = "ApiError";
155
+ var ALLOWED_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
156
+ var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
157
+ "host",
158
+ "connection",
159
+ "content-length",
160
+ "transfer-encoding",
161
+ "keep-alive",
162
+ "te",
163
+ "trailer",
164
+ "upgrade"
165
+ ]);
166
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "proxy-authorization", "set-cookie"]);
167
+ var ApiError = WebhooksCCError;
168
+ function mapStatusToError(status, message, response) {
169
+ const isGeneric = message.length < 30;
170
+ switch (status) {
171
+ case 401: {
172
+ const hint = isGeneric ? `${message} \u2014 Get an API key at https://webhooks.cc/account` : message;
173
+ return new UnauthorizedError(hint);
174
+ }
175
+ case 404: {
176
+ const hint = isGeneric ? `${message} \u2014 Use client.endpoints.list() to see available endpoints.` : message;
177
+ return new NotFoundError(hint);
178
+ }
179
+ case 429: {
180
+ const retryAfterHeader = response.headers.get("retry-after");
181
+ let retryAfter;
182
+ if (retryAfterHeader) {
183
+ const parsed = parseInt(retryAfterHeader, 10);
184
+ retryAfter = Number.isNaN(parsed) ? void 0 : parsed;
185
+ }
186
+ return new RateLimitError(retryAfter);
187
+ }
188
+ default:
189
+ return new WebhooksCCError(status, message);
11
190
  }
12
- };
191
+ }
13
192
  var SAFE_PATH_SEGMENT_REGEX = /^[a-zA-Z0-9_-]+$/;
14
193
  function validatePathSegment(segment, name) {
15
194
  if (!SAFE_PATH_SEGMENT_REGEX.test(segment)) {
@@ -31,9 +210,44 @@ var WebhooksCC = class {
31
210
  validatePathSegment(slug, "slug");
32
211
  return this.request("GET", `/endpoints/${slug}`);
33
212
  },
213
+ update: async (slug, options) => {
214
+ validatePathSegment(slug, "slug");
215
+ if (options.mockResponse && options.mockResponse !== null) {
216
+ const { status } = options.mockResponse;
217
+ if (!Number.isInteger(status) || status < 100 || status > 599) {
218
+ throw new Error(`Invalid mock response status: ${status}. Must be an integer 100-599.`);
219
+ }
220
+ }
221
+ return this.request("PATCH", `/endpoints/${slug}`, options);
222
+ },
34
223
  delete: async (slug) => {
35
224
  validatePathSegment(slug, "slug");
36
225
  await this.request("DELETE", `/endpoints/${slug}`);
226
+ },
227
+ send: async (slug, options = {}) => {
228
+ validatePathSegment(slug, "slug");
229
+ const rawMethod = (options.method ?? "POST").toUpperCase();
230
+ if (!ALLOWED_METHODS.has(rawMethod)) {
231
+ throw new Error(
232
+ `Invalid HTTP method: "${options.method}". Must be one of: ${[...ALLOWED_METHODS].join(", ")}`
233
+ );
234
+ }
235
+ const { headers = {}, body } = options;
236
+ const method = rawMethod;
237
+ const url = `${this.webhookUrl}/w/${slug}`;
238
+ const fetchHeaders = { ...headers };
239
+ const hasContentType = Object.keys(fetchHeaders).some(
240
+ (k) => k.toLowerCase() === "content-type"
241
+ );
242
+ if (body !== void 0 && !hasContentType) {
243
+ fetchHeaders["Content-Type"] = "application/json";
244
+ }
245
+ return fetch(url, {
246
+ method,
247
+ headers: fetchHeaders,
248
+ body: body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : void 0,
249
+ signal: AbortSignal.timeout(this.timeout)
250
+ });
37
251
  }
38
252
  };
39
253
  this.requests = {
@@ -66,13 +280,15 @@ var WebhooksCC = class {
66
280
  */
67
281
  waitFor: async (endpointSlug, options = {}) => {
68
282
  validatePathSegment(endpointSlug, "endpointSlug");
69
- const { timeout = 3e4, pollInterval = 500, match } = options;
283
+ const timeout = parseDuration(options.timeout ?? 3e4);
284
+ const rawPollInterval = parseDuration(options.pollInterval ?? 500);
285
+ const { match } = options;
70
286
  const safePollInterval = Math.max(
71
287
  MIN_POLL_INTERVAL,
72
- Math.min(MAX_POLL_INTERVAL, pollInterval)
288
+ Math.min(MAX_POLL_INTERVAL, rawPollInterval)
73
289
  );
74
290
  const start = Date.now();
75
- let lastChecked = 0;
291
+ let lastChecked = start - 5 * 60 * 1e3;
76
292
  const MAX_ITERATIONS = 1e4;
77
293
  let iterations = 0;
78
294
  while (Date.now() - start < timeout && iterations < MAX_ITERATIONS) {
@@ -89,38 +305,215 @@ var WebhooksCC = class {
89
305
  return matched;
90
306
  }
91
307
  } catch (error) {
92
- if (error instanceof ApiError) {
93
- if (error.statusCode === 401) {
94
- throw new Error("Authentication failed: invalid or expired API key");
95
- }
96
- if (error.statusCode === 403) {
97
- throw new Error("Access denied: insufficient permissions for this endpoint");
308
+ if (error instanceof WebhooksCCError) {
309
+ if (error instanceof UnauthorizedError) {
310
+ throw error;
98
311
  }
99
- if (error.statusCode === 404) {
100
- throw new Error(`Endpoint "${endpointSlug}" not found`);
312
+ if (error instanceof NotFoundError) {
313
+ throw error;
101
314
  }
102
- if (error.statusCode < 500) {
315
+ if (error.statusCode < 500 && !(error instanceof RateLimitError)) {
103
316
  throw error;
104
317
  }
105
318
  }
106
319
  }
107
320
  await sleep(safePollInterval);
108
321
  }
109
- if (iterations >= MAX_ITERATIONS) {
110
- throw new Error(`Max iterations (${MAX_ITERATIONS}) reached while waiting for request`);
322
+ throw new TimeoutError(timeout);
323
+ },
324
+ /**
325
+ * Replay a captured request to a target URL.
326
+ *
327
+ * Fetches the original request by ID and re-sends it to the specified URL
328
+ * with the original method, headers, and body. Hop-by-hop headers are stripped.
329
+ */
330
+ replay: async (requestId, targetUrl) => {
331
+ validatePathSegment(requestId, "requestId");
332
+ let parsed;
333
+ try {
334
+ parsed = new URL(targetUrl);
335
+ } catch {
336
+ throw new Error(`Invalid targetUrl: "${targetUrl}" is not a valid URL`);
337
+ }
338
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
339
+ throw new Error(`Invalid targetUrl: only http and https protocols are supported`);
111
340
  }
112
- throw new Error(`Timeout waiting for request after ${timeout}ms`);
341
+ const captured = await this.requests.get(requestId);
342
+ const headers = {};
343
+ for (const [key, val] of Object.entries(captured.headers)) {
344
+ const lower = key.toLowerCase();
345
+ if (!HOP_BY_HOP_HEADERS.has(lower) && !SENSITIVE_HEADERS.has(lower)) {
346
+ headers[key] = val;
347
+ }
348
+ }
349
+ const upperMethod = captured.method.toUpperCase();
350
+ const body = upperMethod === "GET" || upperMethod === "HEAD" ? void 0 : captured.body ?? void 0;
351
+ return fetch(targetUrl, {
352
+ method: captured.method,
353
+ headers,
354
+ body,
355
+ signal: AbortSignal.timeout(this.timeout)
356
+ });
357
+ },
358
+ /**
359
+ * Stream incoming requests via SSE as an async iterator.
360
+ *
361
+ * Connects to the SSE endpoint and yields Request objects as they arrive.
362
+ * The connection is closed when the iterator is broken, the signal is aborted,
363
+ * or the timeout expires.
364
+ *
365
+ * No automatic reconnection — if the connection drops, the iterator ends.
366
+ */
367
+ subscribe: (slug, options = {}) => {
368
+ validatePathSegment(slug, "slug");
369
+ const { signal, timeout } = options;
370
+ const baseUrl = this.baseUrl;
371
+ const apiKey = this.apiKey;
372
+ const timeoutMs = timeout !== void 0 ? parseDuration(timeout) : void 0;
373
+ return {
374
+ [Symbol.asyncIterator]() {
375
+ const controller = new AbortController();
376
+ let timeoutId;
377
+ let iterator = null;
378
+ let started = false;
379
+ const onAbort = () => controller.abort();
380
+ if (signal) {
381
+ if (signal.aborted) {
382
+ controller.abort();
383
+ } else {
384
+ signal.addEventListener("abort", onAbort, { once: true });
385
+ }
386
+ }
387
+ if (timeoutMs !== void 0) {
388
+ timeoutId = setTimeout(() => controller.abort(), timeoutMs);
389
+ }
390
+ const cleanup = () => {
391
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
392
+ if (signal) signal.removeEventListener("abort", onAbort);
393
+ };
394
+ const start = async () => {
395
+ const url = `${baseUrl}/api/stream/${slug}`;
396
+ const connectController = new AbortController();
397
+ const connectTimeout = setTimeout(() => connectController.abort(), 3e4);
398
+ controller.signal.addEventListener("abort", () => connectController.abort(), {
399
+ once: true
400
+ });
401
+ let response;
402
+ try {
403
+ response = await fetch(url, {
404
+ headers: { Authorization: `Bearer ${apiKey}` },
405
+ signal: connectController.signal
406
+ });
407
+ } finally {
408
+ clearTimeout(connectTimeout);
409
+ }
410
+ if (!response.ok) {
411
+ cleanup();
412
+ const text = await response.text();
413
+ throw mapStatusToError(response.status, text, response);
414
+ }
415
+ if (!response.body) {
416
+ cleanup();
417
+ throw new Error("SSE response has no body");
418
+ }
419
+ controller.signal.addEventListener(
420
+ "abort",
421
+ () => {
422
+ response.body?.cancel();
423
+ },
424
+ { once: true }
425
+ );
426
+ return parseSSE(response.body);
427
+ };
428
+ return {
429
+ [Symbol.asyncIterator]() {
430
+ return this;
431
+ },
432
+ async next() {
433
+ try {
434
+ if (!started) {
435
+ started = true;
436
+ iterator = await start();
437
+ }
438
+ while (iterator) {
439
+ const { done, value } = await iterator.next();
440
+ if (done) {
441
+ cleanup();
442
+ return { done: true, value: void 0 };
443
+ }
444
+ if (value.event === "request") {
445
+ try {
446
+ const data = JSON.parse(value.data);
447
+ if (!data.endpointId || !data.method || !data.headers || !data.receivedAt) {
448
+ continue;
449
+ }
450
+ const req = {
451
+ id: data._id ?? data.id,
452
+ endpointId: data.endpointId,
453
+ method: data.method,
454
+ path: data.path ?? "/",
455
+ headers: data.headers,
456
+ body: data.body ?? void 0,
457
+ queryParams: data.queryParams ?? {},
458
+ contentType: data.contentType ?? void 0,
459
+ ip: data.ip ?? "unknown",
460
+ size: data.size ?? 0,
461
+ receivedAt: data.receivedAt
462
+ };
463
+ return { done: false, value: req };
464
+ } catch {
465
+ continue;
466
+ }
467
+ }
468
+ if (value.event === "timeout" || value.event === "endpoint_deleted") {
469
+ cleanup();
470
+ return { done: true, value: void 0 };
471
+ }
472
+ }
473
+ cleanup();
474
+ return { done: true, value: void 0 };
475
+ } catch (error) {
476
+ cleanup();
477
+ controller.abort();
478
+ if (error instanceof Error && error.name === "AbortError") {
479
+ return { done: true, value: void 0 };
480
+ }
481
+ throw error;
482
+ }
483
+ },
484
+ async return() {
485
+ cleanup();
486
+ controller.abort();
487
+ if (iterator) {
488
+ await iterator.return(void 0);
489
+ }
490
+ return { done: true, value: void 0 };
491
+ }
492
+ };
493
+ }
494
+ };
113
495
  }
114
496
  };
497
+ if (!options.apiKey || typeof options.apiKey !== "string") {
498
+ throw new Error("Missing or invalid apiKey. Get one at https://webhooks.cc/account");
499
+ }
115
500
  this.apiKey = options.apiKey;
116
- this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
501
+ this.baseUrl = stripTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL);
502
+ this.webhookUrl = stripTrailingSlashes(options.webhookUrl ?? DEFAULT_WEBHOOK_URL);
117
503
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
504
+ this.hooks = options.hooks ?? {};
118
505
  }
119
506
  async request(method, path, body) {
507
+ const url = `${this.baseUrl}/api${path}`;
120
508
  const controller = new AbortController();
121
509
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
510
+ const start = Date.now();
122
511
  try {
123
- const response = await fetch(`${this.baseUrl}/api${path}`, {
512
+ this.hooks.onRequest?.({ method, url });
513
+ } catch {
514
+ }
515
+ try {
516
+ const response = await fetch(url, {
124
517
  method,
125
518
  headers: {
126
519
  Authorization: `Bearer ${this.apiKey}`,
@@ -129,10 +522,20 @@ var WebhooksCC = class {
129
522
  body: body ? JSON.stringify(body) : void 0,
130
523
  signal: controller.signal
131
524
  });
525
+ const durationMs = Date.now() - start;
132
526
  if (!response.ok) {
133
- const error = await response.text();
134
- const sanitizedError = error.length > 200 ? error.slice(0, 200) + "..." : error;
135
- throw new ApiError(response.status, sanitizedError);
527
+ const errorText = await response.text();
528
+ const sanitizedError = errorText.length > 200 ? errorText.slice(0, 200) + "..." : errorText;
529
+ const error = mapStatusToError(response.status, sanitizedError, response);
530
+ try {
531
+ this.hooks.onError?.({ method, url, error, durationMs });
532
+ } catch {
533
+ }
534
+ throw error;
535
+ }
536
+ try {
537
+ this.hooks.onResponse?.({ method, url, status: response.status, durationMs });
538
+ } catch {
136
539
  }
137
540
  if (response.status === 204 || response.headers.get("content-length") === "0") {
138
541
  return void 0;
@@ -144,18 +547,196 @@ var WebhooksCC = class {
144
547
  return response.json();
145
548
  } catch (error) {
146
549
  if (error instanceof Error && error.name === "AbortError") {
147
- throw new Error(`Request timed out after ${this.timeout}ms`);
550
+ const timeoutError = new TimeoutError(this.timeout);
551
+ try {
552
+ this.hooks.onError?.({
553
+ method,
554
+ url,
555
+ error: timeoutError,
556
+ durationMs: Date.now() - start
557
+ });
558
+ } catch {
559
+ }
560
+ throw timeoutError;
148
561
  }
149
562
  throw error;
150
563
  } finally {
151
564
  clearTimeout(timeoutId);
152
565
  }
153
566
  }
567
+ /** Returns a static description of all SDK operations (no API call). */
568
+ describe() {
569
+ return {
570
+ version: SDK_VERSION,
571
+ endpoints: {
572
+ create: {
573
+ description: "Create a webhook endpoint",
574
+ params: { name: "string?" }
575
+ },
576
+ list: {
577
+ description: "List all endpoints",
578
+ params: {}
579
+ },
580
+ get: {
581
+ description: "Get endpoint by slug",
582
+ params: { slug: "string" }
583
+ },
584
+ update: {
585
+ description: "Update endpoint settings",
586
+ params: { slug: "string", name: "string?", mockResponse: "object?" }
587
+ },
588
+ delete: {
589
+ description: "Delete endpoint and its requests",
590
+ params: { slug: "string" }
591
+ },
592
+ send: {
593
+ description: "Send a test webhook to endpoint",
594
+ params: { slug: "string", method: "string?", headers: "object?", body: "unknown?" }
595
+ }
596
+ },
597
+ requests: {
598
+ list: {
599
+ description: "List captured requests",
600
+ params: { endpointSlug: "string", limit: "number?", since: "number?" }
601
+ },
602
+ get: {
603
+ description: "Get request by ID",
604
+ params: { requestId: "string" }
605
+ },
606
+ waitFor: {
607
+ description: "Poll until a matching request arrives",
608
+ params: {
609
+ endpointSlug: "string",
610
+ timeout: "number|string?",
611
+ match: "function?"
612
+ }
613
+ },
614
+ subscribe: {
615
+ description: "Stream requests via SSE",
616
+ params: { slug: "string", signal: "AbortSignal?", timeout: "number|string?" }
617
+ },
618
+ replay: {
619
+ description: "Replay a captured request to a URL",
620
+ params: { requestId: "string", targetUrl: "string" }
621
+ }
622
+ }
623
+ };
624
+ }
154
625
  };
155
626
  function sleep(ms) {
156
627
  return new Promise((resolve) => setTimeout(resolve, ms));
157
628
  }
629
+ function stripTrailingSlashes(url) {
630
+ let i = url.length;
631
+ while (i > 0 && url[i - 1] === "/") i--;
632
+ return i === url.length ? url : url.slice(0, i);
633
+ }
634
+
635
+ // src/helpers.ts
636
+ function parseJsonBody(request) {
637
+ if (!request.body) return void 0;
638
+ try {
639
+ return JSON.parse(request.body);
640
+ } catch {
641
+ return void 0;
642
+ }
643
+ }
644
+ function isStripeWebhook(request) {
645
+ return Object.keys(request.headers).some((k) => k.toLowerCase() === "stripe-signature");
646
+ }
647
+ function isGitHubWebhook(request) {
648
+ return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-github-event");
649
+ }
650
+ function matchJsonField(field, value) {
651
+ return (request) => {
652
+ const body = parseJsonBody(request);
653
+ if (typeof body !== "object" || body === null) return false;
654
+ if (!Object.prototype.hasOwnProperty.call(body, field)) return false;
655
+ return body[field] === value;
656
+ };
657
+ }
658
+ function isShopifyWebhook(request) {
659
+ return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-shopify-hmac-sha256");
660
+ }
661
+ function isSlackWebhook(request) {
662
+ return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-slack-signature");
663
+ }
664
+ function isTwilioWebhook(request) {
665
+ return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-twilio-signature");
666
+ }
667
+ function isPaddleWebhook(request) {
668
+ return Object.keys(request.headers).some((k) => k.toLowerCase() === "paddle-signature");
669
+ }
670
+ function isLinearWebhook(request) {
671
+ return Object.keys(request.headers).some((k) => k.toLowerCase() === "linear-signature");
672
+ }
673
+
674
+ // src/matchers.ts
675
+ function matchMethod(method) {
676
+ const upper = method.toUpperCase();
677
+ return (request) => request.method.toUpperCase() === upper;
678
+ }
679
+ function matchHeader(name, value) {
680
+ const lowerName = name.toLowerCase();
681
+ return (request) => {
682
+ const entry = Object.entries(request.headers).find(([k]) => k.toLowerCase() === lowerName);
683
+ if (!entry) return false;
684
+ if (value === void 0) return true;
685
+ return entry[1] === value;
686
+ };
687
+ }
688
+ function matchBodyPath(path, value) {
689
+ const keys = path.split(".");
690
+ return (request) => {
691
+ const body = parseJsonBody(request);
692
+ if (typeof body !== "object" || body === null) return false;
693
+ let current = body;
694
+ for (const key of keys) {
695
+ if (current === null || current === void 0) return false;
696
+ if (Array.isArray(current)) {
697
+ const idx = Number(key);
698
+ if (!Number.isInteger(idx) || idx < 0 || idx >= current.length) return false;
699
+ current = current[idx];
700
+ } else if (typeof current === "object") {
701
+ if (!Object.prototype.hasOwnProperty.call(current, key)) return false;
702
+ current = current[key];
703
+ } else {
704
+ return false;
705
+ }
706
+ }
707
+ return current === value;
708
+ };
709
+ }
710
+ function matchAll(first, ...rest) {
711
+ const matchers = [first, ...rest];
712
+ return (request) => matchers.every((m) => m(request));
713
+ }
714
+ function matchAny(first, ...rest) {
715
+ const matchers = [first, ...rest];
716
+ return (request) => matchers.some((m) => m(request));
717
+ }
158
718
  export {
159
719
  ApiError,
160
- WebhooksCC
720
+ NotFoundError,
721
+ RateLimitError,
722
+ TimeoutError,
723
+ UnauthorizedError,
724
+ WebhooksCC,
725
+ WebhooksCCError,
726
+ isGitHubWebhook,
727
+ isLinearWebhook,
728
+ isPaddleWebhook,
729
+ isShopifyWebhook,
730
+ isSlackWebhook,
731
+ isStripeWebhook,
732
+ isTwilioWebhook,
733
+ matchAll,
734
+ matchAny,
735
+ matchBodyPath,
736
+ matchHeader,
737
+ matchJsonField,
738
+ matchMethod,
739
+ parseDuration,
740
+ parseJsonBody,
741
+ parseSSE
161
742
  };