@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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["assertNonEmptyHeader","extractHeaderValue","resolveSignatureHeader","validatePreconditions","safeParseHeader","failVerification","tryParseJson","assertNonEmptyHeader","extractHeaderValue","resolveSignatureHeader","validatePreconditions","safeParseHeader","failVerification","tryParseJson","extractHeaderValue","tryParseJson","failVerification"],"sources":["../src/errors.ts","../src/store.ts","../src/crypto/subtle.ts","../src/utils/extract-body.ts","../src/providers/github.ts","../src/providers/shopify.ts","../src/providers/stripe.ts","../src/verify.ts","../src/handler.ts"],"sourcesContent":["/**\n * Base error class for all webhook-kit errors.\n */\nexport class WebhookKitError extends Error {\n public readonly code: string;\n\n constructor(message: string, code = \"WEBHOOK_KIT_ERROR\") {\n super(message);\n this.name = \"WebhookKitError\";\n this.code = code;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/**\n * Thrown or returned when signature verification fails (e.g. invalid HMAC or mismatched hash).\n */\nexport class SignatureVerificationError extends WebhookKitError {\n constructor(\n message = \"Signature verification failed: signature does not match expected HMAC\",\n ) {\n super(message, \"SIGNATURE_VERIFICATION_FAILED\");\n this.name = \"SignatureVerificationError\";\n }\n}\n\n/**\n * Thrown when the signature header is missing or malformed.\n */\nexport class InvalidSignatureHeaderError extends WebhookKitError {\n constructor(message = \"Invalid or missing signature header\") {\n super(message, \"INVALID_SIGNATURE_HEADER\");\n this.name = \"InvalidSignatureHeaderError\";\n }\n}\n\n/**\n * Thrown when the timestamp outside the allowed tolerance window (replay attack protection).\n */\nexport class TimestampToleranceError extends WebhookKitError {\n public readonly timestamp: number;\n public readonly toleranceSeconds: number;\n\n constructor(timestamp: number, toleranceSeconds: number, message?: string) {\n super(\n message ??\n `Timestamp outside tolerance window (timestamp: ${timestamp}, tolerance: ${toleranceSeconds}s)`,\n \"TIMESTAMP_TOLERANCE_EXCEEDED\",\n );\n this.name = \"TimestampToleranceError\";\n this.timestamp = timestamp;\n this.toleranceSeconds = toleranceSeconds;\n }\n}\n\n/**\n * Thrown when extracting raw payload from request or stream fails.\n */\nexport class PayloadExtractionError extends WebhookKitError {\n constructor(message = \"Failed to extract raw body payload for verification\") {\n super(message, \"PAYLOAD_EXTRACTION_FAILED\");\n this.name = \"PayloadExtractionError\";\n }\n}\n\n/**\n * Thrown or returned when a webhook event ID has already been processed in the replay store.\n */\nexport class ReplayAttackError extends WebhookKitError {\n public readonly eventId: string;\n\n constructor(eventId: string, message?: string) {\n super(\n message ??\n `Duplicate webhook event rejected (replay protection): event \"${eventId}\" was already processed`,\n \"WEBHOOK_REPLAY_DETECTED\",\n );\n this.name = \"ReplayAttackError\";\n this.eventId = eventId;\n }\n}\n","import { ReplayAttackError } from \"./errors.js\";\nimport type { HeaderRecord, RawBody, WebhookReplayStore } from \"./types.js\";\n\nconst DEFAULT_REPLAY_STORE_MAX_SIZE = 10_000;\n\nconst DEFAULT_REPLAY_STORE_TTL_SECONDS = 300;\n\nconst GENERIC_EVENT_ID_HEADERS = [\n \"x-webhook-id\",\n \"webhook-id\",\n \"x-github-delivery\",\n \"x-shopify-webhook-id\",\n \"x-event-id\",\n \"x-idempotency-key\",\n] as const;\n\n/**\n * Configuration options for `InMemoryReplayStore` / `createInMemoryReplayStore`.\n */\nexport interface InMemoryReplayStoreOptions {\n /**\n * Maximum number of entries retained in memory before evicting expired or oldest entries.\n * Defaults to `10_000`.\n */\n maxSize?: number;\n\n /**\n * Default time-to-live in seconds when `ttlSeconds` is omitted or invalid.\n * Defaults to `300` (5 minutes).\n */\n defaultTtlSeconds?: number;\n\n /**\n * Optional clock function returning current time in milliseconds (defaults to `Date.now`).\n * Useful for deterministic unit testing.\n */\n now?: () => number;\n}\n\n/**\n * Options for checking and recording an event ID in a `WebhookReplayStore`.\n */\nexport interface VerifyReplayStoreOptions {\n /**\n * Optional replay store instance.\n */\n store?: WebhookReplayStore;\n\n /**\n * Resolved event ID to check and record.\n */\n eventId?: string;\n\n /**\n * Explicit TTL in seconds for `store.set(eventId, ttl)`.\n */\n storeTtlSeconds?: number;\n\n /**\n * Fallback tolerance window in seconds used when `storeTtlSeconds` is not provided.\n */\n toleranceSeconds?: number;\n}\n\n/**\n * Default in-memory TTL replay store backed by a JavaScript `Map`.\n * Automatically evicts expired entries on lookup and enforces an upper capacity bound\n * with LRU (Least Recently Used) eviction.\n */\nexport class InMemoryReplayStore implements WebhookReplayStore {\n private readonly maxSize: number;\n\n private readonly defaultTtlSeconds: number;\n\n private readonly now: () => number;\n\n private readonly entries = new Map<string, number>();\n\n constructor(options: InMemoryReplayStoreOptions = {}) {\n this.maxSize = options.maxSize ?? DEFAULT_REPLAY_STORE_MAX_SIZE;\n this.defaultTtlSeconds =\n options.defaultTtlSeconds ?? DEFAULT_REPLAY_STORE_TTL_SECONDS;\n this.now = options.now ?? Date.now;\n }\n\n /**\n * Checks whether `eventId` is currently tracked and unexpired in the store.\n *\n * @param eventId - Unique webhook event or delivery identifier.\n * @returns `true` if `eventId` is present and unexpired, `false` otherwise.\n */\n has(eventId: string): boolean {\n const expiresAt = this.entries.get(eventId);\n\n if (expiresAt === undefined) {\n return false;\n }\n\n if (this.now() >= expiresAt) {\n // WHY: Lazily delete expired entries on read so stale event IDs do not accumulate\n // without requiring background setInterval timers that prevent clean process exit.\n this.entries.delete(eventId);\n\n return false;\n }\n\n return true;\n }\n\n /**\n * Records `eventId` in the store with the specified TTL in seconds.\n *\n * @param eventId - Unique webhook event or delivery identifier.\n * @param ttlSeconds - Time-to-live in seconds before expiration.\n */\n set(eventId: string, ttlSeconds: number = this.defaultTtlSeconds): void {\n const effectiveTtl = Number.isFinite(ttlSeconds)\n ? ttlSeconds\n : this.defaultTtlSeconds;\n\n if (effectiveTtl <= 0) {\n this.entries.delete(eventId);\n\n return;\n }\n\n if (this.entries.has(eventId)) {\n this.entries.delete(eventId);\n } else if (this.entries.size >= this.maxSize) {\n this.evictExpiredOrOldest();\n }\n\n this.entries.set(eventId, this.now() + effectiveTtl * 1000);\n }\n\n /**\n * Removes a specific `eventId` from the store.\n *\n * @param eventId - Unique webhook event or delivery identifier.\n * @returns `true` if the entry existed and was removed.\n */\n delete(eventId: string): boolean {\n return this.entries.delete(eventId);\n }\n\n /**\n * Clears all tracked event IDs from the store.\n */\n clear(): void {\n this.entries.clear();\n }\n\n /**\n * Removes all expired entries from the store and returns the number of purged entries.\n *\n * @returns Count of expired entries removed.\n */\n prune(): number {\n const currentMs = this.now();\n let removed = 0;\n\n for (const [key, expiresAt] of this.entries.entries()) {\n if (currentMs >= expiresAt) {\n this.entries.delete(key);\n removed += 1;\n }\n }\n\n return removed;\n }\n\n /**\n * Returns the current number of entries stored in the underlying map.\n */\n get size(): number {\n return this.entries.size;\n }\n\n /**\n * Returns the maximum capacity of the store.\n */\n get capacity(): number {\n return this.maxSize;\n }\n\n private evictExpiredOrOldest(): void {\n this.prune();\n\n if (this.entries.size < this.maxSize) {\n return;\n }\n\n const oldestKey = this.entries.keys().next().value;\n\n if (oldestKey !== undefined) {\n this.entries.delete(oldestKey);\n }\n }\n}\n\n/**\n * Creates a new `InMemoryReplayStore` instance with optional capacity and TTL settings.\n *\n * @param options - Optional capacity, default TTL, and clock configuration.\n * @returns Configured `InMemoryReplayStore` instance.\n */\nexport function createInMemoryReplayStore(\n options?: InMemoryReplayStoreOptions,\n): InMemoryReplayStore {\n return new InMemoryReplayStore(options);\n}\n\n/**\n * Case-insensitively extracts a single header value from a `Headers` instance or plain record.\n *\n * @param headers - Web standard `Headers` instance or record.\n * @param targetName - Lowercase target header name.\n * @returns Header value string or undefined.\n */\nfunction getHeaderCaseInsensitive(\n headers: HeaderRecord,\n targetName: string,\n): string | undefined {\n if (typeof Headers !== \"undefined\" && headers instanceof Headers) {\n return headers.get(targetName) ?? undefined;\n }\n\n const record = headers as Record<string, string | string[] | undefined>;\n const lowerTarget = targetName.toLowerCase();\n\n for (const key of Object.keys(record)) {\n if (key.toLowerCase() === lowerTarget) {\n const value = record[key];\n\n return Array.isArray(value) ? value[0] : value;\n }\n }\n\n return undefined;\n}\n\n/**\n * Searches candidate header names in a `Headers` or record object for a non-empty value.\n *\n * @param headers - Headers source.\n * @param candidateNames - Ordered list of lowercase header names.\n * @returns Trimmed non-empty header value or undefined.\n */\nfunction findHeaderValue(\n headers: HeaderRecord | undefined,\n candidateNames: readonly string[],\n): string | undefined {\n if (!headers) {\n return undefined;\n }\n\n for (const name of candidateNames) {\n const raw = getHeaderCaseInsensitive(headers, name);\n const trimmed = raw?.trim();\n\n if (trimmed) {\n return trimmed;\n }\n }\n\n return undefined;\n}\n\n/**\n * Extracts a string or finite number `id` property from a parsed payload or raw JSON body.\n *\n * @param payload - Already parsed payload object if available.\n * @param rawBody - Raw JSON string fallback when `parseJson` was disabled.\n * @returns Extracted string event ID or undefined.\n */\nfunction extractIdFromPayload(\n payload: unknown,\n rawBody?: string,\n): string | undefined {\n const candidate = resolvePayloadObject(payload, rawBody);\n\n if (!candidate) {\n return undefined;\n }\n\n const idValue = candidate.id;\n\n if (typeof idValue === \"string\" && idValue.trim()) {\n return idValue.trim();\n }\n\n if (typeof idValue === \"number\" && Number.isFinite(idValue)) {\n return String(idValue);\n }\n\n return undefined;\n}\n\n/**\n * Resolves a record object from either an already-parsed payload or a raw JSON string.\n *\n * @param payload - Parsed payload value.\n * @param rawBody - Raw body string.\n * @returns Object record or null.\n */\nfunction resolvePayloadObject(\n payload: unknown,\n rawBody?: string,\n): Record<string, unknown> | null {\n if (typeof payload === \"object\" && payload !== null) {\n return payload as Record<string, unknown>;\n }\n\n if (!rawBody) {\n return null;\n }\n\n try {\n const parsed: unknown = JSON.parse(rawBody);\n\n return typeof parsed === \"object\" && parsed !== null\n ? (parsed as Record<string, unknown>)\n : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Extracts a matching header value from a Web Standard `Request` body if provided.\n *\n * @param body - Candidate raw body input.\n * @param headerCandidates - Ordered delivery ID header names.\n * @returns Matching header value or undefined.\n */\nfunction findHeaderInRequest(\n body: RawBody | undefined,\n headerCandidates: readonly string[],\n): string | undefined {\n if (typeof Request !== \"undefined\" && body instanceof Request) {\n return findHeaderValue(body.headers, headerCandidates);\n }\n\n return undefined;\n}\n\n/**\n * Resolves the unique webhook event ID from explicit options, delivery headers, or payload `id`.\n *\n * @param options - Source options containing explicit `eventId`, `headers`, `body`, `payload`, and `rawBody`.\n * @param headerCandidates - Ordered delivery ID header names to inspect prior to payload fallback.\n * @returns Resolved non-empty event ID string or `undefined`.\n */\nexport function resolveWebhookEventId(\n options: {\n eventId?: string;\n headers?: HeaderRecord;\n body?: RawBody;\n payload?: unknown;\n rawBody?: string;\n },\n headerCandidates: readonly string[] = GENERIC_EVENT_ID_HEADERS,\n): string | undefined {\n const explicitId = options.eventId?.trim();\n\n if (explicitId) {\n return explicitId;\n }\n\n const fromHeaders =\n findHeaderValue(options.headers, headerCandidates) ??\n findHeaderInRequest(options.body, headerCandidates);\n\n if (fromHeaders) {\n return fromHeaders;\n }\n\n return extractIdFromPayload(options.payload, options.rawBody);\n}\n\n/**\n * Computes the effective replay store TTL in seconds from explicit `storeTtlSeconds` or `toleranceSeconds`.\n *\n * @param storeTtlSeconds - Explicit TTL in seconds.\n * @param toleranceSeconds - Timestamp tolerance in seconds.\n * @returns Positive TTL in seconds (defaults to 300s).\n */\nfunction resolveEffectiveTtl(\n storeTtlSeconds?: number,\n toleranceSeconds?: number,\n): number {\n if (storeTtlSeconds !== undefined && storeTtlSeconds > 0) {\n return storeTtlSeconds;\n }\n\n if (toleranceSeconds !== undefined && toleranceSeconds > 0) {\n return toleranceSeconds;\n }\n\n return DEFAULT_REPLAY_STORE_TTL_SECONDS;\n}\n\n/**\n * Verifies that `eventId` has not already been seen in `store`, and records it with a TTL if unseen.\n *\n * @param options - Store instance, resolved `eventId`, and TTL configuration.\n * @returns `ReplayAttackError` if `eventId` is already present in `store`, or `null` if unseen / no store.\n */\nexport async function checkAndRecordReplay(\n options: VerifyReplayStoreOptions,\n): Promise<ReplayAttackError | null> {\n const { store, eventId } = options;\n\n if (!store || !eventId) {\n return null;\n }\n\n const alreadySeen = await store.has(eventId);\n\n if (alreadySeen) {\n return new ReplayAttackError(eventId);\n }\n\n const ttlSeconds = resolveEffectiveTtl(\n options.storeTtlSeconds,\n options.toleranceSeconds,\n );\n\n await store.set(eventId, ttlSeconds);\n\n return null;\n}\n","/**\n * Web Cryptography API helper utilities using globalThis.crypto.subtle.\n * Provides zero-dependency, edge-compatible HMAC computation and constant-time verification.\n */\n\n/**\n * Output encodings supported by `computeSubtleHmac`.\n */\nexport type SubtleHmacEncoding = \"hex\" | \"base64\" | \"uint8array\" | \"binary\";\n\n/**\n * Options for configuring HMAC computation output.\n */\nexport interface SubtleHmacOptions {\n /**\n * Output encoding format. Defaults to `'uint8array'`.\n */\n encoding?: SubtleHmacEncoding;\n\n /**\n * Whether to bypass the in-memory CryptoKey cache. Defaults to `false`.\n */\n bypassCache?: boolean;\n}\n\n/**\n * Lightweight LRU (Least Recently Used) cache backed by a JavaScript `Map`.\n * Reuses compiled WebCrypto keys across concurrent requests and evicts the\n * oldest entries when capacity is reached.\n */\nexport class CryptoKeyLruCache {\n private readonly maxSize: number;\n\n private readonly map = new Map<string, Promise<CryptoKey>>();\n\n constructor(maxSize = 256) {\n this.maxSize = maxSize;\n }\n\n get(key: string): Promise<CryptoKey> | undefined {\n const item = this.map.get(key);\n\n if (!item) {\n return undefined;\n }\n\n // Refresh LRU order: delete and re-insert to mark as most recently used\n this.map.delete(key);\n this.map.set(key, item);\n\n return item;\n }\n\n set(key: string, value: Promise<CryptoKey>): void {\n if (this.map.has(key)) {\n this.map.delete(key);\n } else if (this.map.size >= this.maxSize) {\n const oldestKey = this.map.keys().next().value;\n\n if (oldestKey !== undefined) {\n this.map.delete(oldestKey);\n }\n }\n\n this.map.set(key, value);\n }\n\n has(key: string): boolean {\n return this.map.has(key);\n }\n\n delete(key: string): boolean {\n return this.map.delete(key);\n }\n\n clear(): void {\n this.map.clear();\n }\n\n get size(): number {\n return this.map.size;\n }\n\n get capacity(): number {\n return this.maxSize;\n }\n}\n\n/**\n * Global in-memory LRU cache holding compiled WebCrypto HMAC keys.\n */\nconst defaultCryptoKeyCache = new CryptoKeyLruCache(256);\n\n/**\n * Computes a fast 128-bit composite hash of secret bytes for safe cache key generation\n * without retaining raw secret strings or byte sequences in cache keys.\n *\n * @param bytes Raw secret bytes.\n * @returns Hex-encoded composite hash with length prefix.\n */\nfunction hashSecret(bytes: Uint8Array): string {\n let h1 = 0x811c9dc5;\n let h2 = 0x27d4eb2f;\n let h3 = 0x9e3779b9;\n let h4 = 0x45670123;\n const len = bytes.length;\n\n for (let i = 0; i < len; i++) {\n const b = bytes[i]!;\n h1 = Math.imul(h1 ^ b, 0x01000193);\n h2 = Math.imul(h2 ^ (b + i), 0x5bd1e995);\n h3 = Math.imul(h3 ^ ((b << 4) | (b >>> 4)), 0x1b873593);\n h4 = Math.imul(h4 ^ (b * 31), 0xcc9e2d51);\n }\n\n return `${len}_${(h1 >>> 0).toString(16)}_${(h2 >>> 0).toString(16)}_${(h3 >>> 0).toString(16)}_${(h4 >>> 0).toString(16)}`;\n}\n\n/**\n * Generates an in-memory cache key composed of the normalized algorithm and secret hash.\n *\n * @param normalizedHash Normalized hash algorithm (e.g. `'SHA-256'`).\n * @param secretBytes Raw secret bytes.\n * @returns Formatted cache key string.\n */\nfunction getSecretCacheKey(\n normalizedHash: string,\n secretBytes: Uint8Array,\n): string {\n return `${normalizedHash}:${hashSecret(secretBytes)}`;\n}\n\n/**\n * Invokes `crypto.subtle.importKey` for an HMAC signing key.\n */\nfunction importHmacKey(\n subtle: SubtleCrypto,\n normalizedHash: string,\n secretBytes: Uint8Array<ArrayBuffer>,\n): Promise<CryptoKey> {\n return subtle.importKey(\n \"raw\",\n secretBytes,\n {\n name: \"HMAC\",\n hash: { name: normalizedHash },\n },\n false,\n [\"sign\"],\n );\n}\n\n/**\n * Resolves a compiled `CryptoKey` from cache or initiates and caches a new one.\n */\nfunction getOrCreateCryptoKey(\n subtle: SubtleCrypto,\n normalizedHash: string,\n secretBytes: Uint8Array<ArrayBuffer>,\n cache: CryptoKeyLruCache = defaultCryptoKeyCache,\n): Promise<CryptoKey> {\n const cacheKey = getSecretCacheKey(normalizedHash, secretBytes);\n const cachedPromise = cache.get(cacheKey);\n\n if (cachedPromise) {\n return cachedPromise;\n }\n\n const keyPromise = importHmacKey(subtle, normalizedHash, secretBytes);\n\n // Evict immediately on error to avoid caching rejected promises\n keyPromise.catch(() => {\n cache.delete(cacheKey);\n });\n\n cache.set(cacheKey, keyPromise);\n\n return keyPromise;\n}\n\n/**\n * Clears all compiled WebCrypto HMAC keys from the global key cache.\n */\nexport function clearCryptoKeyCache(): void {\n defaultCryptoKeyCache.clear();\n}\n\n/**\n * Returns the current number of compiled WebCrypto HMAC keys stored in the global key cache.\n */\nexport function getCryptoKeyCacheSize(): number {\n return defaultCryptoKeyCache.size;\n}\n\n/**\n * Retrieves a cached WebCrypto HMAC key or compiles and caches a new one.\n *\n * @param algorithm HMAC digest algorithm (e.g. `'SHA-256'`).\n * @param secret Raw secret string or byte array.\n * @param cache Optional custom `CryptoKeyLruCache` instance.\n * @returns Compiled `CryptoKey` instance.\n */\nexport async function getCachedCryptoKey(\n algorithm: string,\n secret: string | Uint8Array,\n cache: CryptoKeyLruCache = defaultCryptoKeyCache,\n): Promise<CryptoKey> {\n const subtle = globalThis.crypto?.subtle;\n\n if (!subtle) {\n throw new Error(\n \"Web Cryptography API (crypto.subtle) is not available in the current runtime environment.\",\n );\n }\n\n const normalizedHash = normalizeHashAlgorithm(algorithm);\n const secretBytes = toUint8Array(secret);\n\n return getOrCreateCryptoKey(subtle, normalizedHash, secretBytes, cache);\n}\n\n/**\n * Normalizes hash algorithm names into standard W3C Web Crypto format.\n *\n * @param algorithm Algorithm identifier (e.g. `'sha256'`, `'SHA256'`, `'SHA-256'`).\n * @returns Normalized Web Crypto hash identifier (e.g. `'SHA-256'`).\n */\nfunction normalizeHashAlgorithm(algorithm: string): string {\n const upper = algorithm.toUpperCase().trim();\n // WHY: Web Crypto subtle.importKey strictly requires hyphenated SHA names\n // ('SHA-256', 'SHA-384', 'SHA-512'), whereas Node/OpenSSL callers often pass 'sha256'.\n if (/^SHA-?\\d+$/.test(upper)) {\n return upper.startsWith(\"SHA-\") ? upper : upper.replace(\"SHA\", \"SHA-\");\n }\n return upper;\n}\n\n/**\n * Converts a string or buffer into a `Uint8Array` backed by an `ArrayBuffer`.\n *\n * @param input String, `Uint8Array`, or `ArrayBuffer` payload.\n * @returns `Uint8Array<ArrayBuffer>` view suitable for Web Crypto `BufferSource`.\n */\nfunction toUint8Array(\n input: string | Uint8Array | ArrayBuffer,\n): Uint8Array<ArrayBuffer> {\n if (typeof input === \"string\") {\n return new TextEncoder().encode(input);\n }\n if (input instanceof Uint8Array) {\n // WHY: TypeScript 5.7+/7.0 DOM typings parameterize Uint8Array<ArrayBufferLike>\n // which fails BufferSource assignment due to SharedArrayBuffer. Re-wrapping\n // over ArrayBuffer satisfies strict DOM BufferSource without copying bytes.\n return new Uint8Array(\n input.buffer as ArrayBuffer,\n input.byteOffset,\n input.byteLength,\n );\n }\n return new Uint8Array(input);\n}\n\n/**\n * Constant-time comparison between two buffers or strings to prevent timing side-channel attacks.\n *\n * @param a First byte array or string.\n * @param b Second byte array or string.\n * @returns `true` if byte sequences are identical, `false` otherwise.\n */\nexport function timingSafeEqual(\n a: Uint8Array | string,\n b: Uint8Array | string,\n): boolean {\n const bufA = typeof a === \"string\" ? new TextEncoder().encode(a) : a;\n const bufB = typeof b === \"string\" ? new TextEncoder().encode(b) : b;\n\n const lenA = bufA.length;\n const lenB = bufB.length;\n // WHY: Seed `mismatch` with length inequality and still iterate across `maxLen`\n // so mismatched-length inputs do not short-circuit and leak length via timing.\n let mismatch = lenA === lenB ? 0 : 1;\n const maxLen = Math.max(lenA, lenB);\n\n for (let i = 0; i < maxLen; i++) {\n const byteA = i < lenA ? bufA[i]! : 0;\n const byteB = i < lenB ? bufB[i]! : 0;\n mismatch |= byteA ^ byteB;\n }\n\n return mismatch === 0;\n}\n\n/**\n * Formats a raw HMAC digest according to the requested encoding.\n */\nfunction formatHmacDigest(\n bytes: Uint8Array,\n encoding?: SubtleHmacEncoding,\n): Uint8Array | string {\n if (encoding === \"hex\") {\n return bytesToHex(bytes);\n }\n\n if (encoding === \"base64\") {\n return bytesToBase64(bytes);\n }\n\n return bytes;\n}\n\n/**\n * Normalizes options and extract encoding and cache preferences.\n */\nfunction extractHmacOptions(\n encodingOrOptions?: SubtleHmacEncoding | SubtleHmacOptions,\n): { encoding?: SubtleHmacEncoding; bypassCache: boolean } {\n if (typeof encodingOrOptions === \"string\") {\n return { encoding: encodingOrOptions, bypassCache: false };\n }\n\n return {\n encoding: encodingOrOptions?.encoding,\n bypassCache: Boolean(encodingOrOptions?.bypassCache),\n };\n}\n\n/**\n * Obtains a `CryptoKey` either by bypassing or utilizing the LRU cache.\n */\nfunction resolveSubtleHmacKey(\n subtle: SubtleCrypto,\n normalizedHash: string,\n secretBytes: Uint8Array<ArrayBuffer>,\n bypassCache: boolean,\n): Promise<CryptoKey> {\n if (bypassCache) {\n return importHmacKey(subtle, normalizedHash, secretBytes);\n }\n\n return getOrCreateCryptoKey(subtle, normalizedHash, secretBytes);\n}\n\n/**\n * Computes an HMAC digest using `crypto.subtle` across modern JavaScript runtimes.\n *\n * @param algorithm HMAC digest algorithm (`'SHA-256'`, `'SHA-384'`, `'SHA-512'`, `'SHA-1'`).\n * @param secret Shared secret key as UTF-8 string or raw bytes.\n * @param data Raw payload string or bytes.\n * @param encodingOrOptions Output encoding (`'uint8array'`, `'hex'`, `'base64'`) or options object.\n * @returns Raw HMAC digest bytes (`Uint8Array`) or encoded digest string.\n */\nexport async function computeSubtleHmac(\n algorithm: string,\n secret: string | Uint8Array,\n data: string | Uint8Array,\n encodingOrOptions?:\n | \"uint8array\"\n | \"binary\"\n | { encoding?: \"uint8array\" | \"binary\"; bypassCache?: boolean },\n): Promise<Uint8Array>;\n\nexport async function computeSubtleHmac(\n algorithm: string,\n secret: string | Uint8Array,\n data: string | Uint8Array,\n encodingOrOptions:\n \"hex\" | \"base64\" | { encoding: \"hex\" | \"base64\"; bypassCache?: boolean },\n): Promise<string>;\n\nexport async function computeSubtleHmac(\n algorithm: string,\n secret: string | Uint8Array,\n data: string | Uint8Array,\n encodingOrOptions?: SubtleHmacEncoding | SubtleHmacOptions,\n): Promise<Uint8Array | string> {\n const subtle = globalThis.crypto?.subtle;\n\n if (!subtle) {\n throw new Error(\n \"Web Cryptography API (crypto.subtle) is not available in the current runtime environment.\",\n );\n }\n\n const normalizedHash = normalizeHashAlgorithm(algorithm);\n const secretBytes = toUint8Array(secret);\n const dataBytes = toUint8Array(data);\n const { encoding, bypassCache } = extractHmacOptions(encodingOrOptions);\n\n const key = await resolveSubtleHmacKey(\n subtle,\n normalizedHash,\n secretBytes,\n bypassCache,\n );\n\n const signatureBuffer = await subtle.sign(\"HMAC\", key, dataBytes);\n\n return formatHmacDigest(new Uint8Array(signatureBuffer), encoding);\n}\n\n/**\n * Converts a `Uint8Array` into a lowercase hexadecimal string.\n *\n * @param bytes Input byte array.\n * @returns Lowercase hexadecimal string.\n */\nexport function bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\n/**\n * Converts a hexadecimal string into a `Uint8Array`.\n *\n * @param hex Hexadecimal string (with optional `0x` prefix).\n * @returns Decoded byte array.\n */\nexport function hexToBytes(hex: string): Uint8Array {\n const cleanHex = hex.startsWith(\"0x\") ? hex.slice(2) : hex;\n // WHY: Pad odd-length hex strings with a leading zero so byte slicing remains aligned.\n const normalizedHex = cleanHex.length % 2 === 1 ? `0${cleanHex}` : cleanHex;\n const bytes = new Uint8Array(normalizedHex.length / 2);\n for (let i = 0; i < normalizedHex.length; i += 2) {\n bytes[i / 2] = parseInt(normalizedHex.slice(i, i + 2), 16);\n }\n return bytes;\n}\n\n/**\n * Converts a `Uint8Array` into a Base64-encoded string using standard Web APIs.\n *\n * @param bytes Input byte array.\n * @returns Base64-encoded string.\n */\nexport function bytesToBase64(bytes: Uint8Array): string {\n let binary = \"\";\n const len = bytes.byteLength;\n // WHY: Chunk String.fromCharCode calls at 32 KiB to avoid V8/JSC call-stack\n // argument limits on large buffers while staying 100% Buffer-free on edge runtimes.\n const chunkSize = 0x8000;\n for (let i = 0; i < len; i += chunkSize) {\n const chunk = bytes.subarray(i, Math.min(i + chunkSize, len));\n binary += String.fromCharCode(...chunk);\n }\n return globalThis.btoa(binary);\n}\n\n/**\n * Converts a Base64-encoded string into a `Uint8Array` using standard Web APIs.\n *\n * @param base64 Base64-encoded string.\n * @returns Decoded byte array.\n */\nexport function base64ToBytes(base64: string): Uint8Array {\n const binary = globalThis.atob(base64.trim());\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n","import type { RawBody } from \"../types.js\";\nimport { PayloadExtractionError } from \"../errors.js\";\n\n/**\n * Checks whether an unknown value is a binary BufferSource (ArrayBuffer or ArrayBufferView).\n *\n * @param value Candidate value.\n * @returns `true` if the value is an ArrayBuffer or ArrayBufferView.\n */\nfunction isBinarySource(value: unknown): value is BufferSource {\n return value instanceof ArrayBuffer || ArrayBuffer.isView(value);\n}\n\n/**\n * Checks whether an unknown value is a Web Standard `Request`.\n *\n * @param value Candidate value.\n * @returns `true` if the value is a Request instance.\n */\nfunction isRequest(value: unknown): value is Request {\n return typeof Request !== \"undefined\" && value instanceof Request;\n}\n\n/**\n * Checks whether an unknown value is a Web Standard `ReadableStream`.\n *\n * @param value Candidate value.\n * @returns `true` if the value exposes a stream reader.\n */\nfunction isReadableStream(value: unknown): value is ReadableStream<Uint8Array> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as ReadableStream<Uint8Array>).getReader === \"function\"\n );\n}\n\n/**\n * Clones and reads text from a Web Standard `Request` without consuming the original body.\n *\n * @param request Incoming `Request` instance.\n * @returns Raw UTF-8 body string.\n */\nasync function readRequestBody(request: Request): Promise<string> {\n try {\n // WHY: Clone the Request before reading so downstream framework handlers\n // can still read the original request body stream if needed.\n return await request.clone().text();\n } catch (err) {\n throw new PayloadExtractionError(\n `Failed to read text from Request body: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\n/**\n * Decodes a single chunk from a ReadableStream using a streaming TextDecoder.\n *\n * @param decoder Streaming TextDecoder instance.\n * @param chunk Incoming chunk value.\n * @returns Decoded string chunk.\n */\nfunction decodeStreamChunk(decoder: TextDecoder, chunk: unknown): string {\n if (typeof chunk === \"string\") {\n return chunk;\n }\n\n if (isBinarySource(chunk)) {\n // WHY: Use { stream: true } while decoding chunks so multi-byte UTF-8 sequences\n // split across chunk boundaries are buffered rather than corrupted with replacement characters.\n return decoder.decode(chunk, { stream: true });\n }\n\n throw new PayloadExtractionError(\n `Unsupported stream chunk type: expected binary or string, received ${typeof chunk}`,\n );\n}\n\n/**\n * Reads all chunks from a Web Standard `ReadableStream` into a complete UTF-8 string.\n *\n * @param stream Incoming `ReadableStream` instance.\n * @returns Complete raw string body.\n */\nasync function readReadableStream(\n stream: ReadableStream<Uint8Array>,\n): Promise<string> {\n try {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let result = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n result += decodeStreamChunk(decoder, value);\n }\n\n // WHY: Final decode call with no arguments flushes any remaining buffered multi-byte characters.\n result += decoder.decode();\n\n return result;\n } finally {\n reader.releaseLock();\n }\n } catch (err) {\n if (err instanceof PayloadExtractionError) {\n throw err;\n }\n\n throw new PayloadExtractionError(\n `Failed to read text from ReadableStream: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\n/**\n * Safely extracts the unmodified raw string body from various input representations:\n * Web Standard `Request`, `ReadableStream`, `Uint8Array`, `ArrayBuffer`, `ArrayBufferView`, or primitive `string`.\n *\n * @param body The raw request or body object.\n * @returns Unmodified string representation suitable for cryptographic HMAC verification.\n * @throws {PayloadExtractionError} If body cannot be read or is invalid.\n */\nexport async function extractRawBody(body: RawBody): Promise<string> {\n if (typeof body === \"string\") {\n return body;\n }\n\n if (isBinarySource(body)) {\n return new TextDecoder().decode(body);\n }\n\n if (isRequest(body)) {\n return readRequestBody(body);\n }\n\n if (isReadableStream(body)) {\n return readReadableStream(body);\n }\n\n throw new PayloadExtractionError(\n \"Unsupported body type. Expected string, Uint8Array, ArrayBuffer, ReadableStream, or Request.\",\n );\n}\n","import { computeSubtleHmac, timingSafeEqual } from \"../crypto/subtle.js\";\nimport {\n InvalidSignatureHeaderError,\n SignatureVerificationError,\n WebhookKitError,\n} from \"../errors.js\";\nimport type {\n HeaderRecord,\n WebhookProviderPlugin,\n WebhookVerificationResult,\n} from \"../types.js\";\nimport { checkAndRecordReplay, resolveWebhookEventId } from \"../store.js\";\nimport { extractRawBody } from \"../utils/extract-body.js\";\nimport type { ProviderVerifyOptions } from \"./types.js\";\n\nconst GITHUB_SIGNATURE_PREFIX = \"sha256=\";\n\nconst GITHUB_EVENT_ID_HEADERS = [\n \"x-github-delivery\",\n \"x-webhook-id\",\n \"webhook-id\",\n] as const;\n\n/**\n * Standard shape of a GitHub Webhook Event payload.\n */\nexport interface GithubWebhookEvent {\n action?: string;\n sender?: Record<string, unknown>;\n repository?: Record<string, unknown>;\n organization?: Record<string, unknown>;\n installation?: Record<string, unknown>;\n zen?: string;\n hook_id?: number;\n hook?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\n/**\n * Options for GitHub webhook verification.\n */\nexport interface GithubVerifyOptions extends ProviderVerifyOptions {\n /**\n * X-Hub-Signature-256 header format: `sha256=d57c68d10b284e3d...`\n */\n signature?: string;\n\n /**\n * If true, throws errors rather than returning `{ valid: false, error }`.\n */\n throwOnError?: boolean;\n\n /**\n * Whether to automatically parse the verified body as JSON. Defaults to true.\n */\n parseJson?: boolean;\n}\n\n/**\n * Asserts that a header value is a non-empty string.\n *\n * @param header - Candidate header value.\n */\nfunction assertNonEmptyHeader(header: unknown): asserts header is string {\n if (typeof header !== \"string\" || !header.trim()) {\n throw new InvalidSignatureHeaderError(\n \"Missing or empty X-Hub-Signature-256 header\",\n );\n }\n}\n\n/**\n * Parses an `X-Hub-Signature-256` header, validating and extracting the hex signature.\n *\n * @param header - Raw `X-Hub-Signature-256` header string (e.g. `sha256=d57c68d1...`).\n * @returns Hexadecimal signature string after the `sha256=` prefix.\n * @throws {InvalidSignatureHeaderError} If header is missing, empty, or lacks a valid sha256= prefix.\n */\nexport function parseGithubSignature(header: string): string {\n assertNonEmptyHeader(header);\n\n const trimmed = header.trim();\n const lower = trimmed.toLowerCase();\n\n if (!lower.startsWith(GITHUB_SIGNATURE_PREFIX)) {\n throw new InvalidSignatureHeaderError(\n \"Invalid X-Hub-Signature-256 header format: expected 'sha256=' prefix\",\n );\n }\n\n const signature = trimmed.slice(GITHUB_SIGNATURE_PREFIX.length).trim();\n\n // WHY: GitHub HMAC-SHA256 digests require a non-empty hex hash following sha256=\n if (!signature) {\n throw new InvalidSignatureHeaderError(\n \"Invalid X-Hub-Signature-256 header: missing signature hash after prefix\",\n );\n }\n\n return signature;\n}\n\n/**\n * Case-insensitively extracts a header value from a `Headers` instance or plain record.\n *\n * @param headers - Web standard `Headers` instance or record.\n * @param targetName - Lowercase target header name.\n * @returns Header value string or undefined.\n */\nfunction extractHeaderValue(\n headers: HeaderRecord,\n targetName: string,\n): string | undefined {\n if (typeof Headers !== \"undefined\" && headers instanceof Headers) {\n return headers.get(targetName) ?? undefined;\n }\n\n const record = headers as Record<string, string | string[] | undefined>;\n const lowerTarget = targetName.toLowerCase();\n\n for (const key of Object.keys(record)) {\n if (key.toLowerCase() === lowerTarget) {\n const value = record[key];\n\n return Array.isArray(value) ? value[0] : value;\n }\n }\n\n return undefined;\n}\n\n/**\n * Resolves the raw signature header string from options, headers record, or request.\n *\n * @param options - GitHub verification options.\n * @returns GitHub signature header string or undefined if not found.\n */\nfunction resolveSignatureHeader(\n options: GithubVerifyOptions,\n): string | undefined {\n if (options.signature) {\n return options.signature;\n }\n\n if (options.headers) {\n return extractHeaderValue(options.headers, \"x-hub-signature-256\");\n }\n\n if (typeof Request !== \"undefined\" && options.body instanceof Request) {\n return options.body.headers.get(\"x-hub-signature-256\") ?? undefined;\n }\n\n return undefined;\n}\n\n/**\n * Validates preconditions for GitHub webhook verification.\n *\n * @param options - GitHub verification options.\n * @param rawHeader - Resolved header string.\n * @returns Error if preconditions fail, null otherwise.\n */\nfunction validatePreconditions(\n options: GithubVerifyOptions,\n rawHeader: string | undefined,\n): WebhookKitError | null {\n if (!options.secret) {\n return new SignatureVerificationError(\n \"Missing GitHub webhook signing secret\",\n );\n }\n\n if (!rawHeader) {\n return new InvalidSignatureHeaderError(\n \"Missing X-Hub-Signature-256 header\",\n );\n }\n\n return null;\n}\n\n/**\n * Safely parses the raw signature header without uncaught exceptions.\n *\n * @param rawHeader - Raw header string.\n * @returns Extracted signature string or error object.\n */\nfunction safeParseHeader(\n rawHeader: string,\n): { signature: string } | { error: WebhookKitError } {\n try {\n const signature = parseGithubSignature(rawHeader);\n\n return { signature };\n } catch (err) {\n return {\n error:\n err instanceof WebhookKitError\n ? err\n : new InvalidSignatureHeaderError(String(err)),\n };\n }\n}\n\n/**\n * Constructs a failed WebhookVerificationResult or throws if throwOnError is set.\n *\n * @param rawBody - Raw body string.\n * @param error - WebhookKitError instance.\n * @param throwOnError - Optional flag to throw immediately.\n * @returns Failed verification result.\n */\nfunction failVerification(\n rawBody: string,\n error: WebhookKitError,\n throwOnError?: boolean,\n eventId?: string,\n): WebhookVerificationResult<never> {\n if (throwOnError) {\n throw error;\n }\n\n return {\n valid: false,\n rawBody,\n eventId,\n error,\n };\n}\n\n/**\n * Attempts to parse the raw body as JSON.\n *\n * @param rawBody - Raw body payload string.\n * @returns Parsed payload or undefined.\n */\nfunction tryParseJson<T>(rawBody: string): T | undefined {\n try {\n return JSON.parse(rawBody) as T;\n } catch {\n // WHY: Silently ignore JSON parse errors when rawBody is not valid JSON\n // so verification succeeds if HMAC signature is valid.\n return undefined;\n }\n}\n\n/**\n * Verifies a GitHub webhook payload signature against `X-Hub-Signature-256`.\n *\n * GitHub signs the raw request payload using HMAC-SHA256 with the webhook secret.\n *\n * @param options - GitHub verification options.\n * @returns Verification result with typed payload.\n */\nexport async function verifyGithubWebhook<T = GithubWebhookEvent>(\n options: GithubVerifyOptions,\n): Promise<WebhookVerificationResult<T>> {\n const rawBody = await extractRawBody(options.body);\n const rawHeader = resolveSignatureHeader(options);\n\n const preconditionError = validatePreconditions(options, rawHeader);\n\n if (preconditionError) {\n return failVerification(rawBody, preconditionError, options.throwOnError);\n }\n\n const parsed = safeParseHeader(rawHeader!);\n\n if (\"error\" in parsed) {\n return failVerification(rawBody, parsed.error, options.throwOnError);\n }\n\n // WHY: GitHub computes HMAC-SHA256 over raw body payload using shared webhook secret\n const expectedSig = await computeSubtleHmac(\n \"SHA-256\",\n options.secret,\n rawBody,\n \"hex\",\n );\n\n // WHY: Lowercase both signatures before constant-time comparison to ensure\n // hex casing variations do not produce false rejections while preventing side-channel timing leaks.\n const isValid = timingSafeEqual(\n expectedSig.toLowerCase(),\n parsed.signature.toLowerCase(),\n );\n\n if (!isValid) {\n const error = new SignatureVerificationError(\n \"GitHub signature verification failed: signature does not match expected HMAC\",\n );\n\n return failVerification(rawBody, error, options.throwOnError);\n }\n\n const payload =\n options.parseJson !== false ? tryParseJson<T>(rawBody) : undefined;\n const eventId = resolveWebhookEventId(\n {\n eventId: options.eventId,\n headers: options.headers,\n body: options.body,\n payload,\n rawBody,\n },\n GITHUB_EVENT_ID_HEADERS,\n );\n const replayError = await checkAndRecordReplay({\n store: options.store,\n eventId,\n storeTtlSeconds: options.storeTtlSeconds,\n toleranceSeconds: options.toleranceSeconds,\n });\n\n if (replayError) {\n return failVerification(\n rawBody,\n replayError,\n options.throwOnError,\n eventId,\n );\n }\n\n return {\n valid: true,\n rawBody,\n eventId,\n payload: payload as T,\n };\n}\n\n/**\n * Asserts that a GitHub webhook payload signature is valid, throwing on verification failure.\n *\n * @param options - GitHub verification options.\n * @returns Parsed and typed event payload.\n * @throws {SignatureVerificationError} If signature verification fails.\n * @throws {InvalidSignatureHeaderError} If header format is invalid or missing.\n */\nexport async function assertGithubWebhook<T = GithubWebhookEvent>(\n options: GithubVerifyOptions,\n): Promise<T> {\n const result = await verifyGithubWebhook<T>({\n ...options,\n throwOnError: true,\n });\n\n return result.payload as T;\n}\n\n/**\n * Webhook provider plugin for GitHub.\n */\nexport const githubPlugin: WebhookProviderPlugin<\n GithubWebhookEvent,\n GithubVerifyOptions\n> = {\n name: \"github\",\n verify: verifyGithubWebhook,\n};\n","import { computeSubtleHmac, timingSafeEqual } from \"../crypto/subtle.js\";\nimport {\n InvalidSignatureHeaderError,\n SignatureVerificationError,\n WebhookKitError,\n} from \"../errors.js\";\nimport type {\n HeaderRecord,\n WebhookProviderPlugin,\n WebhookVerificationResult,\n} from \"../types.js\";\nimport { checkAndRecordReplay, resolveWebhookEventId } from \"../store.js\";\nimport { extractRawBody } from \"../utils/extract-body.js\";\nimport type { ProviderVerifyOptions } from \"./types.js\";\n\nconst BASE64_REGEX =\n /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\n\nconst SHOPIFY_EVENT_ID_HEADERS = [\n \"x-shopify-webhook-id\",\n \"x-webhook-id\",\n \"webhook-id\",\n] as const;\n\n/**\n * Standard shape of a Shopify Webhook Event payload.\n */\nexport interface ShopifyWebhookEvent {\n id?: number | string;\n admin_graphql_api_id?: string;\n created_at?: string;\n updated_at?: string;\n [key: string]: unknown;\n}\n\n/**\n * Options for Shopify webhook verification.\n */\nexport interface ShopifyVerifyOptions extends ProviderVerifyOptions {\n /**\n * X-Shopify-Hmac-Sha256 base64-encoded signature header.\n */\n signature?: string;\n\n /**\n * If true, throws errors rather than returning `{ valid: false, error }`.\n */\n throwOnError?: boolean;\n\n /**\n * Whether to automatically parse the verified body as JSON. Defaults to true.\n */\n parseJson?: boolean;\n}\n\n/**\n * Asserts that a header value is a non-empty string.\n *\n * @param header - Candidate header value.\n */\nfunction assertNonEmptyHeader(header: unknown): asserts header is string {\n if (typeof header !== \"string\" || !header.trim()) {\n throw new InvalidSignatureHeaderError(\n \"Missing or empty X-Shopify-Hmac-Sha256 header\",\n );\n }\n}\n\n/**\n * Parses an `X-Shopify-Hmac-Sha256` header, validating and normalizing the Base64 signature.\n *\n * @param header - Raw `X-Shopify-Hmac-Sha256` Base64 header string.\n * @returns Normalized Base64-encoded HMAC-SHA256 signature string.\n * @throws {InvalidSignatureHeaderError} If header is missing, empty, or not valid Base64.\n */\nexport function parseShopifySignature(header: string): string {\n assertNonEmptyHeader(header);\n\n const trimmed = header.trim();\n\n // WHY: Validate strict Base64 structure before cryptographic comparison so malformed\n // or non-Base64 headers fail fast with InvalidSignatureHeaderError.\n if (!BASE64_REGEX.test(trimmed)) {\n throw new InvalidSignatureHeaderError(\n \"Invalid X-Shopify-Hmac-Sha256 header format: expected Base64-encoded string\",\n );\n }\n\n return trimmed;\n}\n\n/**\n * Case-insensitively extracts a header value from a `Headers` instance or plain record.\n *\n * @param headers - Web standard `Headers` instance or record.\n * @param targetName - Lowercase target header name.\n * @returns Header value string or undefined.\n */\nfunction extractHeaderValue(\n headers: HeaderRecord,\n targetName: string,\n): string | undefined {\n if (typeof Headers !== \"undefined\" && headers instanceof Headers) {\n return headers.get(targetName) ?? undefined;\n }\n\n const record = headers as Record<string, string | string[] | undefined>;\n const lowerTarget = targetName.toLowerCase();\n\n for (const key of Object.keys(record)) {\n if (key.toLowerCase() === lowerTarget) {\n const value = record[key];\n\n return Array.isArray(value) ? value[0] : value;\n }\n }\n\n return undefined;\n}\n\n/**\n * Resolves the raw signature header string from options, headers record, or request.\n *\n * @param options - Shopify verification options.\n * @returns Shopify signature header string or undefined if not found.\n */\nfunction resolveSignatureHeader(\n options: ShopifyVerifyOptions,\n): string | undefined {\n if (options.signature) {\n return options.signature;\n }\n\n if (options.headers) {\n return extractHeaderValue(options.headers, \"x-shopify-hmac-sha256\");\n }\n\n if (typeof Request !== \"undefined\" && options.body instanceof Request) {\n return options.body.headers.get(\"x-shopify-hmac-sha256\") ?? undefined;\n }\n\n return undefined;\n}\n\n/**\n * Validates preconditions for Shopify webhook verification.\n *\n * @param options - Shopify verification options.\n * @param rawHeader - Resolved header string.\n * @returns Error if preconditions fail, null otherwise.\n */\nfunction validatePreconditions(\n options: ShopifyVerifyOptions,\n rawHeader: string | undefined,\n): WebhookKitError | null {\n if (!options.secret) {\n return new SignatureVerificationError(\n \"Missing Shopify webhook signing secret\",\n );\n }\n\n if (!rawHeader) {\n return new InvalidSignatureHeaderError(\n \"Missing X-Shopify-Hmac-Sha256 header\",\n );\n }\n\n return null;\n}\n\n/**\n * Safely parses the raw signature header without uncaught exceptions.\n *\n * @param rawHeader - Raw header string.\n * @returns Extracted Base64 signature string or error object.\n */\nfunction safeParseHeader(\n rawHeader: string,\n): { signature: string } | { error: WebhookKitError } {\n try {\n const signature = parseShopifySignature(rawHeader);\n\n return { signature };\n } catch (err) {\n return {\n error:\n err instanceof WebhookKitError\n ? err\n : new InvalidSignatureHeaderError(String(err)),\n };\n }\n}\n\n/**\n * Constructs a failed WebhookVerificationResult or throws if throwOnError is set.\n *\n * @param rawBody - Raw body string.\n * @param error - WebhookKitError instance.\n * @param throwOnError - Optional flag to throw immediately.\n * @returns Failed verification result.\n */\nfunction failVerification(\n rawBody: string,\n error: WebhookKitError,\n throwOnError?: boolean,\n eventId?: string,\n): WebhookVerificationResult<never> {\n if (throwOnError) {\n throw error;\n }\n\n return {\n valid: false,\n rawBody,\n eventId,\n error,\n };\n}\n\n/**\n * Attempts to parse the raw body as JSON.\n *\n * @param rawBody - Raw body payload string.\n * @returns Parsed payload or undefined.\n */\nfunction tryParseJson<T>(rawBody: string): T | undefined {\n try {\n return JSON.parse(rawBody) as T;\n } catch {\n // WHY: Silently ignore JSON parse errors when rawBody is not valid JSON\n // so verification succeeds if HMAC signature is valid.\n return undefined;\n }\n}\n\n/**\n * Verifies a Shopify webhook payload signature against `X-Shopify-Hmac-Sha256`.\n *\n * Shopify computes HMAC-SHA256 of the raw body and encodes it as Base64.\n *\n * @param options - Shopify verification options.\n * @returns Verification result with typed payload.\n */\nexport async function verifyShopifyWebhook<T = ShopifyWebhookEvent>(\n options: ShopifyVerifyOptions,\n): Promise<WebhookVerificationResult<T>> {\n const rawBody = await extractRawBody(options.body);\n const rawHeader = resolveSignatureHeader(options);\n\n const preconditionError = validatePreconditions(options, rawHeader);\n\n if (preconditionError) {\n return failVerification(rawBody, preconditionError, options.throwOnError);\n }\n\n const parsed = safeParseHeader(rawHeader!);\n\n if (\"error\" in parsed) {\n return failVerification(rawBody, parsed.error, options.throwOnError);\n }\n\n // WHY: Shopify computes HMAC-SHA256 over the raw body and encodes the digest as Base64.\n const expectedSig = await computeSubtleHmac(\n \"SHA-256\",\n options.secret,\n rawBody,\n \"base64\",\n );\n\n // WHY: Compare Base64 digests in constant time to prevent timing side-channel leaks.\n // Base64 is case-sensitive, so exact character byte sequences must match.\n const isValid = timingSafeEqual(expectedSig, parsed.signature);\n\n if (!isValid) {\n const error = new SignatureVerificationError(\n \"Shopify signature verification failed: signature does not match expected HMAC\",\n );\n\n return failVerification(rawBody, error, options.throwOnError);\n }\n\n const payload =\n options.parseJson !== false ? tryParseJson<T>(rawBody) : undefined;\n const eventId = resolveWebhookEventId(\n {\n eventId: options.eventId,\n headers: options.headers,\n body: options.body,\n payload,\n rawBody,\n },\n SHOPIFY_EVENT_ID_HEADERS,\n );\n const replayError = await checkAndRecordReplay({\n store: options.store,\n eventId,\n storeTtlSeconds: options.storeTtlSeconds,\n toleranceSeconds: options.toleranceSeconds,\n });\n\n if (replayError) {\n return failVerification(\n rawBody,\n replayError,\n options.throwOnError,\n eventId,\n );\n }\n\n return {\n valid: true,\n rawBody,\n eventId,\n payload: payload as T,\n };\n}\n\n/**\n * Asserts that a Shopify webhook payload signature is valid, throwing on verification failure.\n *\n * @param options - Shopify verification options.\n * @returns Parsed and typed event payload.\n * @throws {SignatureVerificationError} If signature verification fails.\n * @throws {InvalidSignatureHeaderError} If header format is invalid or missing.\n */\nexport async function assertShopifyWebhook<T = ShopifyWebhookEvent>(\n options: ShopifyVerifyOptions,\n): Promise<T> {\n const result = await verifyShopifyWebhook<T>({\n ...options,\n throwOnError: true,\n });\n\n return result.payload as T;\n}\n\n/**\n * Webhook provider plugin for Shopify.\n */\nexport const shopifyPlugin: WebhookProviderPlugin<\n ShopifyWebhookEvent,\n ShopifyVerifyOptions\n> = {\n name: \"shopify\",\n verify: verifyShopifyWebhook,\n};\n","import { computeSubtleHmac, timingSafeEqual } from \"../crypto/subtle.js\";\nimport {\n InvalidSignatureHeaderError,\n SignatureVerificationError,\n TimestampToleranceError,\n WebhookKitError,\n} from \"../errors.js\";\nimport type {\n HeaderRecord,\n WebhookProviderPlugin,\n WebhookVerificationResult,\n} from \"../types.js\";\nimport { checkAndRecordReplay, resolveWebhookEventId } from \"../store.js\";\nimport { extractRawBody } from \"../utils/extract-body.js\";\nimport type { ProviderVerifyOptions } from \"./types.js\";\n\nconst STRIPE_EVENT_ID_HEADERS = [\n \"x-stripe-webhook-id\",\n \"x-webhook-id\",\n \"webhook-id\",\n] as const;\n\n/**\n * Parsed components of a `Stripe-Signature` header.\n */\nexport interface StripeParsedSignatureHeader {\n /**\n * Extracted Unix timestamp in seconds.\n */\n timestamp: number;\n\n /**\n * One or more valid v1 HMAC-SHA256 signatures.\n */\n signatures: string[];\n\n /**\n * Optional v0 legacy signatures if present.\n */\n v0Signatures?: string[];\n}\n\n/**\n * Standard shape of a Stripe Webhook Event payload.\n */\nexport interface StripeWebhookEvent {\n id: string;\n object: string;\n type: string;\n created: number;\n data: {\n object: Record<string, unknown>;\n [key: string]: unknown;\n };\n livemode?: boolean;\n pending_webhooks?: number;\n request?: {\n id: string | null;\n idempotency_key: string | null;\n };\n [key: string]: unknown;\n}\n\n/**\n * Options for Stripe webhook verification.\n */\nexport interface StripeVerifyOptions extends ProviderVerifyOptions {\n /**\n * Stripe-Signature header format: `t=1492774577,v1=5257a869e7ecebeda32affa62cd4923...`\n */\n signature?: string;\n\n /**\n * Timestamp tolerance window in seconds (Stripe default is 300s).\n * Pass 0 to disable replay protection check.\n */\n toleranceSeconds?: number;\n\n /**\n * Current Unix timestamp in seconds (defaults to Math.floor(Date.now() / 1000)).\n * Useful for deterministic testing.\n */\n currentTimestamp?: number;\n\n /**\n * If true, throws errors rather than returning `{ valid: false, error }`.\n */\n throwOnError?: boolean;\n}\n\n/**\n * Case-insensitively extracts a header value from a `Headers` instance or plain record.\n *\n * @param headers - Web standard `Headers` instance or record.\n * @param targetName - Lowercase target header name.\n * @returns Header value string or undefined.\n */\nfunction extractHeaderValue(\n headers: HeaderRecord,\n targetName: string,\n): string | undefined {\n if (typeof Headers !== \"undefined\" && headers instanceof Headers) {\n return headers.get(targetName) ?? undefined;\n }\n\n const record = headers as Record<string, string | string[] | undefined>;\n const lowerTarget = targetName.toLowerCase();\n\n for (const key of Object.keys(record)) {\n if (key.toLowerCase() === lowerTarget) {\n const value = record[key];\n\n return Array.isArray(value) ? value[0] : value;\n }\n }\n\n return undefined;\n}\n\n/**\n * Resolves the raw signature header string from options, headers record, or request.\n *\n * @param options - Stripe verification options.\n * @returns Stripe signature header string or undefined if not found.\n */\nfunction resolveSignatureHeader(\n options: StripeVerifyOptions,\n): string | undefined {\n if (options.signature) {\n return options.signature;\n }\n\n if (options.headers) {\n return extractHeaderValue(options.headers, \"stripe-signature\");\n }\n\n if (typeof Request !== \"undefined\" && options.body instanceof Request) {\n return options.body.headers.get(\"stripe-signature\") ?? undefined;\n }\n\n return undefined;\n}\n\n/**\n * Splits a header tag pair into key and value.\n *\n * @param item - Header entry (e.g. `t=1492774577` or `v1=...`).\n * @returns Key-value tuple or null if malformed.\n */\nfunction parseKeyValue(item: string): [string, string] | null {\n const trimmed = item.trim();\n const eqIndex = trimmed.indexOf(\"=\");\n\n if (eqIndex === -1) {\n return null;\n }\n\n return [trimmed.slice(0, eqIndex).trim(), trimmed.slice(eqIndex + 1).trim()];\n}\n\n/**\n * Processes a single parsed tag from the Stripe signature header into accumulator state.\n *\n * @param key - Tag key (e.g. `'t'`, `'v1'`, `'v0'`).\n * @param value - Tag value string.\n * @param state - Mutable parsing state.\n */\nfunction processHeaderTag(\n key: string,\n value: string,\n state: {\n timestamp?: number;\n signatures: string[];\n v0Signatures: string[];\n },\n): void {\n if (!value) {\n return;\n }\n\n if (key === \"v1\") {\n state.signatures.push(value);\n\n return;\n }\n\n if (key === \"v0\") {\n state.v0Signatures.push(value);\n\n return;\n }\n\n if (key === \"t\") {\n const parsed = parseInt(value, 10);\n // WHY: Verify integer validity to avoid NaN propagation during timestamp comparison.\n if (Number.isInteger(parsed) && parsed > 0) {\n state.timestamp = parsed;\n }\n }\n}\n\n/**\n * Asserts that a header value is a non-empty string.\n *\n * @param header - Candidate header value.\n */\nfunction assertNonEmptyHeader(header: unknown): asserts header is string {\n if (typeof header !== \"string\" || !header.trim()) {\n throw new InvalidSignatureHeaderError(\n \"Missing or empty Stripe-Signature header\",\n );\n }\n}\n\n/**\n * Collects timestamp and signature schemes across comma-separated header entries.\n *\n * @param header - Non-empty header string.\n * @returns Parsed mutable state accumulator.\n */\nfunction collectHeaderState(header: string): {\n timestamp?: number;\n signatures: string[];\n v0Signatures: string[];\n} {\n const state: {\n timestamp?: number;\n signatures: string[];\n v0Signatures: string[];\n } = {\n signatures: [],\n v0Signatures: [],\n };\n\n const items = header.split(\",\");\n\n for (const item of items) {\n const pair = parseKeyValue(item);\n\n if (pair) {\n processHeaderTag(pair[0], pair[1], state);\n }\n }\n\n return state;\n}\n\n/**\n * Validates that parsed header state satisfies Stripe protocol requirements.\n *\n * @param timestamp - Extracted Unix timestamp.\n * @param signaturesCount - Count of valid v1 signatures.\n */\nfunction validateParsedState(\n timestamp?: number,\n signaturesCount?: number,\n): void {\n if (timestamp === undefined) {\n throw new InvalidSignatureHeaderError(\n \"Stripe-Signature header missing or invalid timestamp (t=...)\",\n );\n }\n\n // WHY: Stripe requires at least one v1 signature scheme to mitigate downgrade attacks.\n if (!signaturesCount) {\n throw new InvalidSignatureHeaderError(\n \"Stripe-Signature header missing valid v1 signature scheme (v1=...)\",\n );\n }\n}\n\n/**\n * Parses a Stripe signature header string (`t=...,v1=...,v0=...`).\n *\n * @param header - Raw `Stripe-Signature` header string.\n * @returns Parsed header components including timestamp and signatures.\n * @throws {InvalidSignatureHeaderError} If header format is invalid or missing required tags.\n */\nexport function parseStripeSignature(\n header: string,\n): StripeParsedSignatureHeader {\n assertNonEmptyHeader(header);\n\n const state = collectHeaderState(header);\n\n validateParsedState(state.timestamp, state.signatures.length);\n\n return {\n timestamp: state.timestamp!,\n signatures: state.signatures,\n v0Signatures: state.v0Signatures.length ? state.v0Signatures : undefined,\n };\n}\n\n/**\n * Validates that a webhook timestamp falls within the allowed tolerance window.\n *\n * @param timestamp - Unix timestamp in seconds from the signature header.\n * @param toleranceSeconds - Maximum allowed drift/age in seconds (default: 300s). Pass 0 to disable.\n * @param currentTimestamp - Optional current Unix timestamp in seconds (defaults to now).\n * @throws {TimestampToleranceError} If timestamp falls outside the tolerance window.\n */\nexport function validateTimestampTolerance(\n timestamp: number,\n toleranceSeconds = 300,\n currentTimestamp?: number,\n): void {\n // WHY: Setting tolerance <= 0 explicitly disables replay protection.\n if (toleranceSeconds <= 0) {\n return;\n }\n\n const now = currentTimestamp ?? Math.floor(Date.now() / 1000);\n const diff = Math.abs(now - timestamp);\n\n if (diff > toleranceSeconds) {\n throw new TimestampToleranceError(timestamp, toleranceSeconds);\n }\n}\n\n/**\n * Compares an expected HMAC hex signature against candidate signatures in constant time.\n *\n * @param expectedSignature - Computed hex signature.\n * @param candidates - List of candidate hex signatures from the header.\n * @returns `true` if any candidate matches the expected signature.\n */\nfunction matchesAnySignature(\n expectedSignature: string,\n candidates: string[],\n): boolean {\n let matched = false;\n\n for (const candidate of candidates) {\n // WHY: Use timingSafeEqual on each candidate to protect against side-channel timing leaks.\n if (timingSafeEqual(expectedSignature, candidate)) {\n matched = true;\n }\n }\n\n return matched;\n}\n\n/**\n * Attempts to parse the raw body as JSON.\n *\n * @param rawBody - Raw body payload string.\n * @returns Parsed payload or undefined.\n */\nfunction tryParseJson<T>(rawBody: string): T | undefined {\n try {\n return JSON.parse(rawBody) as T;\n } catch {\n // WHY: Silently ignore JSON parse errors when rawBody is not valid JSON\n // so verifyStripeWebhook succeeds if signature is valid.\n return undefined;\n }\n}\n\n/**\n * Validates preconditions for Stripe webhook verification.\n *\n * @param options - Stripe verification options.\n * @param rawHeader - Resolved header string.\n * @returns Error if preconditions fail, null otherwise.\n */\nfunction validatePreconditions(\n options: StripeVerifyOptions,\n rawHeader: string | undefined,\n): WebhookKitError | null {\n if (!options.secret) {\n return new SignatureVerificationError(\n \"Missing Stripe webhook signing secret\",\n );\n }\n\n if (!rawHeader) {\n return new InvalidSignatureHeaderError(\"Missing Stripe-Signature header\");\n }\n\n return null;\n}\n\n/**\n * Safely parses the raw signature header without uncaught exceptions.\n *\n * @param rawHeader - Raw header string.\n * @returns Parsed header or error object.\n */\nfunction safeParseHeader(\n rawHeader: string,\n): StripeParsedSignatureHeader | { error: WebhookKitError } {\n try {\n return parseStripeSignature(rawHeader);\n } catch (err) {\n return {\n error:\n err instanceof WebhookKitError\n ? err\n : new InvalidSignatureHeaderError(String(err)),\n };\n }\n}\n\n/**\n * Checks timestamp tolerance, returning an error if tolerance window is exceeded.\n *\n * @param timestamp - Unix timestamp in seconds.\n * @param options - Stripe verification options.\n * @returns TimestampToleranceError if exceeded, null otherwise.\n */\nfunction checkTolerance(\n timestamp: number,\n options: StripeVerifyOptions,\n): TimestampToleranceError | null {\n const tolerance = options.toleranceSeconds ?? 300;\n\n try {\n validateTimestampTolerance(timestamp, tolerance, options.currentTimestamp);\n\n return null;\n } catch (err) {\n return err instanceof TimestampToleranceError\n ? err\n : new TimestampToleranceError(timestamp, tolerance);\n }\n}\n\n/**\n * Constructs a failed WebhookVerificationResult or throws if throwOnError is set.\n *\n * @param rawBody - Raw body string.\n * @param error - WebhookKitError instance.\n * @param throwOnError - Optional flag to throw immediately.\n * @param timestamp - Optional parsed timestamp.\n * @returns Failed verification result.\n */\nfunction failVerification(\n rawBody: string,\n error: WebhookKitError,\n throwOnError?: boolean,\n timestamp?: number,\n eventId?: string,\n): WebhookVerificationResult<never> {\n if (throwOnError) {\n throw error;\n }\n\n return {\n valid: false,\n rawBody,\n timestamp,\n eventId,\n error,\n };\n}\n\n/**\n * Verifies a Stripe webhook event signature.\n *\n * Stripe computes HMAC-SHA256 over `${timestamp}.${rawPayload}` and compares\n * against the `v1` signature tag in `Stripe-Signature`.\n *\n * @param options - Stripe verification options\n * @returns Verification result with typed payload\n */\nexport async function verifyStripeWebhook<T = StripeWebhookEvent>(\n options: StripeVerifyOptions,\n): Promise<WebhookVerificationResult<T>> {\n const rawBody = await extractRawBody(options.body);\n const rawHeader = resolveSignatureHeader(options);\n\n const preconditionError = validatePreconditions(options, rawHeader);\n\n if (preconditionError) {\n return failVerification(rawBody, preconditionError, options.throwOnError);\n }\n\n const parsedHeader = safeParseHeader(rawHeader!);\n\n if (\"error\" in parsedHeader) {\n return failVerification(rawBody, parsedHeader.error, options.throwOnError);\n }\n\n const { timestamp, signatures } = parsedHeader;\n const toleranceError = checkTolerance(timestamp, options);\n\n if (toleranceError) {\n return failVerification(\n rawBody,\n toleranceError,\n options.throwOnError,\n timestamp,\n );\n }\n\n // WHY: Stripe computes HMAC-SHA256 over `${timestamp}.${rawBody}`\n const signedPayload = `${timestamp}.${rawBody}`;\n const expectedSig = await computeSubtleHmac(\n \"SHA-256\",\n options.secret,\n signedPayload,\n \"hex\",\n );\n\n const isValid = matchesAnySignature(expectedSig, signatures);\n\n if (!isValid) {\n const error = new SignatureVerificationError(\n \"Stripe signature verification failed: signature does not match expected HMAC\",\n );\n\n return failVerification(rawBody, error, options.throwOnError, timestamp);\n }\n\n const payload =\n options.parseJson !== false ? tryParseJson<T>(rawBody) : undefined;\n const eventId = resolveWebhookEventId(\n {\n eventId: options.eventId,\n headers: options.headers,\n body: options.body,\n payload,\n rawBody,\n },\n STRIPE_EVENT_ID_HEADERS,\n );\n const replayError = await checkAndRecordReplay({\n store: options.store,\n eventId,\n storeTtlSeconds: options.storeTtlSeconds,\n toleranceSeconds: options.toleranceSeconds,\n });\n\n if (replayError) {\n return failVerification(\n rawBody,\n replayError,\n options.throwOnError,\n timestamp,\n eventId,\n );\n }\n\n return {\n valid: true,\n rawBody,\n timestamp,\n eventId,\n payload: payload as T,\n };\n}\n\n/**\n * Asserts that a Stripe webhook event signature is valid, throwing on verification failure.\n *\n * @param options - Stripe verification options.\n * @returns Parsed and typed event payload.\n * @throws {SignatureVerificationError} If signature verification fails.\n * @throws {TimestampToleranceError} If timestamp falls outside the tolerance window.\n * @throws {InvalidSignatureHeaderError} If header format is invalid or missing.\n */\nexport async function assertStripeWebhook<T = StripeWebhookEvent>(\n options: StripeVerifyOptions,\n): Promise<T> {\n const result = await verifyStripeWebhook<T>({\n ...options,\n throwOnError: true,\n });\n\n return result.payload as T;\n}\n\n/**\n * Webhook provider plugin for Stripe.\n */\nexport const stripePlugin: WebhookProviderPlugin<\n StripeWebhookEvent,\n StripeVerifyOptions\n> = {\n name: \"stripe\",\n verify: verifyStripeWebhook,\n};\n","import { computeSubtleHmac, timingSafeEqual } from \"./crypto/subtle.js\";\nimport {\n SignatureVerificationError,\n TimestampToleranceError,\n WebhookKitError,\n} from \"./errors.js\";\nimport { githubPlugin, type GithubWebhookEvent } from \"./providers/github.js\";\nimport {\n shopifyPlugin,\n type ShopifyWebhookEvent,\n} from \"./providers/shopify.js\";\nimport {\n stripePlugin,\n type StripeWebhookEvent,\n validateTimestampTolerance,\n} from \"./providers/stripe.js\";\nimport type {\n BuiltinProviderName,\n HeaderRecord,\n WebhookProviderPlugin,\n WebhookVerificationResult,\n WebhookVerifyOptions,\n} from \"./types.js\";\nimport { checkAndRecordReplay, resolveWebhookEventId } from \"./store.js\";\nimport { extractRawBody } from \"./utils/extract-body.js\";\n\nconst GENERIC_SIGNATURE_HEADERS = [\n \"x-webhook-signature\",\n \"x-signature\",\n \"x-hub-signature-256\",\n \"stripe-signature\",\n \"x-shopify-hmac-sha256\",\n] as const;\n\nconst GENERIC_TIMESTAMP_HEADERS = [\n \"x-webhook-timestamp\",\n \"x-timestamp\",\n \"webhook-timestamp\",\n] as const;\n\nconst HEX_SCHEME_PREFIX_REGEX = /^(?:sha-?(?:256|384|512|1)|v1)=/i;\n\n/**\n * Built-in provider plugin registry keyed by lowercase provider name.\n */\nexport const PROVIDER_REGISTRY: Record<\n BuiltinProviderName,\n WebhookProviderPlugin<unknown, never>\n> = {\n stripe: stripePlugin as WebhookProviderPlugin<unknown, never>,\n github: githubPlugin as WebhookProviderPlugin<unknown, never>,\n shopify: shopifyPlugin as WebhookProviderPlugin<unknown, never>,\n};\n\n/**\n * Case-insensitively extracts a single header value from a `Headers` instance or plain record.\n *\n * @param headers - Web standard `Headers` instance or record.\n * @param targetName - Lowercase target header name.\n * @returns Header value string or undefined.\n */\nfunction extractHeaderValue(\n headers: HeaderRecord,\n targetName: string,\n): string | undefined {\n if (typeof Headers !== \"undefined\" && headers instanceof Headers) {\n return headers.get(targetName) ?? undefined;\n }\n\n const record = headers as Record<string, string | string[] | undefined>;\n const lowerTarget = targetName.toLowerCase();\n\n for (const key of Object.keys(record)) {\n if (key.toLowerCase() === lowerTarget) {\n const value = record[key];\n\n return Array.isArray(value) ? value[0] : value;\n }\n }\n\n return undefined;\n}\n\n/**\n * Searches a list of candidate header names in a `Headers` or record object.\n *\n * @param headers - Headers source.\n * @param candidateNames - Ordered list of lowercase header names to check.\n * @returns First matching non-empty header value or undefined.\n */\nfunction findFirstHeader(\n headers: HeaderRecord | undefined,\n candidateNames: readonly string[],\n): string | undefined {\n if (!headers) {\n return undefined;\n }\n\n for (const name of candidateNames) {\n const val = extractHeaderValue(headers, name);\n\n if (val) {\n return val;\n }\n }\n\n return undefined;\n}\n\n/**\n * Resolves the signature string from explicit options, headers record, or a `Request` body.\n *\n * @param options - Verification options.\n * @returns Resolved signature string or undefined.\n */\nfunction resolveGenericSignature(\n options: WebhookVerifyOptions,\n): string | undefined {\n if (options.signature) {\n return options.signature;\n }\n\n const fromHeaders = findFirstHeader(\n options.headers,\n GENERIC_SIGNATURE_HEADERS,\n );\n\n if (fromHeaders) {\n return fromHeaders;\n }\n\n if (typeof Request !== \"undefined\" && options.body instanceof Request) {\n return findFirstHeader(options.body.headers, GENERIC_SIGNATURE_HEADERS);\n }\n\n return undefined;\n}\n\n/**\n * Resolves an optional timestamp value from explicit options, headers record, or a `Request` body.\n *\n * @param options - Verification options.\n * @returns Raw timestamp value or undefined.\n */\nfunction resolveRawTimestamp(\n options: WebhookVerifyOptions,\n): number | string | undefined {\n if (options.timestamp !== undefined) {\n return options.timestamp;\n }\n\n const fromHeaders = findFirstHeader(\n options.headers,\n GENERIC_TIMESTAMP_HEADERS,\n );\n\n if (fromHeaders !== undefined) {\n return fromHeaders;\n }\n\n if (typeof Request !== \"undefined\" && options.body instanceof Request) {\n return findFirstHeader(options.body.headers, GENERIC_TIMESTAMP_HEADERS);\n }\n\n return undefined;\n}\n\n/**\n * Parses a raw timestamp number or string into a positive integer Unix timestamp.\n *\n * @param rawTimestamp - Raw timestamp from options or headers.\n * @returns Positive integer timestamp or `null` if invalid.\n */\nfunction parseTimestampValue(rawTimestamp: number | string): number | null {\n const parsed =\n typeof rawTimestamp === \"number\"\n ? rawTimestamp\n : parseInt(String(rawTimestamp).trim(), 10);\n\n if (!Number.isInteger(parsed) || parsed <= 0) {\n return null;\n }\n\n return parsed;\n}\n\n/**\n * Parses and validates an optional timestamp against the configured tolerance window.\n *\n * @param options - Verification options.\n * @returns Parsed timestamp or a `TimestampToleranceError`.\n */\nfunction verifyGenericTimestamp(options: WebhookVerifyOptions): {\n timestamp?: number;\n error?: TimestampToleranceError;\n} {\n const rawTimestamp = resolveRawTimestamp(options);\n\n if (rawTimestamp === undefined) {\n return {};\n }\n\n const parsed = parseTimestampValue(rawTimestamp);\n const tolerance = options.toleranceSeconds ?? 300;\n\n if (parsed === null) {\n return {\n error: new TimestampToleranceError(\n 0,\n tolerance,\n \"Invalid webhook timestamp header or option\",\n ),\n };\n }\n\n try {\n validateTimestampTolerance(parsed, tolerance, options.currentTimestamp);\n\n return { timestamp: parsed };\n } catch (err) {\n return {\n error:\n err instanceof TimestampToleranceError\n ? err\n : new TimestampToleranceError(parsed, tolerance),\n };\n }\n}\n\n/**\n * Checks whether a secret string or byte array is non-empty.\n *\n * @param secret - Candidate secret key.\n * @returns `true` if secret is present and non-empty.\n */\nfunction hasSecret(secret: string | Uint8Array | undefined): boolean {\n if (!secret) {\n return false;\n }\n\n return typeof secret === \"string\" ? secret.length > 0 : secret.byteLength > 0;\n}\n\n/**\n * Normalizes a candidate signature string according to the target encoding.\n *\n * @param signature - Raw signature string.\n * @param encoding - Digest encoding (`'hex'` or `'base64'`).\n * @returns Normalized signature string for constant-time comparison.\n */\nfunction normalizeCandidateSignature(\n signature: string,\n encoding: \"hex\" | \"base64\",\n): string {\n const trimmed = signature.trim();\n\n if (encoding === \"base64\") {\n return trimmed;\n }\n\n // WHY: Strip common scheme prefixes (e.g. 'sha256=', 'v1=') and lowercase hex\n // so generic HMAC verification works seamlessly across standard webhook formats.\n return trimmed.replace(HEX_SCHEME_PREFIX_REGEX, \"\").trim().toLowerCase();\n}\n\n/**\n * Computes a normalized HMAC digest string for comparison.\n *\n * @param options - Verification options.\n * @param data - Signing payload string.\n * @param encoding - Digest encoding (`'hex'` or `'base64'`).\n * @returns Normalized digest string.\n */\nasync function computeNormalizedDigest(\n options: WebhookVerifyOptions,\n data: string,\n encoding: \"hex\" | \"base64\",\n): Promise<string> {\n const digest = await computeSubtleHmac(\n options.algorithm ?? \"SHA-256\",\n options.secret,\n data,\n encoding,\n );\n\n return encoding === \"hex\" ? digest.toLowerCase() : digest;\n}\n\n/**\n * Compares candidate signature against HMAC of rawBody (and `${timestamp}.${rawBody}` if timestamped).\n *\n * @param options - Verification options.\n * @param rawBody - Extracted raw body string.\n * @param rawSignature - Raw signature header or option.\n * @param timestamp - Optional validated Unix timestamp.\n * @returns `true` if signature matches expected HMAC.\n */\nasync function matchesGenericHmac(\n options: WebhookVerifyOptions,\n rawBody: string,\n rawSignature: string,\n timestamp?: number,\n): Promise<boolean> {\n const encoding = options.encoding ?? \"hex\";\n const candidate = normalizeCandidateSignature(rawSignature, encoding);\n\n if (!candidate) {\n return false;\n }\n\n const rawExpected = await computeNormalizedDigest(options, rawBody, encoding);\n let matched = timingSafeEqual(rawExpected, candidate);\n\n if (timestamp !== undefined) {\n // WHY: Support both raw-body signing and `${timestamp}.${rawBody}` signing\n // when a timestamp is supplied in generic verification mode.\n const tsExpected = await computeNormalizedDigest(\n options,\n `${timestamp}.${rawBody}`,\n encoding,\n );\n\n if (timingSafeEqual(tsExpected, candidate)) {\n matched = true;\n }\n }\n\n return matched;\n}\n\n/**\n * Constructs a failed `WebhookVerificationResult` or throws if `throwOnError` is enabled.\n *\n * @param rawBody - Extracted raw body string.\n * @param error - `WebhookKitError` instance.\n * @param throwOnError - Whether to throw the error immediately.\n * @param timestamp - Optional parsed timestamp.\n * @returns Failed verification result.\n */\nfunction failVerification(\n rawBody: string,\n error: WebhookKitError,\n throwOnError?: boolean,\n timestamp?: number,\n eventId?: string,\n): WebhookVerificationResult<never> {\n if (throwOnError) {\n throw error;\n }\n\n return {\n valid: false,\n rawBody,\n timestamp,\n eventId,\n error,\n };\n}\n\n/**\n * Safely parses a raw body string as JSON, returning `undefined` on syntax errors.\n *\n * @param rawBody - Raw body string.\n * @returns Parsed JSON value or `undefined`.\n */\nfunction tryParseJson<T>(rawBody: string): T | undefined {\n try {\n return JSON.parse(rawBody) as T;\n } catch {\n // WHY: Silently ignore JSON parse errors when rawBody is not valid JSON\n // so signature verification still succeeds for plain-text or form-encoded payloads.\n return undefined;\n }\n}\n\n/**\n * Executes a `WebhookProviderPlugin` and honors `options.throwOnError` if verification fails.\n *\n * @param plugin - Target webhook provider plugin.\n * @param options - Verification options.\n * @param enforceStoreAfterPlugin - Whether to run replay store enforcement after custom plugin verification.\n * @returns Provider verification result.\n */\nasync function runPluginVerification<T>(\n plugin: WebhookProviderPlugin<T, WebhookVerifyOptions>,\n options: WebhookVerifyOptions,\n enforceStoreAfterPlugin = false,\n): Promise<WebhookVerificationResult<T>> {\n const result = await plugin.verify(options);\n\n if (!result.valid) {\n if (options.throwOnError) {\n throw result.error;\n }\n\n return result;\n }\n\n if (!enforceStoreAfterPlugin) {\n return result;\n }\n\n const eventId =\n result.eventId ??\n resolveWebhookEventId({\n eventId: options.eventId,\n headers: options.headers,\n body: options.body,\n payload: result.payload,\n rawBody: result.rawBody,\n });\n const replayError = await checkAndRecordReplay({\n store: options.store,\n eventId,\n storeTtlSeconds: options.storeTtlSeconds,\n toleranceSeconds: options.toleranceSeconds,\n });\n\n if (replayError) {\n return failVerification(\n result.rawBody,\n replayError,\n options.throwOnError,\n result.timestamp,\n eventId,\n );\n }\n\n return {\n ...result,\n eventId,\n };\n}\n\n/**\n * Delegates verification to a named built-in provider or custom `WebhookProviderPlugin`.\n *\n * @param options - Verification options containing `provider`.\n * @returns Provider verification result.\n */\nasync function delegateToProvider<T>(\n options: WebhookVerifyOptions,\n): Promise<WebhookVerificationResult<T>> {\n const provider = options.provider!;\n\n if (typeof provider === \"object\") {\n return runPluginVerification(\n provider as WebhookProviderPlugin<T, WebhookVerifyOptions>,\n options,\n true,\n );\n }\n\n const normalizedName = String(provider)\n .toLowerCase()\n .trim() as BuiltinProviderName;\n const builtin = PROVIDER_REGISTRY[normalizedName];\n\n if (!builtin) {\n const rawBody = await extractRawBody(options.body);\n const error = new WebhookKitError(\n `Unsupported webhook provider: \"${String(provider)}\"`,\n \"UNSUPPORTED_PROVIDER\",\n );\n\n return failVerification(rawBody, error, options.throwOnError);\n }\n\n return runPluginVerification(\n builtin as WebhookProviderPlugin<T, WebhookVerifyOptions>,\n options,\n );\n}\n\n/**\n * Verifies a generic HMAC webhook signature when no specific provider plugin is selected.\n *\n * @param options - Generic verification options.\n * @returns Verification outcome with typed payload.\n */\nasync function verifyGenericWebhook<T>(\n options: WebhookVerifyOptions,\n): Promise<WebhookVerificationResult<T>> {\n const rawBody = await extractRawBody(options.body);\n const rawSignature = resolveGenericSignature(options);\n\n if (!hasSecret(options.secret) || !rawSignature) {\n const error = new SignatureVerificationError(\"Missing signature or secret\");\n\n return failVerification(rawBody, error, options.throwOnError);\n }\n\n const tsResult = verifyGenericTimestamp(options);\n\n if (tsResult.error) {\n return failVerification(rawBody, tsResult.error, options.throwOnError);\n }\n\n const isValid = await matchesGenericHmac(\n options,\n rawBody,\n rawSignature,\n tsResult.timestamp,\n );\n\n if (!isValid) {\n const error = new SignatureVerificationError();\n\n return failVerification(\n rawBody,\n error,\n options.throwOnError,\n tsResult.timestamp,\n );\n }\n\n const payload =\n options.parseJson !== false ? tryParseJson<T>(rawBody) : undefined;\n const eventId = resolveWebhookEventId({\n eventId: options.eventId,\n headers: options.headers,\n body: options.body,\n payload,\n rawBody,\n });\n const replayError = await checkAndRecordReplay({\n store: options.store,\n eventId,\n storeTtlSeconds: options.storeTtlSeconds,\n toleranceSeconds: options.toleranceSeconds,\n });\n\n if (replayError) {\n return failVerification(\n rawBody,\n replayError,\n options.throwOnError,\n tsResult.timestamp,\n eventId,\n );\n }\n\n return {\n valid: true,\n rawBody,\n timestamp: tsResult.timestamp,\n eventId,\n payload: payload as T,\n };\n}\n\n/**\n * Verifies an incoming Stripe webhook signature via the unified verification API.\n */\nexport async function verifyWebhookSignature<T = StripeWebhookEvent>(\n options: WebhookVerifyOptions & { provider: \"stripe\" },\n): Promise<WebhookVerificationResult<T>>;\n\n/**\n * Verifies an incoming GitHub webhook signature via the unified verification API.\n */\nexport async function verifyWebhookSignature<T = GithubWebhookEvent>(\n options: WebhookVerifyOptions & { provider: \"github\" },\n): Promise<WebhookVerificationResult<T>>;\n\n/**\n * Verifies an incoming Shopify webhook signature via the unified verification API.\n */\nexport async function verifyWebhookSignature<T = ShopifyWebhookEvent>(\n options: WebhookVerifyOptions & { provider: \"shopify\" },\n): Promise<WebhookVerificationResult<T>>;\n\n/**\n * Verifies an incoming webhook signature using a custom `WebhookProviderPlugin`.\n */\nexport async function verifyWebhookSignature<T>(\n options: WebhookVerifyOptions & { provider: WebhookProviderPlugin<T, never> },\n): Promise<WebhookVerificationResult<T>>;\n\n/**\n * Verifies an incoming webhook signature against the raw request body using constant-time HMAC check.\n *\n * @param options - Verification options including body, signature, secret, and optional provider or tolerance\n * @returns Verification result object with validity, extracted raw body, and narrowed payload or error\n */\nexport async function verifyWebhookSignature<T = unknown>(\n options: WebhookVerifyOptions,\n): Promise<WebhookVerificationResult<T>>;\n\nexport async function verifyWebhookSignature<T = unknown>(\n options: WebhookVerifyOptions,\n): Promise<WebhookVerificationResult<T>> {\n if (options.provider !== undefined) {\n return delegateToProvider<T>(options);\n }\n\n return verifyGenericWebhook<T>(options);\n}\n\n/**\n * Asserts that an incoming Stripe webhook signature is valid, returning the typed payload or throwing.\n */\nexport async function assertWebhookSignature<T = StripeWebhookEvent>(\n options: WebhookVerifyOptions & { provider: \"stripe\" },\n): Promise<T>;\n\n/**\n * Asserts that an incoming GitHub webhook signature is valid, returning the typed payload or throwing.\n */\nexport async function assertWebhookSignature<T = GithubWebhookEvent>(\n options: WebhookVerifyOptions & { provider: \"github\" },\n): Promise<T>;\n\n/**\n * Asserts that an incoming Shopify webhook signature is valid, returning the typed payload or throwing.\n */\nexport async function assertWebhookSignature<T = ShopifyWebhookEvent>(\n options: WebhookVerifyOptions & { provider: \"shopify\" },\n): Promise<T>;\n\n/**\n * Asserts that an incoming webhook signature is valid using a custom `WebhookProviderPlugin`.\n */\nexport async function assertWebhookSignature<T>(\n options: WebhookVerifyOptions & { provider: WebhookProviderPlugin<T, never> },\n): Promise<T>;\n\n/**\n * Asserts that an incoming webhook signature is valid, throwing a `WebhookKitError` if invalid.\n * Returns the typed payload if valid and parsed.\n *\n * @throws {SignatureVerificationError} If signature verification fails\n * @throws {TimestampToleranceError} If timestamp tolerance is exceeded\n */\nexport async function assertWebhookSignature<T = unknown>(\n options: WebhookVerifyOptions,\n): Promise<T>;\n\nexport async function assertWebhookSignature<T = unknown>(\n options: WebhookVerifyOptions,\n): Promise<T> {\n const result = await verifyWebhookSignature<T>({\n ...options,\n throwOnError: true,\n });\n\n if (!result.valid) {\n throw result.error;\n }\n\n return result.payload;\n}\n","import {\n PayloadExtractionError,\n ReplayAttackError,\n WebhookKitError,\n} from \"./errors.js\";\nimport type { GithubWebhookEvent } from \"./providers/github.js\";\nimport type { ShopifyWebhookEvent } from \"./providers/shopify.js\";\nimport type { StripeWebhookEvent } from \"./providers/stripe.js\";\nimport type {\n BuiltinProviderName,\n SupportedAlgorithm,\n WebhookProviderPlugin,\n WebhookReplayStore,\n WebhookVerificationSuccess,\n} from \"./types.js\";\nimport { verifyWebhookSignature } from \"./verify.js\";\n\nconst JSON_CONTENT_TYPE = \"application/json; charset=utf-8\";\n\nconst DEFAULT_SUCCESS_BODY = JSON.stringify({ received: true });\n\n/**\n * Structural duck-type for Hono's `Context` (and compatible edge routers that expose\n * the underlying Web Standard `Request` at `req.raw`).\n */\nexport interface WebhookLikeContext {\n readonly req: {\n readonly raw: Request;\n };\n}\n\n/**\n * Supported first-argument inputs to a universal webhook route handler:\n * - Web Standard `Request` (Next.js App Router, Remix, SvelteKit, Astro, Cloudflare Workers, Bun, Deno)\n * - Framework context object exposing `{ req: { raw: Request } }` (Hono)\n */\nexport type WebhookRequestInput = Request | WebhookLikeContext;\n\n/**\n * Static secret value or dynamic secret resolver function.\n */\nexport type SecretResolver<TContext = Request> =\n | string\n | Uint8Array\n | ((\n context: TContext,\n request: Request,\n ) => string | Uint8Array | Promise<string | Uint8Array>);\n\n/**\n * Static replay store instance or dynamic store resolver function.\n */\nexport type ReplayStoreResolver<TContext = Request> =\n | WebhookReplayStore\n | ((\n context: TContext,\n request: Request,\n ) =>\n WebhookReplayStore | undefined | Promise<WebhookReplayStore | undefined>);\n\n/**\n * Metadata passed as the second argument to `onEvent` alongside the verified payload.\n */\nexport interface WebhookEventMeta<TContext = Request> {\n readonly context: TContext;\n readonly request: Request;\n readonly rawBody: string;\n readonly timestamp?: number;\n readonly eventId?: string;\n}\n\n/**\n * Metadata passed as the second argument to `onError` when verification fails.\n */\nexport interface WebhookErrorMeta<TContext = Request> {\n readonly context: TContext;\n readonly request: Request;\n readonly rawBody?: string;\n readonly timestamp?: number;\n readonly eventId?: string;\n}\n\n/**\n * Return value allowed from `onEvent`.\n */\nexport type WebhookHandlerReturn =\n Response | Record<string, unknown> | void | undefined;\n\n/**\n * Configuration options for `createWebhookHandler`.\n */\nexport interface WebhookHandlerOptions<\n TPayload = unknown,\n TContext extends WebhookRequestInput = Request,\n> {\n /**\n * Optional built-in provider name (`'stripe'`, `'github'`, `'shopify'`) or custom provider plugin.\n * When omitted, generic HMAC verification is performed.\n */\n provider?: BuiltinProviderName | WebhookProviderPlugin<TPayload, never>;\n\n /**\n * Shared signing secret or dynamic resolver function receiving `(context, request)`.\n */\n secret: SecretResolver<TContext>;\n\n /**\n * Optional idempotency / replay store (`has(eventId)` / `set(eventId, ttl)`) or dynamic resolver function.\n */\n store?: ReplayStoreResolver<TContext>;\n\n /**\n * Optional explicit webhook event ID for idempotency / replay store tracking.\n */\n eventId?: string;\n\n /**\n * Optional TTL in seconds for entries recorded in `store` (defaults to `toleranceSeconds` or `300`).\n */\n storeTtlSeconds?: number;\n\n /**\n * Callback invoked after the webhook signature and timestamp have been verified.\n */\n onEvent: (\n event: TPayload,\n meta: WebhookEventMeta<TContext>,\n ) => WebhookHandlerReturn | Promise<WebhookHandlerReturn>;\n\n /**\n * Optional custom error responder invoked when signature verification or body extraction fails.\n */\n onError?: (\n error: WebhookKitError,\n meta: WebhookErrorMeta<TContext>,\n ) => Response | Promise<Response>;\n\n /**\n * Cryptographic hash algorithm for generic HMAC verification. Defaults to `'SHA-256'`.\n */\n algorithm?: SupportedAlgorithm;\n\n /**\n * Signature digest encoding for generic HMAC verification. Defaults to `'hex'`.\n */\n encoding?: \"hex\" | \"base64\";\n\n /**\n * Maximum allowed age of the webhook in seconds to mitigate replay attacks.\n */\n toleranceSeconds?: number;\n\n /**\n * Current Unix timestamp in seconds (defaults to `Math.floor(Date.now() / 1000)`).\n */\n currentTimestamp?: number;\n\n /**\n * Whether to automatically parse the verified body as JSON. Defaults to `true`.\n */\n parseJson?: boolean;\n}\n\n/**\n * Universal webhook route handler function produced by `createWebhookHandler`.\n */\nexport type UniversalWebhookHandler<\n TContext extends WebhookRequestInput = Request,\n> = (input: TContext) => Promise<Response>;\n\n/**\n * Creates a JSON `Response` with UTF-8 content-type headers.\n *\n * @param body - Serialized JSON string.\n * @param status - HTTP status code.\n * @returns Web Standard `Response`.\n */\nfunction createJsonResponse(body: string, status: number): Response {\n return new Response(body, {\n status,\n headers: {\n \"content-type\": JSON_CONTENT_TYPE,\n },\n });\n}\n\n/**\n * Resolves the appropriate HTTP status code for a `WebhookKitError`.\n *\n * @param error - Verification, replay, or extraction error.\n * @returns `400` for extraction errors, `409` for duplicate event replays, or `401` for verification errors.\n */\nfunction resolveErrorStatus(error: WebhookKitError): number {\n if (error instanceof PayloadExtractionError) {\n return 400;\n }\n\n if (error instanceof ReplayAttackError) {\n return 409;\n }\n\n return 401;\n}\n\n/**\n * Converts a `WebhookKitError` into a structured JSON HTTP error `Response`.\n *\n * @param error - Verification or extraction error.\n * @returns `400`, `409`, or `401` JSON error response.\n */\nfunction createErrorResponse(error: WebhookKitError): Response {\n // WHY: Map stream/body extraction failures to 400 Bad Request, duplicate replay\n // detections to 409 Conflict, and cryptographic/header failures to 401 Unauthorized.\n const status = resolveErrorStatus(error);\n\n return createJsonResponse(\n JSON.stringify({\n error: {\n code: error.code,\n message: error.message,\n },\n }),\n status,\n );\n}\n\n/**\n * Checks whether an unknown value is a non-null object record.\n *\n * @param value - Candidate value.\n * @returns `true` if `value` is a non-null object.\n */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/**\n * Extracts a Web Standard `Request` from a framework context object (`{ req: { raw: Request } }`).\n *\n * @param value - Candidate context object.\n * @returns Extracted `Request` or `null` if not present.\n */\nfunction extractRequestFromContext(value: unknown): Request | null {\n if (!isRecord(value) || !isRecord(value.req)) {\n return null;\n }\n\n return typeof Request !== \"undefined\" && value.req.raw instanceof Request\n ? value.req.raw\n : null;\n}\n\n/**\n * Unwraps a Web Standard `Request` from either a direct `Request` or a `WebhookLikeContext`.\n *\n * @param input - Handler first argument.\n * @returns Underlying `Request` or `null` if invalid.\n */\nfunction unwrapRequest(input: unknown): Request | null {\n if (typeof Request !== \"undefined\" && input instanceof Request) {\n return input;\n }\n\n return extractRequestFromContext(input);\n}\n\n/**\n * Resolves the signing secret from a static value or dynamic secret resolver function.\n *\n * @param secret - Static secret or resolver function.\n * @param context - Original handler input (`Request` or framework context).\n * @param request - Unwrapped Web Standard `Request`.\n * @returns Resolved secret string or byte array.\n */\nasync function resolveSecret<TContext extends WebhookRequestInput>(\n secret: SecretResolver<TContext>,\n context: TContext,\n request: Request,\n): Promise<string | Uint8Array> {\n if (typeof secret === \"function\") {\n return secret(context, request);\n }\n\n return secret;\n}\n\n/**\n * Resolves the replay store from a static store object or dynamic resolver function.\n *\n * @param store - Static replay store or resolver function.\n * @param context - Original handler input (`Request` or framework context).\n * @param request - Unwrapped Web Standard `Request`.\n * @returns Resolved `WebhookReplayStore` or `undefined`.\n */\nasync function resolveStore<TContext extends WebhookRequestInput>(\n store: ReplayStoreResolver<TContext> | undefined,\n context: TContext,\n request: Request,\n): Promise<WebhookReplayStore | undefined> {\n if (typeof store === \"function\") {\n return store(context, request);\n }\n\n return store;\n}\n\n/**\n * Converts the return value of `onEvent` into a Web Standard `Response`.\n *\n * @param output - Return value from `onEvent`.\n * @returns Web Standard `Response`.\n */\nfunction formatSuccessResponse(output: WebhookHandlerReturn): Response {\n if (typeof Response !== \"undefined\" && output instanceof Response) {\n return output;\n }\n\n if (output === undefined) {\n return createJsonResponse(DEFAULT_SUCCESS_BODY, 200);\n }\n\n return createJsonResponse(JSON.stringify(output), 200);\n}\n\n/**\n * Produces an error `Response` via `options.onError` if configured, or the default JSON responder.\n *\n * @param error - `WebhookKitError` instance.\n * @param meta - Error metadata including context, request, rawBody, and timestamp.\n * @param onError - Optional user-supplied error callback.\n * @returns Error `Response`.\n */\nasync function handleVerificationFailure<TContext extends WebhookRequestInput>(\n error: WebhookKitError,\n meta: WebhookErrorMeta<TContext>,\n onError?: WebhookHandlerOptions<unknown, TContext>[\"onError\"],\n): Promise<Response> {\n if (onError) {\n return onError(error, meta);\n }\n\n return createErrorResponse(error);\n}\n\n/**\n * Resolves the secret and executes `verifyWebhookSignature`, catching `WebhookKitError` exceptions.\n *\n * @param options - Handler configuration options.\n * @param context - Handler input context.\n * @param request - Unwrapped Web Standard `Request`.\n * @returns Discriminated verification outcome for handler dispatch.\n */\nasync function verifyRequest<TPayload, TContext extends WebhookRequestInput>(\n options: WebhookHandlerOptions<TPayload, TContext>,\n context: TContext,\n request: Request,\n): Promise<\n | { ok: true; result: WebhookVerificationSuccess<TPayload> }\n | {\n ok: false;\n error: WebhookKitError;\n rawBody?: string;\n timestamp?: number;\n eventId?: string;\n }\n> {\n try {\n const secret = await resolveSecret(options.secret, context, request);\n const store = await resolveStore(options.store, context, request);\n const result = await verifyWebhookSignature<TPayload>({\n body: request,\n secret,\n provider: options.provider,\n algorithm: options.algorithm,\n encoding: options.encoding,\n toleranceSeconds: options.toleranceSeconds,\n currentTimestamp: options.currentTimestamp,\n store,\n eventId: options.eventId,\n storeTtlSeconds: options.storeTtlSeconds,\n parseJson: options.parseJson,\n });\n\n if (!result.valid) {\n return {\n ok: false,\n error: result.error,\n rawBody: result.rawBody,\n timestamp: result.timestamp,\n eventId: result.eventId,\n };\n }\n\n return { ok: true, result };\n } catch (err) {\n // WHY: `extractRawBody` throws `PayloadExtractionError` when a Request stream\n // was already consumed prior to reaching the handler; convert it into a structured failure.\n if (err instanceof WebhookKitError) {\n return { ok: false, error: err };\n }\n\n throw err;\n }\n}\n\n/**\n * Creates a universal Stripe webhook route handler for Next.js App Router, Hono, and WinterCG runtimes.\n */\nexport function createWebhookHandler<\n TPayload = StripeWebhookEvent,\n TContext extends WebhookRequestInput = Request,\n>(\n options: WebhookHandlerOptions<TPayload, TContext> & { provider: \"stripe\" },\n): UniversalWebhookHandler<TContext>;\n\n/**\n * Creates a universal GitHub webhook route handler for Next.js App Router, Hono, and WinterCG runtimes.\n */\nexport function createWebhookHandler<\n TPayload = GithubWebhookEvent,\n TContext extends WebhookRequestInput = Request,\n>(\n options: WebhookHandlerOptions<TPayload, TContext> & { provider: \"github\" },\n): UniversalWebhookHandler<TContext>;\n\n/**\n * Creates a universal Shopify webhook route handler for Next.js App Router, Hono, and WinterCG runtimes.\n */\nexport function createWebhookHandler<\n TPayload = ShopifyWebhookEvent,\n TContext extends WebhookRequestInput = Request,\n>(\n options: WebhookHandlerOptions<TPayload, TContext> & { provider: \"shopify\" },\n): UniversalWebhookHandler<TContext>;\n\n/**\n * Creates a universal webhook route handler using a custom `WebhookProviderPlugin`.\n */\nexport function createWebhookHandler<\n TPayload,\n TContext extends WebhookRequestInput = Request,\n>(\n options: WebhookHandlerOptions<TPayload, TContext> & {\n provider: WebhookProviderPlugin<TPayload, never>;\n },\n): UniversalWebhookHandler<TContext>;\n\n/**\n * Creates a universal webhook route handler that unwraps a Web Standard `Request` (Next.js App Router,\n * Cloudflare Workers, Remix, Bun, Deno) or `{ req: { raw: Request } }` context (Hono), verifies the\n * cryptographic signature, and dispatches the typed payload to `onEvent`.\n *\n * @param options - Provider, secret resolver, event handler, and optional verification settings.\n * @returns Universal async route handler `(input: TContext) => Promise<Response>`.\n */\nexport function createWebhookHandler<\n TPayload = unknown,\n TContext extends WebhookRequestInput = Request,\n>(\n options: WebhookHandlerOptions<TPayload, TContext>,\n): UniversalWebhookHandler<TContext>;\n\nexport function createWebhookHandler<\n TPayload = unknown,\n TContext extends WebhookRequestInput = Request,\n>(\n options: WebhookHandlerOptions<TPayload, TContext>,\n): UniversalWebhookHandler<TContext> {\n return async (input: TContext): Promise<Response> => {\n const request = unwrapRequest(input);\n\n if (!request) {\n const extractionError = new PayloadExtractionError(\n \"Invalid webhook handler input: expected Web Standard Request or context with { req: { raw: Request } }\",\n );\n\n return createErrorResponse(extractionError);\n }\n\n const outcome = await verifyRequest(options, input, request);\n\n if (!outcome.ok) {\n return handleVerificationFailure(\n outcome.error,\n {\n context: input,\n request,\n rawBody: outcome.rawBody,\n timestamp: outcome.timestamp,\n eventId: outcome.eventId,\n },\n options.onError,\n );\n }\n\n // WHY: Do not wrap `onEvent` in try/catch so unhandled application errors\n // propagate naturally as 500s rather than being misreported as 401/400 verification errors.\n const output = await options.onEvent(outcome.result.payload, {\n context: input,\n request,\n rawBody: outcome.result.rawBody,\n timestamp: outcome.result.timestamp,\n eventId: outcome.result.eventId,\n });\n\n return formatSuccessResponse(output);\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,151 @@
1
+ # Agent Coding & Review Rules: `webhook-kit`
2
+
3
+ This document defines mandatory guidelines, conventions, and anti-patterns for AI coding assistants (such as Cursor, Claude Code, GitHub Copilot, and Gemini) when writing, refactoring, or reviewing code using or extending `webhook-kit`.
4
+
5
+ ---
6
+
7
+ ## 1. Core Principles & Philosophy
8
+
9
+ 1. **Zero Runtime Dependencies**: `webhook-kit` core has zero third-party dependencies.
10
+ 2. **Universal Edge Compatibility**: Works identically across Node.js (22+), Bun, Deno, Cloudflare Workers, Next.js Edge, and Fastly Compute.
11
+ 3. **Web Standards First**: Relies entirely on Web APIs (`crypto.subtle`, `Request`, `Response`, `Headers`, `TextEncoder`, `TextDecoder`, `Uint8Array`). Never import `node:crypto` into core library modules.
12
+ 4. **Cryptographic Rigor**: All signature verifications and comparisons must be timing-safe to eliminate side-channel vulnerabilities.
13
+
14
+ ---
15
+
16
+ ## 2. The #1 Anti-Pattern: The JSON Re-serialization Trap
17
+
18
+ ### Fatal Mistake: Re-stringifying Parsed JSON
19
+
20
+ ```ts
21
+ // WRONG: Calling .json() parses the payload, stripping whitespace and reordering keys.
22
+ // Calling JSON.stringify() produces a DIFFERENT string than the sender sent!
23
+ // HMAC check will ALWAYS fail in production.
24
+ export async function POST(req: Request) {
25
+ const json = await req.json();
26
+ const signature = req.headers.get("stripe-signature");
27
+ const valid = await verifyWebhookSignature({
28
+ body: JSON.stringify(json), // BREAKS SIGNATURE VERIFICATION
29
+ signature,
30
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
31
+ });
32
+ }
33
+ ```
34
+
35
+ ### Correct Pattern: Capture Raw Body First
36
+
37
+ ```ts
38
+ // CORRECT: Extract raw text or clone the Request stream BEFORE parsing JSON.
39
+ export async function POST(req: Request) {
40
+ const rawBody = await req.text(); // or pass the Request directly to verifyWebhookSignature
41
+ const signature = req.headers.get("stripe-signature")!;
42
+
43
+ const result = await verifyWebhookSignature({
44
+ body: rawBody,
45
+ signature,
46
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
47
+ });
48
+
49
+ if (!result.valid) {
50
+ return new Response("Invalid signature", { status: 400 });
51
+ }
52
+
53
+ // Safely use typed payload parsed after signature verification
54
+ const payload = result.payload;
55
+ }
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 3. Cryptography Standards
61
+
62
+ 1. **Always Use Constant-Time Equality**:
63
+ - Never use `===` or `!==` for signature verification strings or buffers.
64
+ - Always route comparisons through `timingSafeEqual(expectedBytes, actualBytes)`.
65
+ 2. **Web Cryptography API**:
66
+ - Use `globalThis.crypto.subtle.importKey(...)` and `globalThis.crypto.subtle.sign(...)`.
67
+ - Never import `node:crypto` into `src/`.
68
+ 3. **Replay Attack Mitigation & Idempotency**:
69
+ - Always enforce a timestamp tolerance check when the webhook provider includes a timestamp header (e.g., Stripe `t=...`).
70
+ - Default tolerance: 300 seconds (5 minutes).
71
+ - For deduplication across retries, pass `store: createInMemoryReplayStore()` or a `WebhookReplayStore` adapter (`has(eventId)` / `set(eventId, ttl)`) backed by Redis, Upstash, or Cloudflare KV.
72
+
73
+ ---
74
+
75
+ ## 4. Framework Integration Recipes
76
+
77
+ ### Next.js App Router (`app/api/webhook/route.ts`)
78
+
79
+ Prefer `createWebhookHandler` for zero-boilerplate route handlers, or pass the Web Standard `Request` directly to `verifyWebhookSignature`:
80
+
81
+ ```ts
82
+ import { createWebhookHandler } from "@amirabaris/webhook-kit";
83
+
84
+ export const POST = createWebhookHandler({
85
+ provider: "stripe",
86
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
87
+ onEvent: async (event) => {
88
+ // event is strongly typed StripeWebhookEvent
89
+ console.log("Verified Stripe event:", event.id);
90
+ },
91
+ });
92
+ ```
93
+
94
+ ### Hono (Edge / Node / Bun / Cloudflare)
95
+
96
+ `createWebhookHandler` unwraps Hono's `Context` (`c.req.raw`) and resolves secrets from `c.env`:
97
+
98
+ ```ts
99
+ import { Hono } from "hono";
100
+ import { createWebhookHandler } from "@amirabaris/webhook-kit";
101
+
102
+ type Bindings = {
103
+ GITHUB_WEBHOOK_SECRET: string;
104
+ };
105
+
106
+ const app = new Hono<{ Bindings: Bindings }>();
107
+
108
+ app.post(
109
+ "/webhook",
110
+ createWebhookHandler({
111
+ provider: "github",
112
+ secret: (c) => c.env.GITHUB_WEBHOOK_SECRET,
113
+ onEvent: async (event) => {
114
+ console.log("Verified GitHub action:", event.action);
115
+ },
116
+ }),
117
+ );
118
+ ```
119
+
120
+ ---
121
+
122
+ ## 5. Commit & Contribution Rules
123
+
124
+ When creating commits, PRs, or contributing code to `webhook-kit`:
125
+
126
+ - **Conventional Commits**: Format strictly as `type(scope): brief description in imperative mood`.
127
+ - **Allowed Types**:
128
+ - `feat`: New feature or provider implementation
129
+ - `fix`: Bug fix
130
+ - `docs`: Documentation only changes
131
+ - `test`: Adding or correcting tests
132
+ - `refactor`: Code change that neither fixes a bug nor adds a feature
133
+ - `chore`: Tooling, build config, dependency updates
134
+ - **Subject Line**:
135
+ - All lowercase type and scope.
136
+ - Imperative mood (e.g., `feat(crypto): add subtle timing-safe compare`, NOT `added` or `adds`).
137
+ - No trailing period.
138
+ - Commits must be atomic (one single logical concern).
139
+
140
+ ---
141
+
142
+ ## 6. Code Review Checklist for AI Agents
143
+
144
+ Before generating or approving code:
145
+
146
+ - [ ] Is raw body retained without intermediate JSON parsing?
147
+ - [ ] Are timing-safe comparisons used for all HMAC/signature checks?
148
+ - [ ] Is the code free from `node:crypto` imports in universal source code?
149
+ - [ ] Are TypeScript types strict (`noImplicitAny`, proper generics on `verifyWebhookSignature<T>`)?
150
+ - [ ] Is test coverage included with both valid signatures and tampered payload assertions?
151
+ - [ ] Does the commit message strictly follow Conventional Commits?
package/package.json CHANGED
@@ -1,6 +1,86 @@
1
1
  {
2
2
  "name": "@amirabaris/webhook-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Zero-dependency, edge-compatible webhook signature verification toolkit using Web Cryptography API",
5
+ "keywords": [
6
+ "webhook",
7
+ "webhooks",
8
+ "signature",
9
+ "verification",
10
+ "hmac",
11
+ "crypto",
12
+ "crypto-subtle",
13
+ "edge",
14
+ "cloudflare-workers",
15
+ "nextjs",
16
+ "hono",
17
+ "fastify"
18
+ ],
19
+ "homepage": "https://github.com/AmirAbaris/webhook-kit#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/AmirAbaris/webhook-kit/issues"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/AmirAbaris/webhook-kit.git"
26
+ },
27
+ "license": "MIT",
28
+ "author": "Amir Abaris",
29
+ "type": "module",
30
+ "sideEffects": false,
31
+ "main": "./dist/index.cjs",
32
+ "module": "./dist/index.mjs",
33
+ "types": "./dist/index.d.ts",
34
+ "bin": {
35
+ "webhook-kit": "./bin/webhook-kit.mjs"
36
+ },
37
+ "exports": {
38
+ ".": {
39
+ "types": {
40
+ "import": "./dist/index.d.mts",
41
+ "require": "./dist/index.d.cts",
42
+ "default": "./dist/index.d.ts"
43
+ },
44
+ "import": "./dist/index.mjs",
45
+ "require": "./dist/index.cjs",
46
+ "default": "./dist/index.mjs"
47
+ },
48
+ "./package.json": "./package.json"
49
+ },
50
+ "files": [
51
+ "dist",
52
+ "bin",
53
+ "docs/agent-rules.md",
54
+ "README.md",
55
+ "LICENSE"
56
+ ],
57
+ "devDependencies": {
58
+ "@stylistic/eslint-plugin": "^5.10.0",
59
+ "@types/node": "^26.6.6",
60
+ "eslint": "^10.14.0",
61
+ "lefthook": "^2.2.2",
62
+ "oxlint": "^1.90.0",
63
+ "prettier": "^3.9.10",
64
+ "tsd": "^0.33.0",
65
+ "tsdown": "^0.23.4",
66
+ "typescript": "^6.0.3",
67
+ "typescript-eslint": "^8.72.1",
68
+ "unrun": "^0.3.1",
69
+ "vitest": "^2.1.9"
70
+ },
71
+ "engines": {
72
+ "node": ">=22.0.0"
73
+ },
74
+ "publishConfig": {
75
+ "access": "public"
76
+ },
77
+ "scripts": {
78
+ "build": "tsdown --config-loader native",
79
+ "test": "vitest run",
80
+ "test:watch": "vitest",
81
+ "test:types": "tsd --files 'tests-d/**/*.test-d.ts'",
82
+ "test:smoke": "node ./tests/package-smoke.mjs",
83
+ "lint": "oxlint . && eslint . && prettier --check . && tsc -p tsconfig.lint.json --noEmit",
84
+ "format": "eslint --fix . && prettier --write ."
85
+ }
6
86
  }