@amirabaris/webhook-kit 0.0.0-stage → 0.1.2

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