@amirabaris/webhook-kit 0.0.0-stage → 0.1.1

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.cjs ADDED
@@ -0,0 +1,1774 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ // src/errors.ts
3
+ /**
4
+ * Base error class for all webhook-kit errors.
5
+ */
6
+ var WebhookKitError = class extends Error {
7
+ code;
8
+ constructor(message, code = "WEBHOOK_KIT_ERROR") {
9
+ super(message);
10
+ this.name = "WebhookKitError";
11
+ this.code = code;
12
+ Object.setPrototypeOf(this, new.target.prototype);
13
+ }
14
+ };
15
+ /**
16
+ * Thrown or returned when signature verification fails (e.g. invalid HMAC or mismatched hash).
17
+ */
18
+ var SignatureVerificationError = class extends WebhookKitError {
19
+ constructor(message = "Signature verification failed: signature does not match expected HMAC") {
20
+ super(message, "SIGNATURE_VERIFICATION_FAILED");
21
+ this.name = "SignatureVerificationError";
22
+ }
23
+ };
24
+ /**
25
+ * Thrown when the signature header is missing or malformed.
26
+ */
27
+ var InvalidSignatureHeaderError = class extends WebhookKitError {
28
+ constructor(message = "Invalid or missing signature header") {
29
+ super(message, "INVALID_SIGNATURE_HEADER");
30
+ this.name = "InvalidSignatureHeaderError";
31
+ }
32
+ };
33
+ /**
34
+ * Thrown when the timestamp outside the allowed tolerance window (replay attack protection).
35
+ */
36
+ var TimestampToleranceError = class extends WebhookKitError {
37
+ timestamp;
38
+ toleranceSeconds;
39
+ constructor(timestamp, toleranceSeconds, message) {
40
+ super(message ?? `Timestamp outside tolerance window (timestamp: ${timestamp}, tolerance: ${toleranceSeconds}s)`, "TIMESTAMP_TOLERANCE_EXCEEDED");
41
+ this.name = "TimestampToleranceError";
42
+ this.timestamp = timestamp;
43
+ this.toleranceSeconds = toleranceSeconds;
44
+ }
45
+ };
46
+ /**
47
+ * Thrown when extracting raw payload from request or stream fails.
48
+ */
49
+ var PayloadExtractionError = class extends WebhookKitError {
50
+ constructor(message = "Failed to extract raw body payload for verification") {
51
+ super(message, "PAYLOAD_EXTRACTION_FAILED");
52
+ this.name = "PayloadExtractionError";
53
+ }
54
+ };
55
+ /**
56
+ * Thrown or returned when a webhook event ID has already been processed in the replay store.
57
+ */
58
+ var ReplayAttackError = class extends WebhookKitError {
59
+ eventId;
60
+ constructor(eventId, message) {
61
+ super(message ?? `Duplicate webhook event rejected (replay protection): event "${eventId}" was already processed`, "WEBHOOK_REPLAY_DETECTED");
62
+ this.name = "ReplayAttackError";
63
+ this.eventId = eventId;
64
+ }
65
+ };
66
+ // src/store.ts
67
+ const DEFAULT_REPLAY_STORE_MAX_SIZE = 1e4;
68
+ const DEFAULT_REPLAY_STORE_TTL_SECONDS = 300;
69
+ const GENERIC_EVENT_ID_HEADERS = [
70
+ "x-webhook-id",
71
+ "webhook-id",
72
+ "x-github-delivery",
73
+ "x-shopify-webhook-id",
74
+ "x-event-id",
75
+ "x-idempotency-key"
76
+ ];
77
+ /**
78
+ * Default in-memory TTL replay store backed by a JavaScript `Map`.
79
+ * Automatically evicts expired entries on lookup and enforces an upper capacity bound
80
+ * with LRU (Least Recently Used) eviction.
81
+ */
82
+ var InMemoryReplayStore = class {
83
+ maxSize;
84
+ defaultTtlSeconds;
85
+ now;
86
+ entries = /* @__PURE__ */ new Map();
87
+ constructor(options = {}) {
88
+ this.maxSize = options.maxSize ?? DEFAULT_REPLAY_STORE_MAX_SIZE;
89
+ this.defaultTtlSeconds = options.defaultTtlSeconds ?? DEFAULT_REPLAY_STORE_TTL_SECONDS;
90
+ this.now = options.now ?? Date.now;
91
+ }
92
+ /**
93
+ * Checks whether `eventId` is currently tracked and unexpired in the store.
94
+ *
95
+ * @param eventId - Unique webhook event or delivery identifier.
96
+ * @returns `true` if `eventId` is present and unexpired, `false` otherwise.
97
+ */
98
+ has(eventId) {
99
+ const expiresAt = this.entries.get(eventId);
100
+ if (expiresAt === void 0) return false;
101
+ if (this.now() >= expiresAt) {
102
+ this.entries.delete(eventId);
103
+ return false;
104
+ }
105
+ return true;
106
+ }
107
+ /**
108
+ * Records `eventId` in the store with the specified TTL in seconds.
109
+ *
110
+ * @param eventId - Unique webhook event or delivery identifier.
111
+ * @param ttlSeconds - Time-to-live in seconds before expiration.
112
+ */
113
+ set(eventId, ttlSeconds = this.defaultTtlSeconds) {
114
+ const effectiveTtl = Number.isFinite(ttlSeconds) ? ttlSeconds : this.defaultTtlSeconds;
115
+ if (effectiveTtl <= 0) {
116
+ this.entries.delete(eventId);
117
+ return;
118
+ }
119
+ if (this.entries.has(eventId)) this.entries.delete(eventId);
120
+ else if (this.entries.size >= this.maxSize) this.evictExpiredOrOldest();
121
+ this.entries.set(eventId, this.now() + effectiveTtl * 1e3);
122
+ }
123
+ /**
124
+ * Removes a specific `eventId` from the store.
125
+ *
126
+ * @param eventId - Unique webhook event or delivery identifier.
127
+ * @returns `true` if the entry existed and was removed.
128
+ */
129
+ delete(eventId) {
130
+ return this.entries.delete(eventId);
131
+ }
132
+ /**
133
+ * Clears all tracked event IDs from the store.
134
+ */
135
+ clear() {
136
+ this.entries.clear();
137
+ }
138
+ /**
139
+ * Removes all expired entries from the store and returns the number of purged entries.
140
+ *
141
+ * @returns Count of expired entries removed.
142
+ */
143
+ prune() {
144
+ const currentMs = this.now();
145
+ let removed = 0;
146
+ for (const [key, expiresAt] of this.entries.entries()) if (currentMs >= expiresAt) {
147
+ this.entries.delete(key);
148
+ removed += 1;
149
+ }
150
+ return removed;
151
+ }
152
+ /**
153
+ * Returns the current number of entries stored in the underlying map.
154
+ */
155
+ get size() {
156
+ return this.entries.size;
157
+ }
158
+ /**
159
+ * Returns the maximum capacity of the store.
160
+ */
161
+ get capacity() {
162
+ return this.maxSize;
163
+ }
164
+ evictExpiredOrOldest() {
165
+ this.prune();
166
+ if (this.entries.size < this.maxSize) return;
167
+ const oldestKey = this.entries.keys().next().value;
168
+ if (oldestKey !== void 0) this.entries.delete(oldestKey);
169
+ }
170
+ };
171
+ /**
172
+ * Creates a new `InMemoryReplayStore` instance with optional capacity and TTL settings.
173
+ *
174
+ * @param options - Optional capacity, default TTL, and clock configuration.
175
+ * @returns Configured `InMemoryReplayStore` instance.
176
+ */
177
+ function createInMemoryReplayStore(options) {
178
+ return new InMemoryReplayStore(options);
179
+ }
180
+ /**
181
+ * Case-insensitively extracts a single header value from a `Headers` instance or plain record.
182
+ *
183
+ * @param headers - Web standard `Headers` instance or record.
184
+ * @param targetName - Lowercase target header name.
185
+ * @returns Header value string or undefined.
186
+ */
187
+ function getHeaderCaseInsensitive(headers, targetName) {
188
+ if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(targetName) ?? void 0;
189
+ const record = headers;
190
+ const lowerTarget = targetName.toLowerCase();
191
+ for (const key of Object.keys(record)) if (key.toLowerCase() === lowerTarget) {
192
+ const value = record[key];
193
+ return Array.isArray(value) ? value[0] : value;
194
+ }
195
+ }
196
+ /**
197
+ * Searches candidate header names in a `Headers` or record object for a non-empty value.
198
+ *
199
+ * @param headers - Headers source.
200
+ * @param candidateNames - Ordered list of lowercase header names.
201
+ * @returns Trimmed non-empty header value or undefined.
202
+ */
203
+ function findHeaderValue(headers, candidateNames) {
204
+ if (!headers) return;
205
+ for (const name of candidateNames) {
206
+ const trimmed = getHeaderCaseInsensitive(headers, name)?.trim();
207
+ if (trimmed) return trimmed;
208
+ }
209
+ }
210
+ /**
211
+ * Extracts a string or finite number `id` property from a parsed payload or raw JSON body.
212
+ *
213
+ * @param payload - Already parsed payload object if available.
214
+ * @param rawBody - Raw JSON string fallback when `parseJson` was disabled.
215
+ * @returns Extracted string event ID or undefined.
216
+ */
217
+ function extractIdFromPayload(payload, rawBody) {
218
+ const candidate = resolvePayloadObject(payload, rawBody);
219
+ if (!candidate) return;
220
+ const idValue = candidate.id;
221
+ if (typeof idValue === "string" && idValue.trim()) return idValue.trim();
222
+ if (typeof idValue === "number" && Number.isFinite(idValue)) return String(idValue);
223
+ }
224
+ /**
225
+ * Resolves a record object from either an already-parsed payload or a raw JSON string.
226
+ *
227
+ * @param payload - Parsed payload value.
228
+ * @param rawBody - Raw body string.
229
+ * @returns Object record or null.
230
+ */
231
+ function resolvePayloadObject(payload, rawBody) {
232
+ if (typeof payload === "object" && payload !== null) return payload;
233
+ if (!rawBody) return null;
234
+ try {
235
+ const parsed = JSON.parse(rawBody);
236
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+ /**
242
+ * Extracts a matching header value from a Web Standard `Request` body if provided.
243
+ *
244
+ * @param body - Candidate raw body input.
245
+ * @param headerCandidates - Ordered delivery ID header names.
246
+ * @returns Matching header value or undefined.
247
+ */
248
+ function findHeaderInRequest(body, headerCandidates) {
249
+ if (typeof Request !== "undefined" && body instanceof Request) return findHeaderValue(body.headers, headerCandidates);
250
+ }
251
+ /**
252
+ * Resolves the unique webhook event ID from explicit options, delivery headers, or payload `id`.
253
+ *
254
+ * @param options - Source options containing explicit `eventId`, `headers`, `body`, `payload`, and `rawBody`.
255
+ * @param headerCandidates - Ordered delivery ID header names to inspect prior to payload fallback.
256
+ * @returns Resolved non-empty event ID string or `undefined`.
257
+ */
258
+ function resolveWebhookEventId(options, headerCandidates = GENERIC_EVENT_ID_HEADERS) {
259
+ const explicitId = options.eventId?.trim();
260
+ if (explicitId) return explicitId;
261
+ const fromHeaders = findHeaderValue(options.headers, headerCandidates) ?? findHeaderInRequest(options.body, headerCandidates);
262
+ if (fromHeaders) return fromHeaders;
263
+ return extractIdFromPayload(options.payload, options.rawBody);
264
+ }
265
+ /**
266
+ * Computes the effective replay store TTL in seconds from explicit `storeTtlSeconds` or `toleranceSeconds`.
267
+ *
268
+ * @param storeTtlSeconds - Explicit TTL in seconds.
269
+ * @param toleranceSeconds - Timestamp tolerance in seconds.
270
+ * @returns Positive TTL in seconds (defaults to 300s).
271
+ */
272
+ function resolveEffectiveTtl(storeTtlSeconds, toleranceSeconds) {
273
+ if (storeTtlSeconds !== void 0 && storeTtlSeconds > 0) return storeTtlSeconds;
274
+ if (toleranceSeconds !== void 0 && toleranceSeconds > 0) return toleranceSeconds;
275
+ return DEFAULT_REPLAY_STORE_TTL_SECONDS;
276
+ }
277
+ /**
278
+ * Verifies that `eventId` has not already been seen in `store`, and records it with a TTL if unseen.
279
+ *
280
+ * @param options - Store instance, resolved `eventId`, and TTL configuration.
281
+ * @returns `ReplayAttackError` if `eventId` is already present in `store`, or `null` if unseen / no store.
282
+ */
283
+ async function checkAndRecordReplay(options) {
284
+ const { store, eventId } = options;
285
+ if (!store || !eventId) return null;
286
+ if (await store.has(eventId)) return new ReplayAttackError(eventId);
287
+ const ttlSeconds = resolveEffectiveTtl(options.storeTtlSeconds, options.toleranceSeconds);
288
+ await store.set(eventId, ttlSeconds);
289
+ return null;
290
+ }
291
+ // src/crypto/subtle.ts
292
+ /**
293
+ * Lightweight LRU (Least Recently Used) cache backed by a JavaScript `Map`.
294
+ * Reuses compiled WebCrypto keys across concurrent requests and evicts the
295
+ * oldest entries when capacity is reached.
296
+ */
297
+ var CryptoKeyLruCache = class {
298
+ maxSize;
299
+ map = /* @__PURE__ */ new Map();
300
+ constructor(maxSize = 256) {
301
+ this.maxSize = maxSize;
302
+ }
303
+ get(key) {
304
+ const item = this.map.get(key);
305
+ if (!item) return;
306
+ this.map.delete(key);
307
+ this.map.set(key, item);
308
+ return item;
309
+ }
310
+ set(key, value) {
311
+ if (this.map.has(key)) this.map.delete(key);
312
+ else if (this.map.size >= this.maxSize) {
313
+ const oldestKey = this.map.keys().next().value;
314
+ if (oldestKey !== void 0) this.map.delete(oldestKey);
315
+ }
316
+ this.map.set(key, value);
317
+ }
318
+ has(key) {
319
+ return this.map.has(key);
320
+ }
321
+ delete(key) {
322
+ return this.map.delete(key);
323
+ }
324
+ clear() {
325
+ this.map.clear();
326
+ }
327
+ get size() {
328
+ return this.map.size;
329
+ }
330
+ get capacity() {
331
+ return this.maxSize;
332
+ }
333
+ };
334
+ /**
335
+ * Global in-memory LRU cache holding compiled WebCrypto HMAC keys.
336
+ */
337
+ const defaultCryptoKeyCache = new CryptoKeyLruCache(256);
338
+ /**
339
+ * Computes a fast 128-bit composite hash of secret bytes for safe cache key generation
340
+ * without retaining raw secret strings or byte sequences in cache keys.
341
+ *
342
+ * @param bytes Raw secret bytes.
343
+ * @returns Hex-encoded composite hash with length prefix.
344
+ */
345
+ function hashSecret(bytes) {
346
+ let h1 = 2166136261;
347
+ let h2 = 668265263;
348
+ let h3 = 2654435769;
349
+ let h4 = 1164378403;
350
+ const len = bytes.length;
351
+ for (let i = 0; i < len; i++) {
352
+ const b = bytes[i];
353
+ h1 = Math.imul(h1 ^ b, 16777619);
354
+ h2 = Math.imul(h2 ^ b + i, 1540483477);
355
+ h3 = Math.imul(h3 ^ (b << 4 | b >>> 4), 461845907);
356
+ h4 = Math.imul(h4 ^ b * 31, 3432918353);
357
+ }
358
+ return `${len}_${(h1 >>> 0).toString(16)}_${(h2 >>> 0).toString(16)}_${(h3 >>> 0).toString(16)}_${(h4 >>> 0).toString(16)}`;
359
+ }
360
+ /**
361
+ * Generates an in-memory cache key composed of the normalized algorithm and secret hash.
362
+ *
363
+ * @param normalizedHash Normalized hash algorithm (e.g. `'SHA-256'`).
364
+ * @param secretBytes Raw secret bytes.
365
+ * @returns Formatted cache key string.
366
+ */
367
+ function getSecretCacheKey(normalizedHash, secretBytes) {
368
+ return `${normalizedHash}:${hashSecret(secretBytes)}`;
369
+ }
370
+ /**
371
+ * Invokes `crypto.subtle.importKey` for an HMAC signing key.
372
+ */
373
+ function importHmacKey(subtle, normalizedHash, secretBytes) {
374
+ return subtle.importKey("raw", secretBytes, {
375
+ name: "HMAC",
376
+ hash: { name: normalizedHash }
377
+ }, false, ["sign"]);
378
+ }
379
+ /**
380
+ * Resolves a compiled `CryptoKey` from cache or initiates and caches a new one.
381
+ */
382
+ function getOrCreateCryptoKey(subtle, normalizedHash, secretBytes, cache = defaultCryptoKeyCache) {
383
+ const cacheKey = getSecretCacheKey(normalizedHash, secretBytes);
384
+ const cachedPromise = cache.get(cacheKey);
385
+ if (cachedPromise) return cachedPromise;
386
+ const keyPromise = importHmacKey(subtle, normalizedHash, secretBytes);
387
+ keyPromise.catch(() => {
388
+ cache.delete(cacheKey);
389
+ });
390
+ cache.set(cacheKey, keyPromise);
391
+ return keyPromise;
392
+ }
393
+ /**
394
+ * Clears all compiled WebCrypto HMAC keys from the global key cache.
395
+ */
396
+ function clearCryptoKeyCache() {
397
+ defaultCryptoKeyCache.clear();
398
+ }
399
+ /**
400
+ * Returns the current number of compiled WebCrypto HMAC keys stored in the global key cache.
401
+ */
402
+ function getCryptoKeyCacheSize() {
403
+ return defaultCryptoKeyCache.size;
404
+ }
405
+ /**
406
+ * Retrieves a cached WebCrypto HMAC key or compiles and caches a new one.
407
+ *
408
+ * @param algorithm HMAC digest algorithm (e.g. `'SHA-256'`).
409
+ * @param secret Raw secret string or byte array.
410
+ * @param cache Optional custom `CryptoKeyLruCache` instance.
411
+ * @returns Compiled `CryptoKey` instance.
412
+ */
413
+ async function getCachedCryptoKey(algorithm, secret, cache = defaultCryptoKeyCache) {
414
+ const subtle = globalThis.crypto?.subtle;
415
+ if (!subtle) throw new Error("Web Cryptography API (crypto.subtle) is not available in the current runtime environment.");
416
+ return getOrCreateCryptoKey(subtle, normalizeHashAlgorithm(algorithm), toUint8Array(secret), cache);
417
+ }
418
+ /**
419
+ * Normalizes hash algorithm names into standard W3C Web Crypto format.
420
+ *
421
+ * @param algorithm Algorithm identifier (e.g. `'sha256'`, `'SHA256'`, `'SHA-256'`).
422
+ * @returns Normalized Web Crypto hash identifier (e.g. `'SHA-256'`).
423
+ */
424
+ function normalizeHashAlgorithm(algorithm) {
425
+ const upper = algorithm.toUpperCase().trim();
426
+ if (/^SHA-?\d+$/.test(upper)) return upper.startsWith("SHA-") ? upper : upper.replace("SHA", "SHA-");
427
+ return upper;
428
+ }
429
+ /**
430
+ * Converts a string or buffer into a `Uint8Array` backed by an `ArrayBuffer`.
431
+ *
432
+ * @param input String, `Uint8Array`, or `ArrayBuffer` payload.
433
+ * @returns `Uint8Array<ArrayBuffer>` view suitable for Web Crypto `BufferSource`.
434
+ */
435
+ function toUint8Array(input) {
436
+ if (typeof input === "string") return new TextEncoder().encode(input);
437
+ if (input instanceof Uint8Array) return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
438
+ return new Uint8Array(input);
439
+ }
440
+ /**
441
+ * Constant-time comparison between two buffers or strings to prevent timing side-channel attacks.
442
+ *
443
+ * @param a First byte array or string.
444
+ * @param b Second byte array or string.
445
+ * @returns `true` if byte sequences are identical, `false` otherwise.
446
+ */
447
+ function timingSafeEqual(a, b) {
448
+ const bufA = typeof a === "string" ? new TextEncoder().encode(a) : a;
449
+ const bufB = typeof b === "string" ? new TextEncoder().encode(b) : b;
450
+ const lenA = bufA.length;
451
+ const lenB = bufB.length;
452
+ let mismatch = lenA === lenB ? 0 : 1;
453
+ const maxLen = Math.max(lenA, lenB);
454
+ for (let i = 0; i < maxLen; i++) {
455
+ const byteA = i < lenA ? bufA[i] : 0;
456
+ const byteB = i < lenB ? bufB[i] : 0;
457
+ mismatch |= byteA ^ byteB;
458
+ }
459
+ return mismatch === 0;
460
+ }
461
+ /**
462
+ * Formats a raw HMAC digest according to the requested encoding.
463
+ */
464
+ function formatHmacDigest(bytes, encoding) {
465
+ if (encoding === "hex") return bytesToHex(bytes);
466
+ if (encoding === "base64") return bytesToBase64(bytes);
467
+ return bytes;
468
+ }
469
+ /**
470
+ * Normalizes options and extract encoding and cache preferences.
471
+ */
472
+ function extractHmacOptions(encodingOrOptions) {
473
+ if (typeof encodingOrOptions === "string") return {
474
+ encoding: encodingOrOptions,
475
+ bypassCache: false
476
+ };
477
+ return {
478
+ encoding: encodingOrOptions?.encoding,
479
+ bypassCache: Boolean(encodingOrOptions?.bypassCache)
480
+ };
481
+ }
482
+ /**
483
+ * Obtains a `CryptoKey` either by bypassing or utilizing the LRU cache.
484
+ */
485
+ function resolveSubtleHmacKey(subtle, normalizedHash, secretBytes, bypassCache) {
486
+ if (bypassCache) return importHmacKey(subtle, normalizedHash, secretBytes);
487
+ return getOrCreateCryptoKey(subtle, normalizedHash, secretBytes);
488
+ }
489
+ async function computeSubtleHmac(algorithm, secret, data, encodingOrOptions) {
490
+ const subtle = globalThis.crypto?.subtle;
491
+ if (!subtle) throw new Error("Web Cryptography API (crypto.subtle) is not available in the current runtime environment.");
492
+ const normalizedHash = normalizeHashAlgorithm(algorithm);
493
+ const secretBytes = toUint8Array(secret);
494
+ const dataBytes = toUint8Array(data);
495
+ const { encoding, bypassCache } = extractHmacOptions(encodingOrOptions);
496
+ const key = await resolveSubtleHmacKey(subtle, normalizedHash, secretBytes, bypassCache);
497
+ const signatureBuffer = await subtle.sign("HMAC", key, dataBytes);
498
+ return formatHmacDigest(new Uint8Array(signatureBuffer), encoding);
499
+ }
500
+ /**
501
+ * Converts a `Uint8Array` into a lowercase hexadecimal string.
502
+ *
503
+ * @param bytes Input byte array.
504
+ * @returns Lowercase hexadecimal string.
505
+ */
506
+ function bytesToHex(bytes) {
507
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
508
+ }
509
+ /**
510
+ * Converts a hexadecimal string into a `Uint8Array`.
511
+ *
512
+ * @param hex Hexadecimal string (with optional `0x` prefix).
513
+ * @returns Decoded byte array.
514
+ */
515
+ function hexToBytes(hex) {
516
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
517
+ const normalizedHex = cleanHex.length % 2 === 1 ? `0${cleanHex}` : cleanHex;
518
+ const bytes = new Uint8Array(normalizedHex.length / 2);
519
+ for (let i = 0; i < normalizedHex.length; i += 2) bytes[i / 2] = parseInt(normalizedHex.slice(i, i + 2), 16);
520
+ return bytes;
521
+ }
522
+ /**
523
+ * Converts a `Uint8Array` into a Base64-encoded string using standard Web APIs.
524
+ *
525
+ * @param bytes Input byte array.
526
+ * @returns Base64-encoded string.
527
+ */
528
+ function bytesToBase64(bytes) {
529
+ let binary = "";
530
+ const len = bytes.byteLength;
531
+ const chunkSize = 32768;
532
+ for (let i = 0; i < len; i += chunkSize) {
533
+ const chunk = bytes.subarray(i, Math.min(i + chunkSize, len));
534
+ binary += String.fromCharCode(...chunk);
535
+ }
536
+ return globalThis.btoa(binary);
537
+ }
538
+ /**
539
+ * Converts a Base64-encoded string into a `Uint8Array` using standard Web APIs.
540
+ *
541
+ * @param base64 Base64-encoded string.
542
+ * @returns Decoded byte array.
543
+ */
544
+ function base64ToBytes(base64) {
545
+ const binary = globalThis.atob(base64.trim());
546
+ const bytes = new Uint8Array(binary.length);
547
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
548
+ return bytes;
549
+ }
550
+ // src/utils/extract-body.ts
551
+ /**
552
+ * Checks whether an unknown value is a binary BufferSource (ArrayBuffer or ArrayBufferView).
553
+ *
554
+ * @param value Candidate value.
555
+ * @returns `true` if the value is an ArrayBuffer or ArrayBufferView.
556
+ */
557
+ function isBinarySource(value) {
558
+ return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
559
+ }
560
+ /**
561
+ * Checks whether an unknown value is a Web Standard `Request`.
562
+ *
563
+ * @param value Candidate value.
564
+ * @returns `true` if the value is a Request instance.
565
+ */
566
+ function isRequest(value) {
567
+ return typeof Request !== "undefined" && value instanceof Request;
568
+ }
569
+ /**
570
+ * Checks whether an unknown value is a Web Standard `ReadableStream`.
571
+ *
572
+ * @param value Candidate value.
573
+ * @returns `true` if the value exposes a stream reader.
574
+ */
575
+ function isReadableStream(value) {
576
+ return typeof value === "object" && value !== null && typeof value.getReader === "function";
577
+ }
578
+ /**
579
+ * Clones and reads text from a Web Standard `Request` without consuming the original body.
580
+ *
581
+ * @param request Incoming `Request` instance.
582
+ * @returns Raw UTF-8 body string.
583
+ */
584
+ async function readRequestBody(request) {
585
+ try {
586
+ return await request.clone().text();
587
+ } catch (err) {
588
+ throw new PayloadExtractionError(`Failed to read text from Request body: ${err instanceof Error ? err.message : String(err)}`);
589
+ }
590
+ }
591
+ /**
592
+ * Decodes a single chunk from a ReadableStream using a streaming TextDecoder.
593
+ *
594
+ * @param decoder Streaming TextDecoder instance.
595
+ * @param chunk Incoming chunk value.
596
+ * @returns Decoded string chunk.
597
+ */
598
+ function decodeStreamChunk(decoder, chunk) {
599
+ if (typeof chunk === "string") return chunk;
600
+ if (isBinarySource(chunk)) return decoder.decode(chunk, { stream: true });
601
+ throw new PayloadExtractionError(`Unsupported stream chunk type: expected binary or string, received ${typeof chunk}`);
602
+ }
603
+ /**
604
+ * Reads all chunks from a Web Standard `ReadableStream` into a complete UTF-8 string.
605
+ *
606
+ * @param stream Incoming `ReadableStream` instance.
607
+ * @returns Complete raw string body.
608
+ */
609
+ async function readReadableStream(stream) {
610
+ try {
611
+ const reader = stream.getReader();
612
+ const decoder = new TextDecoder();
613
+ let result = "";
614
+ try {
615
+ while (true) {
616
+ const { done, value } = await reader.read();
617
+ if (done) break;
618
+ result += decodeStreamChunk(decoder, value);
619
+ }
620
+ result += decoder.decode();
621
+ return result;
622
+ } finally {
623
+ reader.releaseLock();
624
+ }
625
+ } catch (err) {
626
+ if (err instanceof PayloadExtractionError) throw err;
627
+ throw new PayloadExtractionError(`Failed to read text from ReadableStream: ${err instanceof Error ? err.message : String(err)}`);
628
+ }
629
+ }
630
+ /**
631
+ * Safely extracts the unmodified raw string body from various input representations:
632
+ * Web Standard `Request`, `ReadableStream`, `Uint8Array`, `ArrayBuffer`, `ArrayBufferView`, or primitive `string`.
633
+ *
634
+ * @param body The raw request or body object.
635
+ * @returns Unmodified string representation suitable for cryptographic HMAC verification.
636
+ * @throws {PayloadExtractionError} If body cannot be read or is invalid.
637
+ */
638
+ async function extractRawBody(body) {
639
+ if (typeof body === "string") return body;
640
+ if (isBinarySource(body)) return new TextDecoder().decode(body);
641
+ if (isRequest(body)) return readRequestBody(body);
642
+ if (isReadableStream(body)) return readReadableStream(body);
643
+ throw new PayloadExtractionError("Unsupported body type. Expected string, Uint8Array, ArrayBuffer, ReadableStream, or Request.");
644
+ }
645
+ // src/providers/github.ts
646
+ const GITHUB_SIGNATURE_PREFIX = "sha256=";
647
+ const GITHUB_EVENT_ID_HEADERS = [
648
+ "x-github-delivery",
649
+ "x-webhook-id",
650
+ "webhook-id"
651
+ ];
652
+ /**
653
+ * Asserts that a header value is a non-empty string.
654
+ *
655
+ * @param header - Candidate header value.
656
+ */
657
+ function assertNonEmptyHeader$2(header) {
658
+ if (typeof header !== "string" || !header.trim()) throw new InvalidSignatureHeaderError("Missing or empty X-Hub-Signature-256 header");
659
+ }
660
+ /**
661
+ * Parses an `X-Hub-Signature-256` header, validating and extracting the hex signature.
662
+ *
663
+ * @param header - Raw `X-Hub-Signature-256` header string (e.g. `sha256=d57c68d1...`).
664
+ * @returns Hexadecimal signature string after the `sha256=` prefix.
665
+ * @throws {InvalidSignatureHeaderError} If header is missing, empty, or lacks a valid sha256= prefix.
666
+ */
667
+ function parseGithubSignature(header) {
668
+ assertNonEmptyHeader$2(header);
669
+ const trimmed = header.trim();
670
+ if (!trimmed.toLowerCase().startsWith(GITHUB_SIGNATURE_PREFIX)) throw new InvalidSignatureHeaderError("Invalid X-Hub-Signature-256 header format: expected 'sha256=' prefix");
671
+ const signature = trimmed.slice(7).trim();
672
+ if (!signature) throw new InvalidSignatureHeaderError("Invalid X-Hub-Signature-256 header: missing signature hash after prefix");
673
+ return signature;
674
+ }
675
+ /**
676
+ * Case-insensitively extracts a header value from a `Headers` instance or plain record.
677
+ *
678
+ * @param headers - Web standard `Headers` instance or record.
679
+ * @param targetName - Lowercase target header name.
680
+ * @returns Header value string or undefined.
681
+ */
682
+ function extractHeaderValue$3(headers, targetName) {
683
+ if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(targetName) ?? void 0;
684
+ const record = headers;
685
+ const lowerTarget = targetName.toLowerCase();
686
+ for (const key of Object.keys(record)) if (key.toLowerCase() === lowerTarget) {
687
+ const value = record[key];
688
+ return Array.isArray(value) ? value[0] : value;
689
+ }
690
+ }
691
+ /**
692
+ * Resolves the raw signature header string from options, headers record, or request.
693
+ *
694
+ * @param options - GitHub verification options.
695
+ * @returns GitHub signature header string or undefined if not found.
696
+ */
697
+ function resolveSignatureHeader$2(options) {
698
+ if (options.signature) return options.signature;
699
+ if (options.headers) return extractHeaderValue$3(options.headers, "x-hub-signature-256");
700
+ if (typeof Request !== "undefined" && options.body instanceof Request) return options.body.headers.get("x-hub-signature-256") ?? void 0;
701
+ }
702
+ /**
703
+ * Validates preconditions for GitHub webhook verification.
704
+ *
705
+ * @param options - GitHub verification options.
706
+ * @param rawHeader - Resolved header string.
707
+ * @returns Error if preconditions fail, null otherwise.
708
+ */
709
+ function validatePreconditions$2(options, rawHeader) {
710
+ if (!options.secret) return new SignatureVerificationError("Missing GitHub webhook signing secret");
711
+ if (!rawHeader) return new InvalidSignatureHeaderError("Missing X-Hub-Signature-256 header");
712
+ return null;
713
+ }
714
+ /**
715
+ * Safely parses the raw signature header without uncaught exceptions.
716
+ *
717
+ * @param rawHeader - Raw header string.
718
+ * @returns Extracted signature string or error object.
719
+ */
720
+ function safeParseHeader$2(rawHeader) {
721
+ try {
722
+ return { signature: parseGithubSignature(rawHeader) };
723
+ } catch (err) {
724
+ return { error: err instanceof WebhookKitError ? err : new InvalidSignatureHeaderError(String(err)) };
725
+ }
726
+ }
727
+ /**
728
+ * Constructs a failed WebhookVerificationResult or throws if throwOnError is set.
729
+ *
730
+ * @param rawBody - Raw body string.
731
+ * @param error - WebhookKitError instance.
732
+ * @param throwOnError - Optional flag to throw immediately.
733
+ * @returns Failed verification result.
734
+ */
735
+ function failVerification$3(rawBody, error, throwOnError, eventId) {
736
+ if (throwOnError) throw error;
737
+ return {
738
+ valid: false,
739
+ rawBody,
740
+ eventId,
741
+ error
742
+ };
743
+ }
744
+ /**
745
+ * Attempts to parse the raw body as JSON.
746
+ *
747
+ * @param rawBody - Raw body payload string.
748
+ * @returns Parsed payload or undefined.
749
+ */
750
+ function tryParseJson$3(rawBody) {
751
+ try {
752
+ return JSON.parse(rawBody);
753
+ } catch {
754
+ return;
755
+ }
756
+ }
757
+ /**
758
+ * Verifies a GitHub webhook payload signature against `X-Hub-Signature-256`.
759
+ *
760
+ * GitHub signs the raw request payload using HMAC-SHA256 with the webhook secret.
761
+ *
762
+ * @param options - GitHub verification options.
763
+ * @returns Verification result with typed payload.
764
+ */
765
+ async function verifyGithubWebhook(options) {
766
+ const rawBody = await extractRawBody(options.body);
767
+ const rawHeader = resolveSignatureHeader$2(options);
768
+ const preconditionError = validatePreconditions$2(options, rawHeader);
769
+ if (preconditionError) return failVerification$3(rawBody, preconditionError, options.throwOnError);
770
+ const parsed = safeParseHeader$2(rawHeader);
771
+ if ("error" in parsed) return failVerification$3(rawBody, parsed.error, options.throwOnError);
772
+ if (!timingSafeEqual((await computeSubtleHmac("SHA-256", options.secret, rawBody, "hex")).toLowerCase(), parsed.signature.toLowerCase())) return failVerification$3(rawBody, new SignatureVerificationError("GitHub signature verification failed: signature does not match expected HMAC"), options.throwOnError);
773
+ const payload = options.parseJson !== false ? tryParseJson$3(rawBody) : void 0;
774
+ const eventId = resolveWebhookEventId({
775
+ eventId: options.eventId,
776
+ headers: options.headers,
777
+ body: options.body,
778
+ payload,
779
+ rawBody
780
+ }, GITHUB_EVENT_ID_HEADERS);
781
+ const replayError = await checkAndRecordReplay({
782
+ store: options.store,
783
+ eventId,
784
+ storeTtlSeconds: options.storeTtlSeconds,
785
+ toleranceSeconds: options.toleranceSeconds
786
+ });
787
+ if (replayError) return failVerification$3(rawBody, replayError, options.throwOnError, eventId);
788
+ return {
789
+ valid: true,
790
+ rawBody,
791
+ eventId,
792
+ payload
793
+ };
794
+ }
795
+ /**
796
+ * Asserts that a GitHub webhook payload signature is valid, throwing on verification failure.
797
+ *
798
+ * @param options - GitHub verification options.
799
+ * @returns Parsed and typed event payload.
800
+ * @throws {SignatureVerificationError} If signature verification fails.
801
+ * @throws {InvalidSignatureHeaderError} If header format is invalid or missing.
802
+ */
803
+ async function assertGithubWebhook(options) {
804
+ return (await verifyGithubWebhook({
805
+ ...options,
806
+ throwOnError: true
807
+ })).payload;
808
+ }
809
+ /**
810
+ * Webhook provider plugin for GitHub.
811
+ */
812
+ const githubPlugin = {
813
+ name: "github",
814
+ verify: verifyGithubWebhook
815
+ };
816
+ // src/providers/shopify.ts
817
+ const BASE64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
818
+ const SHOPIFY_EVENT_ID_HEADERS = [
819
+ "x-shopify-webhook-id",
820
+ "x-webhook-id",
821
+ "webhook-id"
822
+ ];
823
+ /**
824
+ * Asserts that a header value is a non-empty string.
825
+ *
826
+ * @param header - Candidate header value.
827
+ */
828
+ function assertNonEmptyHeader$1(header) {
829
+ if (typeof header !== "string" || !header.trim()) throw new InvalidSignatureHeaderError("Missing or empty X-Shopify-Hmac-Sha256 header");
830
+ }
831
+ /**
832
+ * Parses an `X-Shopify-Hmac-Sha256` header, validating and normalizing the Base64 signature.
833
+ *
834
+ * @param header - Raw `X-Shopify-Hmac-Sha256` Base64 header string.
835
+ * @returns Normalized Base64-encoded HMAC-SHA256 signature string.
836
+ * @throws {InvalidSignatureHeaderError} If header is missing, empty, or not valid Base64.
837
+ */
838
+ function parseShopifySignature(header) {
839
+ assertNonEmptyHeader$1(header);
840
+ const trimmed = header.trim();
841
+ if (!BASE64_REGEX.test(trimmed)) throw new InvalidSignatureHeaderError("Invalid X-Shopify-Hmac-Sha256 header format: expected Base64-encoded string");
842
+ return trimmed;
843
+ }
844
+ /**
845
+ * Case-insensitively extracts a header value from a `Headers` instance or plain record.
846
+ *
847
+ * @param headers - Web standard `Headers` instance or record.
848
+ * @param targetName - Lowercase target header name.
849
+ * @returns Header value string or undefined.
850
+ */
851
+ function extractHeaderValue$2(headers, targetName) {
852
+ if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(targetName) ?? void 0;
853
+ const record = headers;
854
+ const lowerTarget = targetName.toLowerCase();
855
+ for (const key of Object.keys(record)) if (key.toLowerCase() === lowerTarget) {
856
+ const value = record[key];
857
+ return Array.isArray(value) ? value[0] : value;
858
+ }
859
+ }
860
+ /**
861
+ * Resolves the raw signature header string from options, headers record, or request.
862
+ *
863
+ * @param options - Shopify verification options.
864
+ * @returns Shopify signature header string or undefined if not found.
865
+ */
866
+ function resolveSignatureHeader$1(options) {
867
+ if (options.signature) return options.signature;
868
+ if (options.headers) return extractHeaderValue$2(options.headers, "x-shopify-hmac-sha256");
869
+ if (typeof Request !== "undefined" && options.body instanceof Request) return options.body.headers.get("x-shopify-hmac-sha256") ?? void 0;
870
+ }
871
+ /**
872
+ * Validates preconditions for Shopify webhook verification.
873
+ *
874
+ * @param options - Shopify verification options.
875
+ * @param rawHeader - Resolved header string.
876
+ * @returns Error if preconditions fail, null otherwise.
877
+ */
878
+ function validatePreconditions$1(options, rawHeader) {
879
+ if (!options.secret) return new SignatureVerificationError("Missing Shopify webhook signing secret");
880
+ if (!rawHeader) return new InvalidSignatureHeaderError("Missing X-Shopify-Hmac-Sha256 header");
881
+ return null;
882
+ }
883
+ /**
884
+ * Safely parses the raw signature header without uncaught exceptions.
885
+ *
886
+ * @param rawHeader - Raw header string.
887
+ * @returns Extracted Base64 signature string or error object.
888
+ */
889
+ function safeParseHeader$1(rawHeader) {
890
+ try {
891
+ return { signature: parseShopifySignature(rawHeader) };
892
+ } catch (err) {
893
+ return { error: err instanceof WebhookKitError ? err : new InvalidSignatureHeaderError(String(err)) };
894
+ }
895
+ }
896
+ /**
897
+ * Constructs a failed WebhookVerificationResult or throws if throwOnError is set.
898
+ *
899
+ * @param rawBody - Raw body string.
900
+ * @param error - WebhookKitError instance.
901
+ * @param throwOnError - Optional flag to throw immediately.
902
+ * @returns Failed verification result.
903
+ */
904
+ function failVerification$2(rawBody, error, throwOnError, eventId) {
905
+ if (throwOnError) throw error;
906
+ return {
907
+ valid: false,
908
+ rawBody,
909
+ eventId,
910
+ error
911
+ };
912
+ }
913
+ /**
914
+ * Attempts to parse the raw body as JSON.
915
+ *
916
+ * @param rawBody - Raw body payload string.
917
+ * @returns Parsed payload or undefined.
918
+ */
919
+ function tryParseJson$2(rawBody) {
920
+ try {
921
+ return JSON.parse(rawBody);
922
+ } catch {
923
+ return;
924
+ }
925
+ }
926
+ /**
927
+ * Verifies a Shopify webhook payload signature against `X-Shopify-Hmac-Sha256`.
928
+ *
929
+ * Shopify computes HMAC-SHA256 of the raw body and encodes it as Base64.
930
+ *
931
+ * @param options - Shopify verification options.
932
+ * @returns Verification result with typed payload.
933
+ */
934
+ async function verifyShopifyWebhook(options) {
935
+ const rawBody = await extractRawBody(options.body);
936
+ const rawHeader = resolveSignatureHeader$1(options);
937
+ const preconditionError = validatePreconditions$1(options, rawHeader);
938
+ if (preconditionError) return failVerification$2(rawBody, preconditionError, options.throwOnError);
939
+ const parsed = safeParseHeader$1(rawHeader);
940
+ if ("error" in parsed) return failVerification$2(rawBody, parsed.error, options.throwOnError);
941
+ if (!timingSafeEqual(await computeSubtleHmac("SHA-256", options.secret, rawBody, "base64"), parsed.signature)) return failVerification$2(rawBody, new SignatureVerificationError("Shopify signature verification failed: signature does not match expected HMAC"), options.throwOnError);
942
+ const payload = options.parseJson !== false ? tryParseJson$2(rawBody) : void 0;
943
+ const eventId = resolveWebhookEventId({
944
+ eventId: options.eventId,
945
+ headers: options.headers,
946
+ body: options.body,
947
+ payload,
948
+ rawBody
949
+ }, SHOPIFY_EVENT_ID_HEADERS);
950
+ const replayError = await checkAndRecordReplay({
951
+ store: options.store,
952
+ eventId,
953
+ storeTtlSeconds: options.storeTtlSeconds,
954
+ toleranceSeconds: options.toleranceSeconds
955
+ });
956
+ if (replayError) return failVerification$2(rawBody, replayError, options.throwOnError, eventId);
957
+ return {
958
+ valid: true,
959
+ rawBody,
960
+ eventId,
961
+ payload
962
+ };
963
+ }
964
+ /**
965
+ * Asserts that a Shopify webhook payload signature is valid, throwing on verification failure.
966
+ *
967
+ * @param options - Shopify verification options.
968
+ * @returns Parsed and typed event payload.
969
+ * @throws {SignatureVerificationError} If signature verification fails.
970
+ * @throws {InvalidSignatureHeaderError} If header format is invalid or missing.
971
+ */
972
+ async function assertShopifyWebhook(options) {
973
+ return (await verifyShopifyWebhook({
974
+ ...options,
975
+ throwOnError: true
976
+ })).payload;
977
+ }
978
+ /**
979
+ * Webhook provider plugin for Shopify.
980
+ */
981
+ const shopifyPlugin = {
982
+ name: "shopify",
983
+ verify: verifyShopifyWebhook
984
+ };
985
+ // src/providers/stripe.ts
986
+ const STRIPE_EVENT_ID_HEADERS = [
987
+ "x-stripe-webhook-id",
988
+ "x-webhook-id",
989
+ "webhook-id"
990
+ ];
991
+ /**
992
+ * Case-insensitively extracts a header value from a `Headers` instance or plain record.
993
+ *
994
+ * @param headers - Web standard `Headers` instance or record.
995
+ * @param targetName - Lowercase target header name.
996
+ * @returns Header value string or undefined.
997
+ */
998
+ function extractHeaderValue$1(headers, targetName) {
999
+ if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(targetName) ?? void 0;
1000
+ const record = headers;
1001
+ const lowerTarget = targetName.toLowerCase();
1002
+ for (const key of Object.keys(record)) if (key.toLowerCase() === lowerTarget) {
1003
+ const value = record[key];
1004
+ return Array.isArray(value) ? value[0] : value;
1005
+ }
1006
+ }
1007
+ /**
1008
+ * Resolves the raw signature header string from options, headers record, or request.
1009
+ *
1010
+ * @param options - Stripe verification options.
1011
+ * @returns Stripe signature header string or undefined if not found.
1012
+ */
1013
+ function resolveSignatureHeader(options) {
1014
+ if (options.signature) return options.signature;
1015
+ if (options.headers) return extractHeaderValue$1(options.headers, "stripe-signature");
1016
+ if (typeof Request !== "undefined" && options.body instanceof Request) return options.body.headers.get("stripe-signature") ?? void 0;
1017
+ }
1018
+ /**
1019
+ * Splits a header tag pair into key and value.
1020
+ *
1021
+ * @param item - Header entry (e.g. `t=1492774577` or `v1=...`).
1022
+ * @returns Key-value tuple or null if malformed.
1023
+ */
1024
+ function parseKeyValue(item) {
1025
+ const trimmed = item.trim();
1026
+ const eqIndex = trimmed.indexOf("=");
1027
+ if (eqIndex === -1) return null;
1028
+ return [trimmed.slice(0, eqIndex).trim(), trimmed.slice(eqIndex + 1).trim()];
1029
+ }
1030
+ /**
1031
+ * Processes a single parsed tag from the Stripe signature header into accumulator state.
1032
+ *
1033
+ * @param key - Tag key (e.g. `'t'`, `'v1'`, `'v0'`).
1034
+ * @param value - Tag value string.
1035
+ * @param state - Mutable parsing state.
1036
+ */
1037
+ function processHeaderTag(key, value, state) {
1038
+ if (!value) return;
1039
+ if (key === "v1") {
1040
+ state.signatures.push(value);
1041
+ return;
1042
+ }
1043
+ if (key === "v0") {
1044
+ state.v0Signatures.push(value);
1045
+ return;
1046
+ }
1047
+ if (key === "t") {
1048
+ const parsed = parseInt(value, 10);
1049
+ if (Number.isInteger(parsed) && parsed > 0) state.timestamp = parsed;
1050
+ }
1051
+ }
1052
+ /**
1053
+ * Asserts that a header value is a non-empty string.
1054
+ *
1055
+ * @param header - Candidate header value.
1056
+ */
1057
+ function assertNonEmptyHeader(header) {
1058
+ if (typeof header !== "string" || !header.trim()) throw new InvalidSignatureHeaderError("Missing or empty Stripe-Signature header");
1059
+ }
1060
+ /**
1061
+ * Collects timestamp and signature schemes across comma-separated header entries.
1062
+ *
1063
+ * @param header - Non-empty header string.
1064
+ * @returns Parsed mutable state accumulator.
1065
+ */
1066
+ function collectHeaderState(header) {
1067
+ const state = {
1068
+ signatures: [],
1069
+ v0Signatures: []
1070
+ };
1071
+ const items = header.split(",");
1072
+ for (const item of items) {
1073
+ const pair = parseKeyValue(item);
1074
+ if (pair) processHeaderTag(pair[0], pair[1], state);
1075
+ }
1076
+ return state;
1077
+ }
1078
+ /**
1079
+ * Validates that parsed header state satisfies Stripe protocol requirements.
1080
+ *
1081
+ * @param timestamp - Extracted Unix timestamp.
1082
+ * @param signaturesCount - Count of valid v1 signatures.
1083
+ */
1084
+ function validateParsedState(timestamp, signaturesCount) {
1085
+ if (timestamp === void 0) throw new InvalidSignatureHeaderError("Stripe-Signature header missing or invalid timestamp (t=...)");
1086
+ if (!signaturesCount) throw new InvalidSignatureHeaderError("Stripe-Signature header missing valid v1 signature scheme (v1=...)");
1087
+ }
1088
+ /**
1089
+ * Parses a Stripe signature header string (`t=...,v1=...,v0=...`).
1090
+ *
1091
+ * @param header - Raw `Stripe-Signature` header string.
1092
+ * @returns Parsed header components including timestamp and signatures.
1093
+ * @throws {InvalidSignatureHeaderError} If header format is invalid or missing required tags.
1094
+ */
1095
+ function parseStripeSignature(header) {
1096
+ assertNonEmptyHeader(header);
1097
+ const state = collectHeaderState(header);
1098
+ validateParsedState(state.timestamp, state.signatures.length);
1099
+ return {
1100
+ timestamp: state.timestamp,
1101
+ signatures: state.signatures,
1102
+ v0Signatures: state.v0Signatures.length ? state.v0Signatures : void 0
1103
+ };
1104
+ }
1105
+ /**
1106
+ * Validates that a webhook timestamp falls within the allowed tolerance window.
1107
+ *
1108
+ * @param timestamp - Unix timestamp in seconds from the signature header.
1109
+ * @param toleranceSeconds - Maximum allowed drift/age in seconds (default: 300s). Pass 0 to disable.
1110
+ * @param currentTimestamp - Optional current Unix timestamp in seconds (defaults to now).
1111
+ * @throws {TimestampToleranceError} If timestamp falls outside the tolerance window.
1112
+ */
1113
+ function validateTimestampTolerance(timestamp, toleranceSeconds = 300, currentTimestamp) {
1114
+ if (toleranceSeconds <= 0) return;
1115
+ const now = currentTimestamp ?? Math.floor(Date.now() / 1e3);
1116
+ if (Math.abs(now - timestamp) > toleranceSeconds) throw new TimestampToleranceError(timestamp, toleranceSeconds);
1117
+ }
1118
+ /**
1119
+ * Compares an expected HMAC hex signature against candidate signatures in constant time.
1120
+ *
1121
+ * @param expectedSignature - Computed hex signature.
1122
+ * @param candidates - List of candidate hex signatures from the header.
1123
+ * @returns `true` if any candidate matches the expected signature.
1124
+ */
1125
+ function matchesAnySignature(expectedSignature, candidates) {
1126
+ let matched = false;
1127
+ for (const candidate of candidates) if (timingSafeEqual(expectedSignature, candidate)) matched = true;
1128
+ return matched;
1129
+ }
1130
+ /**
1131
+ * Attempts to parse the raw body as JSON.
1132
+ *
1133
+ * @param rawBody - Raw body payload string.
1134
+ * @returns Parsed payload or undefined.
1135
+ */
1136
+ function tryParseJson$1(rawBody) {
1137
+ try {
1138
+ return JSON.parse(rawBody);
1139
+ } catch {
1140
+ return;
1141
+ }
1142
+ }
1143
+ /**
1144
+ * Validates preconditions for Stripe webhook verification.
1145
+ *
1146
+ * @param options - Stripe verification options.
1147
+ * @param rawHeader - Resolved header string.
1148
+ * @returns Error if preconditions fail, null otherwise.
1149
+ */
1150
+ function validatePreconditions(options, rawHeader) {
1151
+ if (!options.secret) return new SignatureVerificationError("Missing Stripe webhook signing secret");
1152
+ if (!rawHeader) return new InvalidSignatureHeaderError("Missing Stripe-Signature header");
1153
+ return null;
1154
+ }
1155
+ /**
1156
+ * Safely parses the raw signature header without uncaught exceptions.
1157
+ *
1158
+ * @param rawHeader - Raw header string.
1159
+ * @returns Parsed header or error object.
1160
+ */
1161
+ function safeParseHeader(rawHeader) {
1162
+ try {
1163
+ return parseStripeSignature(rawHeader);
1164
+ } catch (err) {
1165
+ return { error: err instanceof WebhookKitError ? err : new InvalidSignatureHeaderError(String(err)) };
1166
+ }
1167
+ }
1168
+ /**
1169
+ * Checks timestamp tolerance, returning an error if tolerance window is exceeded.
1170
+ *
1171
+ * @param timestamp - Unix timestamp in seconds.
1172
+ * @param options - Stripe verification options.
1173
+ * @returns TimestampToleranceError if exceeded, null otherwise.
1174
+ */
1175
+ function checkTolerance(timestamp, options) {
1176
+ const tolerance = options.toleranceSeconds ?? 300;
1177
+ try {
1178
+ validateTimestampTolerance(timestamp, tolerance, options.currentTimestamp);
1179
+ return null;
1180
+ } catch (err) {
1181
+ return err instanceof TimestampToleranceError ? err : new TimestampToleranceError(timestamp, tolerance);
1182
+ }
1183
+ }
1184
+ /**
1185
+ * Constructs a failed WebhookVerificationResult or throws if throwOnError is set.
1186
+ *
1187
+ * @param rawBody - Raw body string.
1188
+ * @param error - WebhookKitError instance.
1189
+ * @param throwOnError - Optional flag to throw immediately.
1190
+ * @param timestamp - Optional parsed timestamp.
1191
+ * @returns Failed verification result.
1192
+ */
1193
+ function failVerification$1(rawBody, error, throwOnError, timestamp, eventId) {
1194
+ if (throwOnError) throw error;
1195
+ return {
1196
+ valid: false,
1197
+ rawBody,
1198
+ timestamp,
1199
+ eventId,
1200
+ error
1201
+ };
1202
+ }
1203
+ /**
1204
+ * Verifies a Stripe webhook event signature.
1205
+ *
1206
+ * Stripe computes HMAC-SHA256 over `${timestamp}.${rawPayload}` and compares
1207
+ * against the `v1` signature tag in `Stripe-Signature`.
1208
+ *
1209
+ * @param options - Stripe verification options
1210
+ * @returns Verification result with typed payload
1211
+ */
1212
+ async function verifyStripeWebhook(options) {
1213
+ const rawBody = await extractRawBody(options.body);
1214
+ const rawHeader = resolveSignatureHeader(options);
1215
+ const preconditionError = validatePreconditions(options, rawHeader);
1216
+ if (preconditionError) return failVerification$1(rawBody, preconditionError, options.throwOnError);
1217
+ const parsedHeader = safeParseHeader(rawHeader);
1218
+ if ("error" in parsedHeader) return failVerification$1(rawBody, parsedHeader.error, options.throwOnError);
1219
+ const { timestamp, signatures } = parsedHeader;
1220
+ const toleranceError = checkTolerance(timestamp, options);
1221
+ if (toleranceError) return failVerification$1(rawBody, toleranceError, options.throwOnError, timestamp);
1222
+ const signedPayload = `${timestamp}.${rawBody}`;
1223
+ if (!matchesAnySignature(await computeSubtleHmac("SHA-256", options.secret, signedPayload, "hex"), signatures)) return failVerification$1(rawBody, new SignatureVerificationError("Stripe signature verification failed: signature does not match expected HMAC"), options.throwOnError, timestamp);
1224
+ const payload = options.parseJson !== false ? tryParseJson$1(rawBody) : void 0;
1225
+ const eventId = resolveWebhookEventId({
1226
+ eventId: options.eventId,
1227
+ headers: options.headers,
1228
+ body: options.body,
1229
+ payload,
1230
+ rawBody
1231
+ }, STRIPE_EVENT_ID_HEADERS);
1232
+ const replayError = await checkAndRecordReplay({
1233
+ store: options.store,
1234
+ eventId,
1235
+ storeTtlSeconds: options.storeTtlSeconds,
1236
+ toleranceSeconds: options.toleranceSeconds
1237
+ });
1238
+ if (replayError) return failVerification$1(rawBody, replayError, options.throwOnError, timestamp, eventId);
1239
+ return {
1240
+ valid: true,
1241
+ rawBody,
1242
+ timestamp,
1243
+ eventId,
1244
+ payload
1245
+ };
1246
+ }
1247
+ /**
1248
+ * Asserts that a Stripe webhook event signature is valid, throwing on verification failure.
1249
+ *
1250
+ * @param options - Stripe verification options.
1251
+ * @returns Parsed and typed event payload.
1252
+ * @throws {SignatureVerificationError} If signature verification fails.
1253
+ * @throws {TimestampToleranceError} If timestamp falls outside the tolerance window.
1254
+ * @throws {InvalidSignatureHeaderError} If header format is invalid or missing.
1255
+ */
1256
+ async function assertStripeWebhook(options) {
1257
+ return (await verifyStripeWebhook({
1258
+ ...options,
1259
+ throwOnError: true
1260
+ })).payload;
1261
+ }
1262
+ /**
1263
+ * Webhook provider plugin for Stripe.
1264
+ */
1265
+ const stripePlugin = {
1266
+ name: "stripe",
1267
+ verify: verifyStripeWebhook
1268
+ };
1269
+ // src/verify.ts
1270
+ const GENERIC_SIGNATURE_HEADERS = [
1271
+ "x-webhook-signature",
1272
+ "x-signature",
1273
+ "x-hub-signature-256",
1274
+ "stripe-signature",
1275
+ "x-shopify-hmac-sha256"
1276
+ ];
1277
+ const GENERIC_TIMESTAMP_HEADERS = [
1278
+ "x-webhook-timestamp",
1279
+ "x-timestamp",
1280
+ "webhook-timestamp"
1281
+ ];
1282
+ const HEX_SCHEME_PREFIX_REGEX = /^(?:sha-?(?:256|384|512|1)|v1)=/i;
1283
+ /**
1284
+ * Built-in provider plugin registry keyed by lowercase provider name.
1285
+ */
1286
+ const PROVIDER_REGISTRY = {
1287
+ stripe: stripePlugin,
1288
+ github: githubPlugin,
1289
+ shopify: shopifyPlugin
1290
+ };
1291
+ /**
1292
+ * Case-insensitively extracts a single header value from a `Headers` instance or plain record.
1293
+ *
1294
+ * @param headers - Web standard `Headers` instance or record.
1295
+ * @param targetName - Lowercase target header name.
1296
+ * @returns Header value string or undefined.
1297
+ */
1298
+ function extractHeaderValue(headers, targetName) {
1299
+ if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(targetName) ?? void 0;
1300
+ const record = headers;
1301
+ const lowerTarget = targetName.toLowerCase();
1302
+ for (const key of Object.keys(record)) if (key.toLowerCase() === lowerTarget) {
1303
+ const value = record[key];
1304
+ return Array.isArray(value) ? value[0] : value;
1305
+ }
1306
+ }
1307
+ /**
1308
+ * Searches a list of candidate header names in a `Headers` or record object.
1309
+ *
1310
+ * @param headers - Headers source.
1311
+ * @param candidateNames - Ordered list of lowercase header names to check.
1312
+ * @returns First matching non-empty header value or undefined.
1313
+ */
1314
+ function findFirstHeader(headers, candidateNames) {
1315
+ if (!headers) return;
1316
+ for (const name of candidateNames) {
1317
+ const val = extractHeaderValue(headers, name);
1318
+ if (val) return val;
1319
+ }
1320
+ }
1321
+ /**
1322
+ * Resolves the signature string from explicit options, headers record, or a `Request` body.
1323
+ *
1324
+ * @param options - Verification options.
1325
+ * @returns Resolved signature string or undefined.
1326
+ */
1327
+ function resolveGenericSignature(options) {
1328
+ if (options.signature) return options.signature;
1329
+ const fromHeaders = findFirstHeader(options.headers, GENERIC_SIGNATURE_HEADERS);
1330
+ if (fromHeaders) return fromHeaders;
1331
+ if (typeof Request !== "undefined" && options.body instanceof Request) return findFirstHeader(options.body.headers, GENERIC_SIGNATURE_HEADERS);
1332
+ }
1333
+ /**
1334
+ * Resolves an optional timestamp value from explicit options, headers record, or a `Request` body.
1335
+ *
1336
+ * @param options - Verification options.
1337
+ * @returns Raw timestamp value or undefined.
1338
+ */
1339
+ function resolveRawTimestamp(options) {
1340
+ if (options.timestamp !== void 0) return options.timestamp;
1341
+ const fromHeaders = findFirstHeader(options.headers, GENERIC_TIMESTAMP_HEADERS);
1342
+ if (fromHeaders !== void 0) return fromHeaders;
1343
+ if (typeof Request !== "undefined" && options.body instanceof Request) return findFirstHeader(options.body.headers, GENERIC_TIMESTAMP_HEADERS);
1344
+ }
1345
+ /**
1346
+ * Parses a raw timestamp number or string into a positive integer Unix timestamp.
1347
+ *
1348
+ * @param rawTimestamp - Raw timestamp from options or headers.
1349
+ * @returns Positive integer timestamp or `null` if invalid.
1350
+ */
1351
+ function parseTimestampValue(rawTimestamp) {
1352
+ const parsed = typeof rawTimestamp === "number" ? rawTimestamp : parseInt(String(rawTimestamp).trim(), 10);
1353
+ if (!Number.isInteger(parsed) || parsed <= 0) return null;
1354
+ return parsed;
1355
+ }
1356
+ /**
1357
+ * Parses and validates an optional timestamp against the configured tolerance window.
1358
+ *
1359
+ * @param options - Verification options.
1360
+ * @returns Parsed timestamp or a `TimestampToleranceError`.
1361
+ */
1362
+ function verifyGenericTimestamp(options) {
1363
+ const rawTimestamp = resolveRawTimestamp(options);
1364
+ if (rawTimestamp === void 0) return {};
1365
+ const parsed = parseTimestampValue(rawTimestamp);
1366
+ const tolerance = options.toleranceSeconds ?? 300;
1367
+ if (parsed === null) return { error: new TimestampToleranceError(0, tolerance, "Invalid webhook timestamp header or option") };
1368
+ try {
1369
+ validateTimestampTolerance(parsed, tolerance, options.currentTimestamp);
1370
+ return { timestamp: parsed };
1371
+ } catch (err) {
1372
+ return { error: err instanceof TimestampToleranceError ? err : new TimestampToleranceError(parsed, tolerance) };
1373
+ }
1374
+ }
1375
+ /**
1376
+ * Checks whether a secret string or byte array is non-empty.
1377
+ *
1378
+ * @param secret - Candidate secret key.
1379
+ * @returns `true` if secret is present and non-empty.
1380
+ */
1381
+ function hasSecret(secret) {
1382
+ if (!secret) return false;
1383
+ return typeof secret === "string" ? secret.length > 0 : secret.byteLength > 0;
1384
+ }
1385
+ /**
1386
+ * Normalizes a candidate signature string according to the target encoding.
1387
+ *
1388
+ * @param signature - Raw signature string.
1389
+ * @param encoding - Digest encoding (`'hex'` or `'base64'`).
1390
+ * @returns Normalized signature string for constant-time comparison.
1391
+ */
1392
+ function normalizeCandidateSignature(signature, encoding) {
1393
+ const trimmed = signature.trim();
1394
+ if (encoding === "base64") return trimmed;
1395
+ return trimmed.replace(HEX_SCHEME_PREFIX_REGEX, "").trim().toLowerCase();
1396
+ }
1397
+ /**
1398
+ * Computes a normalized HMAC digest string for comparison.
1399
+ *
1400
+ * @param options - Verification options.
1401
+ * @param data - Signing payload string.
1402
+ * @param encoding - Digest encoding (`'hex'` or `'base64'`).
1403
+ * @returns Normalized digest string.
1404
+ */
1405
+ async function computeNormalizedDigest(options, data, encoding) {
1406
+ const digest = await computeSubtleHmac(options.algorithm ?? "SHA-256", options.secret, data, encoding);
1407
+ return encoding === "hex" ? digest.toLowerCase() : digest;
1408
+ }
1409
+ /**
1410
+ * Compares candidate signature against HMAC of rawBody (and `${timestamp}.${rawBody}` if timestamped).
1411
+ *
1412
+ * @param options - Verification options.
1413
+ * @param rawBody - Extracted raw body string.
1414
+ * @param rawSignature - Raw signature header or option.
1415
+ * @param timestamp - Optional validated Unix timestamp.
1416
+ * @returns `true` if signature matches expected HMAC.
1417
+ */
1418
+ async function matchesGenericHmac(options, rawBody, rawSignature, timestamp) {
1419
+ const encoding = options.encoding ?? "hex";
1420
+ const candidate = normalizeCandidateSignature(rawSignature, encoding);
1421
+ if (!candidate) return false;
1422
+ let matched = timingSafeEqual(await computeNormalizedDigest(options, rawBody, encoding), candidate);
1423
+ if (timestamp !== void 0) {
1424
+ if (timingSafeEqual(await computeNormalizedDigest(options, `${timestamp}.${rawBody}`, encoding), candidate)) matched = true;
1425
+ }
1426
+ return matched;
1427
+ }
1428
+ /**
1429
+ * Constructs a failed `WebhookVerificationResult` or throws if `throwOnError` is enabled.
1430
+ *
1431
+ * @param rawBody - Extracted raw body string.
1432
+ * @param error - `WebhookKitError` instance.
1433
+ * @param throwOnError - Whether to throw the error immediately.
1434
+ * @param timestamp - Optional parsed timestamp.
1435
+ * @returns Failed verification result.
1436
+ */
1437
+ function failVerification(rawBody, error, throwOnError, timestamp, eventId) {
1438
+ if (throwOnError) throw error;
1439
+ return {
1440
+ valid: false,
1441
+ rawBody,
1442
+ timestamp,
1443
+ eventId,
1444
+ error
1445
+ };
1446
+ }
1447
+ /**
1448
+ * Safely parses a raw body string as JSON, returning `undefined` on syntax errors.
1449
+ *
1450
+ * @param rawBody - Raw body string.
1451
+ * @returns Parsed JSON value or `undefined`.
1452
+ */
1453
+ function tryParseJson(rawBody) {
1454
+ try {
1455
+ return JSON.parse(rawBody);
1456
+ } catch {
1457
+ return;
1458
+ }
1459
+ }
1460
+ /**
1461
+ * Executes a `WebhookProviderPlugin` and honors `options.throwOnError` if verification fails.
1462
+ *
1463
+ * @param plugin - Target webhook provider plugin.
1464
+ * @param options - Verification options.
1465
+ * @param enforceStoreAfterPlugin - Whether to run replay store enforcement after custom plugin verification.
1466
+ * @returns Provider verification result.
1467
+ */
1468
+ async function runPluginVerification(plugin, options, enforceStoreAfterPlugin = false) {
1469
+ const result = await plugin.verify(options);
1470
+ if (!result.valid) {
1471
+ if (options.throwOnError) throw result.error;
1472
+ return result;
1473
+ }
1474
+ if (!enforceStoreAfterPlugin) return result;
1475
+ const eventId = result.eventId ?? resolveWebhookEventId({
1476
+ eventId: options.eventId,
1477
+ headers: options.headers,
1478
+ body: options.body,
1479
+ payload: result.payload,
1480
+ rawBody: result.rawBody
1481
+ });
1482
+ const replayError = await checkAndRecordReplay({
1483
+ store: options.store,
1484
+ eventId,
1485
+ storeTtlSeconds: options.storeTtlSeconds,
1486
+ toleranceSeconds: options.toleranceSeconds
1487
+ });
1488
+ if (replayError) return failVerification(result.rawBody, replayError, options.throwOnError, result.timestamp, eventId);
1489
+ return {
1490
+ ...result,
1491
+ eventId
1492
+ };
1493
+ }
1494
+ /**
1495
+ * Delegates verification to a named built-in provider or custom `WebhookProviderPlugin`.
1496
+ *
1497
+ * @param options - Verification options containing `provider`.
1498
+ * @returns Provider verification result.
1499
+ */
1500
+ async function delegateToProvider(options) {
1501
+ const provider = options.provider;
1502
+ if (typeof provider === "object") return runPluginVerification(provider, options, true);
1503
+ const normalizedName = String(provider).toLowerCase().trim();
1504
+ const builtin = PROVIDER_REGISTRY[normalizedName];
1505
+ if (!builtin) return failVerification(await extractRawBody(options.body), new WebhookKitError(`Unsupported webhook provider: "${String(provider)}"`, "UNSUPPORTED_PROVIDER"), options.throwOnError);
1506
+ return runPluginVerification(builtin, options);
1507
+ }
1508
+ /**
1509
+ * Verifies a generic HMAC webhook signature when no specific provider plugin is selected.
1510
+ *
1511
+ * @param options - Generic verification options.
1512
+ * @returns Verification outcome with typed payload.
1513
+ */
1514
+ async function verifyGenericWebhook(options) {
1515
+ const rawBody = await extractRawBody(options.body);
1516
+ const rawSignature = resolveGenericSignature(options);
1517
+ if (!hasSecret(options.secret) || !rawSignature) return failVerification(rawBody, new SignatureVerificationError("Missing signature or secret"), options.throwOnError);
1518
+ const tsResult = verifyGenericTimestamp(options);
1519
+ if (tsResult.error) return failVerification(rawBody, tsResult.error, options.throwOnError);
1520
+ if (!await matchesGenericHmac(options, rawBody, rawSignature, tsResult.timestamp)) return failVerification(rawBody, new SignatureVerificationError(), options.throwOnError, tsResult.timestamp);
1521
+ const payload = options.parseJson !== false ? tryParseJson(rawBody) : void 0;
1522
+ const eventId = resolveWebhookEventId({
1523
+ eventId: options.eventId,
1524
+ headers: options.headers,
1525
+ body: options.body,
1526
+ payload,
1527
+ rawBody
1528
+ });
1529
+ const replayError = await checkAndRecordReplay({
1530
+ store: options.store,
1531
+ eventId,
1532
+ storeTtlSeconds: options.storeTtlSeconds,
1533
+ toleranceSeconds: options.toleranceSeconds
1534
+ });
1535
+ if (replayError) return failVerification(rawBody, replayError, options.throwOnError, tsResult.timestamp, eventId);
1536
+ return {
1537
+ valid: true,
1538
+ rawBody,
1539
+ timestamp: tsResult.timestamp,
1540
+ eventId,
1541
+ payload
1542
+ };
1543
+ }
1544
+ async function verifyWebhookSignature(options) {
1545
+ if (options.provider !== void 0) return delegateToProvider(options);
1546
+ return verifyGenericWebhook(options);
1547
+ }
1548
+ async function assertWebhookSignature(options) {
1549
+ const result = await verifyWebhookSignature({
1550
+ ...options,
1551
+ throwOnError: true
1552
+ });
1553
+ if (!result.valid) throw result.error;
1554
+ return result.payload;
1555
+ }
1556
+ // src/handler.ts
1557
+ const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
1558
+ const DEFAULT_SUCCESS_BODY = JSON.stringify({ received: true });
1559
+ /**
1560
+ * Creates a JSON `Response` with UTF-8 content-type headers.
1561
+ *
1562
+ * @param body - Serialized JSON string.
1563
+ * @param status - HTTP status code.
1564
+ * @returns Web Standard `Response`.
1565
+ */
1566
+ function createJsonResponse(body, status) {
1567
+ return new Response(body, {
1568
+ status,
1569
+ headers: { "content-type": JSON_CONTENT_TYPE }
1570
+ });
1571
+ }
1572
+ /**
1573
+ * Resolves the appropriate HTTP status code for a `WebhookKitError`.
1574
+ *
1575
+ * @param error - Verification, replay, or extraction error.
1576
+ * @returns `400` for extraction errors, `409` for duplicate event replays, or `401` for verification errors.
1577
+ */
1578
+ function resolveErrorStatus(error) {
1579
+ if (error instanceof PayloadExtractionError) return 400;
1580
+ if (error instanceof ReplayAttackError) return 409;
1581
+ return 401;
1582
+ }
1583
+ /**
1584
+ * Converts a `WebhookKitError` into a structured JSON HTTP error `Response`.
1585
+ *
1586
+ * @param error - Verification or extraction error.
1587
+ * @returns `400`, `409`, or `401` JSON error response.
1588
+ */
1589
+ function createErrorResponse(error) {
1590
+ const status = resolveErrorStatus(error);
1591
+ return createJsonResponse(JSON.stringify({ error: {
1592
+ code: error.code,
1593
+ message: error.message
1594
+ } }), status);
1595
+ }
1596
+ /**
1597
+ * Checks whether an unknown value is a non-null object record.
1598
+ *
1599
+ * @param value - Candidate value.
1600
+ * @returns `true` if `value` is a non-null object.
1601
+ */
1602
+ function isRecord(value) {
1603
+ return typeof value === "object" && value !== null;
1604
+ }
1605
+ /**
1606
+ * Extracts a Web Standard `Request` from a framework context object (`{ req: { raw: Request } }`).
1607
+ *
1608
+ * @param value - Candidate context object.
1609
+ * @returns Extracted `Request` or `null` if not present.
1610
+ */
1611
+ function extractRequestFromContext(value) {
1612
+ if (!isRecord(value) || !isRecord(value.req)) return null;
1613
+ return typeof Request !== "undefined" && value.req.raw instanceof Request ? value.req.raw : null;
1614
+ }
1615
+ /**
1616
+ * Unwraps a Web Standard `Request` from either a direct `Request` or a `WebhookLikeContext`.
1617
+ *
1618
+ * @param input - Handler first argument.
1619
+ * @returns Underlying `Request` or `null` if invalid.
1620
+ */
1621
+ function unwrapRequest(input) {
1622
+ if (typeof Request !== "undefined" && input instanceof Request) return input;
1623
+ return extractRequestFromContext(input);
1624
+ }
1625
+ /**
1626
+ * Resolves the signing secret from a static value or dynamic secret resolver function.
1627
+ *
1628
+ * @param secret - Static secret or resolver function.
1629
+ * @param context - Original handler input (`Request` or framework context).
1630
+ * @param request - Unwrapped Web Standard `Request`.
1631
+ * @returns Resolved secret string or byte array.
1632
+ */
1633
+ async function resolveSecret(secret, context, request) {
1634
+ if (typeof secret === "function") return secret(context, request);
1635
+ return secret;
1636
+ }
1637
+ /**
1638
+ * Resolves the replay store from a static store object or dynamic resolver function.
1639
+ *
1640
+ * @param store - Static replay store or resolver function.
1641
+ * @param context - Original handler input (`Request` or framework context).
1642
+ * @param request - Unwrapped Web Standard `Request`.
1643
+ * @returns Resolved `WebhookReplayStore` or `undefined`.
1644
+ */
1645
+ async function resolveStore(store, context, request) {
1646
+ if (typeof store === "function") return store(context, request);
1647
+ return store;
1648
+ }
1649
+ /**
1650
+ * Converts the return value of `onEvent` into a Web Standard `Response`.
1651
+ *
1652
+ * @param output - Return value from `onEvent`.
1653
+ * @returns Web Standard `Response`.
1654
+ */
1655
+ function formatSuccessResponse(output) {
1656
+ if (typeof Response !== "undefined" && output instanceof Response) return output;
1657
+ if (output === void 0) return createJsonResponse(DEFAULT_SUCCESS_BODY, 200);
1658
+ return createJsonResponse(JSON.stringify(output), 200);
1659
+ }
1660
+ /**
1661
+ * Produces an error `Response` via `options.onError` if configured, or the default JSON responder.
1662
+ *
1663
+ * @param error - `WebhookKitError` instance.
1664
+ * @param meta - Error metadata including context, request, rawBody, and timestamp.
1665
+ * @param onError - Optional user-supplied error callback.
1666
+ * @returns Error `Response`.
1667
+ */
1668
+ async function handleVerificationFailure(error, meta, onError) {
1669
+ if (onError) return onError(error, meta);
1670
+ return createErrorResponse(error);
1671
+ }
1672
+ /**
1673
+ * Resolves the secret and executes `verifyWebhookSignature`, catching `WebhookKitError` exceptions.
1674
+ *
1675
+ * @param options - Handler configuration options.
1676
+ * @param context - Handler input context.
1677
+ * @param request - Unwrapped Web Standard `Request`.
1678
+ * @returns Discriminated verification outcome for handler dispatch.
1679
+ */
1680
+ async function verifyRequest(options, context, request) {
1681
+ try {
1682
+ const secret = await resolveSecret(options.secret, context, request);
1683
+ const store = await resolveStore(options.store, context, request);
1684
+ const result = await verifyWebhookSignature({
1685
+ body: request,
1686
+ secret,
1687
+ provider: options.provider,
1688
+ algorithm: options.algorithm,
1689
+ encoding: options.encoding,
1690
+ toleranceSeconds: options.toleranceSeconds,
1691
+ currentTimestamp: options.currentTimestamp,
1692
+ store,
1693
+ eventId: options.eventId,
1694
+ storeTtlSeconds: options.storeTtlSeconds,
1695
+ parseJson: options.parseJson
1696
+ });
1697
+ if (!result.valid) return {
1698
+ ok: false,
1699
+ error: result.error,
1700
+ rawBody: result.rawBody,
1701
+ timestamp: result.timestamp,
1702
+ eventId: result.eventId
1703
+ };
1704
+ return {
1705
+ ok: true,
1706
+ result
1707
+ };
1708
+ } catch (err) {
1709
+ if (err instanceof WebhookKitError) return {
1710
+ ok: false,
1711
+ error: err
1712
+ };
1713
+ throw err;
1714
+ }
1715
+ }
1716
+ function createWebhookHandler(options) {
1717
+ return async (input) => {
1718
+ const request = unwrapRequest(input);
1719
+ if (!request) return createErrorResponse(new PayloadExtractionError("Invalid webhook handler input: expected Web Standard Request or context with { req: { raw: Request } }"));
1720
+ const outcome = await verifyRequest(options, input, request);
1721
+ if (!outcome.ok) return handleVerificationFailure(outcome.error, {
1722
+ context: input,
1723
+ request,
1724
+ rawBody: outcome.rawBody,
1725
+ timestamp: outcome.timestamp,
1726
+ eventId: outcome.eventId
1727
+ }, options.onError);
1728
+ return formatSuccessResponse(await options.onEvent(outcome.result.payload, {
1729
+ context: input,
1730
+ request,
1731
+ rawBody: outcome.result.rawBody,
1732
+ timestamp: outcome.result.timestamp,
1733
+ eventId: outcome.result.eventId
1734
+ }));
1735
+ };
1736
+ }
1737
+ exports.CryptoKeyLruCache = CryptoKeyLruCache;
1738
+ exports.InMemoryReplayStore = InMemoryReplayStore;
1739
+ exports.InvalidSignatureHeaderError = InvalidSignatureHeaderError;
1740
+ exports.PROVIDER_REGISTRY = PROVIDER_REGISTRY;
1741
+ exports.PayloadExtractionError = PayloadExtractionError;
1742
+ exports.ReplayAttackError = ReplayAttackError;
1743
+ exports.SignatureVerificationError = SignatureVerificationError;
1744
+ exports.TimestampToleranceError = TimestampToleranceError;
1745
+ exports.WebhookKitError = WebhookKitError;
1746
+ exports.assertGithubWebhook = assertGithubWebhook;
1747
+ exports.assertShopifyWebhook = assertShopifyWebhook;
1748
+ exports.assertStripeWebhook = assertStripeWebhook;
1749
+ exports.assertWebhookSignature = assertWebhookSignature;
1750
+ exports.base64ToBytes = base64ToBytes;
1751
+ exports.bytesToBase64 = bytesToBase64;
1752
+ exports.bytesToHex = bytesToHex;
1753
+ exports.checkAndRecordReplay = checkAndRecordReplay;
1754
+ exports.clearCryptoKeyCache = clearCryptoKeyCache;
1755
+ exports.computeSubtleHmac = computeSubtleHmac;
1756
+ exports.createInMemoryReplayStore = createInMemoryReplayStore;
1757
+ exports.createWebhookHandler = createWebhookHandler;
1758
+ exports.extractRawBody = extractRawBody;
1759
+ exports.getCachedCryptoKey = getCachedCryptoKey;
1760
+ exports.getCryptoKeyCacheSize = getCryptoKeyCacheSize;
1761
+ exports.githubPlugin = githubPlugin;
1762
+ exports.hexToBytes = hexToBytes;
1763
+ exports.parseGithubSignature = parseGithubSignature;
1764
+ exports.parseShopifySignature = parseShopifySignature;
1765
+ exports.parseStripeSignature = parseStripeSignature;
1766
+ exports.resolveWebhookEventId = resolveWebhookEventId;
1767
+ exports.shopifyPlugin = shopifyPlugin;
1768
+ exports.stripePlugin = stripePlugin;
1769
+ exports.timingSafeEqual = timingSafeEqual;
1770
+ exports.validateTimestampTolerance = validateTimestampTolerance;
1771
+ exports.verifyGithubWebhook = verifyGithubWebhook;
1772
+ exports.verifyShopifyWebhook = verifyShopifyWebhook;
1773
+ exports.verifyStripeWebhook = verifyStripeWebhook;
1774
+ exports.verifyWebhookSignature = verifyWebhookSignature;