@amirabaris/webhook-kit 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["extractHeaderValue","extractHeaderValue","extractHeaderValue","extractHeaderValue","extractHeaderValue","failVerification","tryParseJson"],"sources":["../src/errors.ts","../src/utils/headers.ts","../src/store/store.ts","../src/crypto/subtle.ts","../src/providers/common/assertions.ts","../src/providers/github/signature.ts","../src/utils/extract-body.ts","../src/providers/common/helpers.ts","../src/providers/github/verify.ts","../src/providers/shopify/signature.ts","../src/providers/shopify/verify.ts","../src/providers/stripe/signature.ts","../src/providers/stripe/verify.ts","../src/verify.ts","../src/handler/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 type { HeaderRecord } from \"../types.js\";\n\n/**\n * Extracts a header value from a plain headers record case-insensitively.\n *\n * @param record - Plain headers record object.\n * @param lowerTarget - Pre-lowercased target header name.\n * @returns Header value string or undefined.\n */\nfunction extractFromRecord(\n record: Record<string, string | string[] | undefined>,\n lowerTarget: string,\n): string | undefined {\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 * 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 */\nexport function extractHeaderValue(\n headers: HeaderRecord | undefined | null,\n targetName: string,\n): string | undefined {\n if (!headers || typeof headers !== \"object\") {\n return undefined;\n }\n\n if (typeof Headers !== \"undefined\" && headers instanceof Headers) {\n return headers.get(targetName) ?? undefined;\n }\n\n return extractFromRecord(\n headers as Record<string, string | string[] | undefined>,\n targetName.toLowerCase(),\n );\n}\n","import { ReplayAttackError } from \"../errors.js\";\nimport type { HeaderRecord, RawBody, WebhookReplayStore } from \"../types.js\";\nimport { extractHeaderValue } from \"../utils/headers.js\";\nimport type {\n InMemoryReplayStoreOptions,\n VerifyReplayStoreOptions,\n} from \"./types.js\";\n\nexport const DEFAULT_REPLAY_STORE_MAX_SIZE = 10_000;\n\nexport const DEFAULT_REPLAY_STORE_TTL_SECONDS = 300;\n\nexport const GENERIC_EVENT_ID_HEADERS = [\n \"x-webhook-id\",\n \"webhook-id\",\n \"svix-id\",\n \"x-github-delivery\",\n \"x-shopify-webhook-id\",\n \"x-event-id\",\n \"x-idempotency-key\",\n] as const;\n\n/**\n * Tracks in-flight replay check promises per store instance to prevent TOCTOU race conditions\n * during concurrent duplicate deliveries across stores that only implement `has` and `set`.\n */\nconst inFlightPerStore = new WeakMap<\n WebhookReplayStore,\n Map<string, Promise<ReplayAttackError | null>>\n>();\n\n/**\n * Sanitizes and bounds the maximum capacity for `InMemoryReplayStore`.\n *\n * @param size - Candidate capacity option.\n * @returns Positive bounded integer capacity.\n */\nfunction sanitizeCapacity(size: number | undefined): number {\n return typeof size === \"number\" && Number.isFinite(size) && size > 0\n ? Math.floor(size)\n : DEFAULT_REPLAY_STORE_MAX_SIZE;\n}\n\n/**\n * Sanitizes and bounds the default TTL for `InMemoryReplayStore`.\n *\n * @param ttl - Candidate default TTL option.\n * @returns Positive finite TTL in seconds.\n */\nfunction sanitizeTtl(ttl: number | undefined): number {\n return typeof ttl === \"number\" && Number.isFinite(ttl) && ttl > 0\n ? ttl\n : DEFAULT_REPLAY_STORE_TTL_SECONDS;\n}\n\n/**\n * Resolves the effective TTL for a store insert from explicit TTL or default fallback.\n *\n * @param ttl - Explicit TTL in seconds.\n * @param defaultTtl - Default TTL fallback.\n * @returns Resolved finite TTL in seconds.\n */\nfunction resolveInsertTtl(ttl: number | undefined, defaultTtl: number): number {\n return typeof ttl === \"number\" && Number.isFinite(ttl) ? ttl : defaultTtl;\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 private lastPruneMs = 0;\n\n constructor(options: InMemoryReplayStoreOptions = {}) {\n this.maxSize = sanitizeCapacity(options.maxSize);\n this.defaultTtlSeconds = sanitizeTtl(options.defaultTtlSeconds);\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 if (typeof eventId !== \"string\" || eventId.length === 0) {\n return false;\n }\n\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 if (typeof eventId !== \"string\" || eventId.length === 0) {\n return;\n }\n\n const effectiveTtl = resolveInsertTtl(ttlSeconds, this.defaultTtlSeconds);\n\n if (effectiveTtl <= 0) {\n this.entries.delete(eventId);\n\n return;\n }\n\n this.prepareInsertSlot(eventId);\n this.entries.set(eventId, this.now() + effectiveTtl * 1000);\n }\n\n /**\n * Atomically records `eventId` with TTL if it does not already exist in the store (set-if-not-exists).\n *\n * @param eventId - Unique webhook event or delivery identifier.\n * @param ttlSeconds - Time-to-live in seconds before expiration.\n * @returns `true` if newly recorded, `false` if already present in the store.\n */\n setNx(eventId: string, ttlSeconds: number = this.defaultTtlSeconds): boolean {\n if (this.has(eventId)) {\n return false;\n }\n\n this.set(eventId, ttlSeconds);\n\n return true;\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 if (typeof eventId !== \"string\" || eventId.length === 0) {\n return false;\n }\n\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 /**\n * Prepares a map slot for insertion by evicting an existing key or pruning capacity.\n *\n * @param eventId - Candidate event ID key.\n */\n private prepareInsertSlot(eventId: string): void {\n if (this.entries.has(eventId)) {\n this.entries.delete(eventId);\n\n return;\n }\n\n if (this.entries.size >= this.maxSize) {\n this.evictExpiredOrOldest();\n }\n }\n\n /**\n * Fast-path check from the head of the insertion-ordered Map to purge expired entries.\n *\n * @param currentMs - Current time in milliseconds.\n */\n private evictHeadExpired(currentMs: number): void {\n for (const [key, expiresAt] of this.entries) {\n if (currentMs >= expiresAt) {\n this.entries.delete(key);\n\n if (this.entries.size < this.maxSize) {\n return;\n }\n } else {\n // WHY: Map entries are insertion-ordered. If the head entry is not expired,\n // entries with identical or later expiration dates will not be expired either.\n break;\n }\n }\n }\n\n private evictExpiredOrOldest(): void {\n const currentMs = this.now();\n\n this.evictHeadExpired(currentMs);\n\n // WHY: Throttle full O(N) map scans to at most once per second under sustained load\n // so bursts of new events at maximum capacity do not stall the JavaScript event loop.\n const shouldPrune =\n this.entries.size >= this.maxSize &&\n (currentMs - this.lastPruneMs >= 1000 || currentMs < this.lastPruneMs);\n\n if (shouldPrune) {\n this.lastPruneMs = currentMs;\n this.prune();\n }\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 * 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 = extractHeaderValue(headers, name);\n const trimmed = raw?.trim();\n\n if (trimmed) {\n return trimmed;\n }\n }\n\n return undefined;\n}\n\n/**\n * Determines whether a candidate value is a non-null, non-array object record.\n *\n * @param value - Candidate value.\n * @returns `true` if value is a plain object record.\n */\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Parses a raw JSON string into a record object, ignoring non-object outputs.\n *\n * @param rawBody - Raw body text.\n * @returns Parsed object record or null.\n */\nfunction parseJsonRecord(rawBody?: string): Record<string, unknown> | null {\n if (typeof rawBody !== \"string\" || rawBody.length === 0) {\n return null;\n }\n\n try {\n const parsed: unknown = JSON.parse(rawBody);\n\n return isPlainRecord(parsed) ? parsed : null;\n } catch {\n return null;\n }\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 (isPlainRecord(payload)) {\n return payload;\n }\n\n return parseJsonRecord(rawBody);\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 * 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 * Sanitizes an explicitly provided `eventId` option into a non-empty string.\n *\n * @param eventId - Candidate event ID.\n * @returns Trimmed event ID or undefined.\n */\nfunction resolveExplicitEventId(eventId?: unknown): string | undefined {\n if (typeof eventId === \"string\") {\n const trimmed = eventId.trim();\n\n return trimmed.length > 0 ? trimmed : undefined;\n }\n\n if (typeof eventId === \"number\" && Number.isFinite(eventId)) {\n return String(eventId);\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 = resolveExplicitEventId(options.eventId);\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 (\n typeof storeTtlSeconds === \"number\" &&\n Number.isFinite(storeTtlSeconds) &&\n storeTtlSeconds > 0\n ) {\n return storeTtlSeconds;\n }\n\n if (\n typeof toleranceSeconds === \"number\" &&\n Number.isFinite(toleranceSeconds) &&\n toleranceSeconds > 0\n ) {\n return toleranceSeconds;\n }\n\n return DEFAULT_REPLAY_STORE_TTL_SECONDS;\n}\n\n/**\n * Checks and records an event ID using a store's basic `has` and `set` primitives.\n *\n * @param store - Webhook replay store.\n * @param eventId - Resolved event identifier.\n * @param ttlSeconds - Time-to-live in seconds.\n * @returns `ReplayAttackError` if already seen, or `null` on success.\n */\nasync function executeStoreCheckAndRecord(\n store: WebhookReplayStore,\n eventId: string,\n ttlSeconds: number,\n): Promise<ReplayAttackError | null> {\n const alreadySeen = await store.has(eventId);\n\n if (alreadySeen) {\n return new ReplayAttackError(eventId);\n }\n\n await store.set(eventId, ttlSeconds);\n\n return null;\n}\n\n/**\n * Synchronizes concurrent in-flight checks for the same `eventId` across stores lacking atomic operations.\n *\n * @param store - Webhook replay store.\n * @param eventId - Resolved event identifier.\n * @param ttlSeconds - Time-to-live in seconds.\n * @returns `ReplayAttackError` if duplicate or already tracked, or `null` on success.\n */\nasync function handleInFlightReplayCheck(\n store: WebhookReplayStore,\n eventId: string,\n ttlSeconds: number,\n): Promise<ReplayAttackError | null> {\n let storeInFlight = inFlightPerStore.get(store);\n\n if (!storeInFlight) {\n storeInFlight = new Map();\n inFlightPerStore.set(store, storeInFlight);\n }\n\n const existing = storeInFlight.get(eventId);\n\n if (existing) {\n // WHY: A concurrent verification is already in flight for this exact event ID.\n // If that prior check succeeds, this concurrent invocation is a duplicate replay.\n const result = await existing;\n\n return result === null ? new ReplayAttackError(eventId) : result;\n }\n\n const checkPromise = executeStoreCheckAndRecord(store, eventId, ttlSeconds);\n\n storeInFlight.set(eventId, checkPromise);\n\n try {\n return await checkPromise;\n } finally {\n storeInFlight.delete(eventId);\n }\n}\n\n/**\n * Verifies that `eventId` has not already been seen in `store`, and records it with a TTL if unseen.\n * Automatically leverages atomic `setNx` when supported, or serializes in-flight checks to prevent\n * TOCTOU race conditions under high concurrency.\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 } = options;\n\n if (!store) {\n return null;\n }\n\n const resolvedId = resolveExplicitEventId(options.eventId);\n\n if (!resolvedId) {\n return null;\n }\n\n const ttlSeconds = resolveEffectiveTtl(\n options.storeTtlSeconds,\n options.toleranceSeconds,\n );\n\n // WHY: Prefer atomic set-if-not-exists when provided by the store (e.g. InMemoryReplayStore or Redis SET NX)\n // to eliminate race conditions between checking existence and recording the key.\n if (typeof store.setNx === \"function\") {\n const recorded = await store.setNx(resolvedId, ttlSeconds);\n\n return recorded ? null : new ReplayAttackError(resolvedId);\n }\n\n return handleInFlightReplayCheck(store, resolvedId, ttlSeconds);\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 * Sanitizes and bounds the maximum capacity for `CryptoKeyLruCache`.\n *\n * @param size - Candidate capacity option.\n * @returns Positive bounded integer capacity.\n */\nfunction sanitizeCacheCapacity(size: number | undefined): number {\n return typeof size === \"number\" && Number.isFinite(size) && size > 0\n ? Math.floor(size)\n : 256;\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 = sanitizeCacheCapacity(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 if (cache.get(cacheKey) === keyPromise) {\n cache.delete(cacheKey);\n }\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\n if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {\n throw new TypeError(\n \"Invalid hexadecimal string: contains non-hexadecimal characters\",\n );\n }\n\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\n for (let i = 0; i < normalizedHex.length; i += 2) {\n bytes[i / 2] = parseInt(normalizedHex.slice(i, i + 2), 16);\n }\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 { InvalidSignatureHeaderError } from \"../../errors.js\";\n\n/**\n * Asserts that a header value is a non-empty string.\n *\n * @param header - Candidate header value.\n * @param errorMessage - Error message to throw if header is invalid or missing.\n */\nexport function assertNonEmptyHeader(\n header: unknown,\n errorMessage: string,\n): asserts header is string {\n if (typeof header !== \"string\" || !header.trim()) {\n throw new InvalidSignatureHeaderError(errorMessage);\n }\n}\n","import {\n InvalidSignatureHeaderError,\n SignatureVerificationError,\n WebhookKitError,\n} from \"../../errors.js\";\nimport { extractHeaderValue } from \"../../utils/headers.js\";\nimport { assertNonEmptyHeader } from \"../common/assertions.js\";\nimport type { GithubVerifyOptions } from \"./types.js\";\n\nexport const GITHUB_SIGNATURE_PREFIX = \"sha256=\";\n\nexport const GITHUB_SIGNATURE_HEX_REGEX = /^[0-9a-fA-F]{64}$/;\n\nexport const GITHUB_EVENT_ID_HEADERS = [\n \"x-github-delivery\",\n \"x-webhook-id\",\n \"webhook-id\",\n] as const;\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, \"Missing or empty X-Hub-Signature-256 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 strict 64-character hex hash following sha256=\n if (!signature || !GITHUB_SIGNATURE_HEX_REGEX.test(signature)) {\n throw new InvalidSignatureHeaderError(\n \"Invalid X-Hub-Signature-256 header: missing or malformed 64-character hex signature after prefix\",\n );\n }\n\n return signature;\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 */\nexport function resolveGithubSignatureHeader(\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 */\nexport function validateGithubPreconditions(\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 */\nexport function safeParseGithubHeader(\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","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 type { WebhookKitError } from \"../../errors.js\";\nimport type { WebhookVerificationResult } from \"../../types.js\";\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 * @param eventId - Optional webhook event ID.\n * @returns Failed verification result.\n */\nexport function 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 * Attempts to parse the raw body as JSON.\n *\n * @param rawBody - Raw body payload string.\n * @returns Parsed payload or undefined.\n */\nexport function 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","import { computeSubtleHmac, timingSafeEqual } from \"../../crypto/subtle.js\";\nimport { SignatureVerificationError } from \"../../errors.js\";\nimport { checkAndRecordReplay, resolveWebhookEventId } from \"../../store.js\";\nimport type {\n WebhookProviderPlugin,\n WebhookVerificationResult,\n} from \"../../types.js\";\nimport { extractRawBody } from \"../../utils/extract-body.js\";\nimport { failVerification, tryParseJson } from \"../common/helpers.js\";\nimport {\n GITHUB_EVENT_ID_HEADERS,\n resolveGithubSignatureHeader,\n safeParseGithubHeader,\n validateGithubPreconditions,\n} from \"./signature.js\";\nimport type { GithubVerifyOptions, GithubWebhookEvent } from \"./types.js\";\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 = resolveGithubSignatureHeader(options);\n\n const preconditionError = validateGithubPreconditions(options, rawHeader);\n\n if (preconditionError) {\n return failVerification(rawBody, preconditionError, options.throwOnError);\n }\n\n const parsed = safeParseGithubHeader(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 undefined,\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 {\n InvalidSignatureHeaderError,\n SignatureVerificationError,\n WebhookKitError,\n} from \"../../errors.js\";\nimport { extractHeaderValue } from \"../../utils/headers.js\";\nimport { assertNonEmptyHeader } from \"../common/assertions.js\";\nimport type { ShopifyVerifyOptions } from \"./types.js\";\n\nexport const BASE64_REGEX =\n /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\n\nexport const SHOPIFY_EVENT_ID_HEADERS = [\n \"x-shopify-webhook-id\",\n \"x-webhook-id\",\n \"webhook-id\",\n] as const;\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, \"Missing or empty X-Shopify-Hmac-Sha256 header\");\n\n const trimmed = header.trim();\n\n // WHY: Validate strict 44-character Base64 structure for 32-byte HMAC-SHA256 digests\n // before cryptographic comparison so malformed or truncated headers fail fast with InvalidSignatureHeaderError.\n if (trimmed.length !== 44 || !BASE64_REGEX.test(trimmed)) {\n throw new InvalidSignatureHeaderError(\n \"Invalid X-Shopify-Hmac-Sha256 header format: expected 44-character Base64-encoded SHA-256 digest\",\n );\n }\n\n return trimmed;\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 */\nexport function resolveShopifySignatureHeader(\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 */\nexport function validateShopifyPreconditions(\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 */\nexport function safeParseShopifyHeader(\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","import { computeSubtleHmac, timingSafeEqual } from \"../../crypto/subtle.js\";\nimport { SignatureVerificationError } from \"../../errors.js\";\nimport { checkAndRecordReplay, resolveWebhookEventId } from \"../../store.js\";\nimport type {\n WebhookProviderPlugin,\n WebhookVerificationResult,\n} from \"../../types.js\";\nimport { extractRawBody } from \"../../utils/extract-body.js\";\nimport { failVerification, tryParseJson } from \"../common/helpers.js\";\nimport {\n resolveShopifySignatureHeader,\n safeParseShopifyHeader,\n SHOPIFY_EVENT_ID_HEADERS,\n validateShopifyPreconditions,\n} from \"./signature.js\";\nimport type { ShopifyVerifyOptions, ShopifyWebhookEvent } from \"./types.js\";\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 = resolveShopifySignatureHeader(options);\n\n const preconditionError = validateShopifyPreconditions(options, rawHeader);\n\n if (preconditionError) {\n return failVerification(rawBody, preconditionError, options.throwOnError);\n }\n\n const parsed = safeParseShopifyHeader(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 undefined,\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 {\n InvalidSignatureHeaderError,\n SignatureVerificationError,\n TimestampToleranceError,\n WebhookKitError,\n} from \"../../errors.js\";\nimport { timingSafeEqual } from \"../../crypto/subtle.js\";\nimport { extractHeaderValue } from \"../../utils/headers.js\";\nimport { assertNonEmptyHeader } from \"../common/assertions.js\";\nimport type {\n StripeParsedSignatureHeader,\n StripeVerifyOptions,\n} from \"./types.js\";\n\nexport const STRIPE_EVENT_ID_HEADERS = [\n \"x-stripe-webhook-id\",\n \"x-webhook-id\",\n \"webhook-id\",\n] as const;\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\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 * 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, \"Missing or empty Stripe-Signature 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 * Asserts that a timestamp number is a positive finite number.\n *\n * @param val - Candidate timestamp value.\n * @param name - Descriptive parameter name for error messages.\n * @param eventTimestamp - Event timestamp for error attribution.\n * @param tolerance - Tolerance window for error attribution.\n */\nfunction assertPositiveFiniteTimestamp(\n val: number | undefined,\n name: string,\n eventTimestamp: number,\n tolerance: number,\n): void {\n if (val !== undefined && (!Number.isFinite(val) || val <= 0)) {\n throw new TimestampToleranceError(\n eventTimestamp,\n tolerance,\n `Invalid ${name}: expected a positive finite number`,\n );\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: Guard against NaN or non-finite tolerance options to prevent silent validation bypass.\n if (!Number.isFinite(toleranceSeconds)) {\n throw new TimestampToleranceError(\n timestamp,\n 0,\n \"Invalid timestamp tolerance value: expected a finite number\",\n );\n }\n\n // WHY: Setting tolerance <= 0 explicitly disables replay protection.\n if (toleranceSeconds <= 0) {\n return;\n }\n\n assertPositiveFiniteTimestamp(\n timestamp,\n \"webhook timestamp\",\n timestamp,\n toleranceSeconds,\n );\n assertPositiveFiniteTimestamp(\n currentTimestamp,\n \"current timestamp\",\n timestamp,\n toleranceSeconds,\n );\n\n const now = currentTimestamp ?? Math.floor(Date.now() / 1000);\n\n if (Math.abs(now - timestamp) > toleranceSeconds) {\n throw new TimestampToleranceError(timestamp, toleranceSeconds);\n }\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 */\nexport function resolveStripeSignatureHeader(\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 * 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 */\nexport function validateStripePreconditions(\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 */\nexport function safeParseStripeHeader(\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 */\nexport function checkStripeTolerance(\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 * 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 */\nexport function matchesAnyStripeSignature(\n expectedSignature: string,\n candidates: string[],\n): boolean {\n let matched = false;\n const expectedLower = expectedSignature.trim().toLowerCase();\n\n for (const candidate of candidates) {\n // WHY: Lowercase and trim candidate signatures before constant-time comparison\n // to prevent hex casing variations from producing false rejections while defending against timing leaks.\n if (timingSafeEqual(expectedLower, candidate.trim().toLowerCase())) {\n matched = true;\n }\n }\n\n return matched;\n}\n","import { computeSubtleHmac } from \"../../crypto/subtle.js\";\nimport { SignatureVerificationError } from \"../../errors.js\";\nimport { checkAndRecordReplay, resolveWebhookEventId } from \"../../store.js\";\nimport type {\n WebhookProviderPlugin,\n WebhookVerificationResult,\n} from \"../../types.js\";\nimport { extractRawBody } from \"../../utils/extract-body.js\";\nimport { failVerification, tryParseJson } from \"../common/helpers.js\";\nimport {\n checkStripeTolerance,\n matchesAnyStripeSignature,\n resolveStripeSignatureHeader,\n safeParseStripeHeader,\n STRIPE_EVENT_ID_HEADERS,\n validateStripePreconditions,\n} from \"./signature.js\";\nimport type { StripeVerifyOptions, StripeWebhookEvent } from \"./types.js\";\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 = resolveStripeSignatureHeader(options);\n\n const preconditionError = validateStripePreconditions(options, rawHeader);\n\n if (preconditionError) {\n return failVerification(rawBody, preconditionError, options.throwOnError);\n }\n\n const parsedHeader = safeParseStripeHeader(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 = checkStripeTolerance(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 = matchesAnyStripeSignature(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 ? Math.floor(rawTimestamp)\n : parseInt(String(rawTimestamp).trim(), 10);\n\n if (!Number.isFinite(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/types.js\";\nimport type { ShopifyWebhookEvent } from \"../providers/shopify/types.js\";\nimport type { StripeWebhookEvent } from \"../providers/stripe/types.js\";\nimport type {\n WebhookProviderPlugin,\n WebhookReplayStore,\n WebhookVerificationSuccess,\n} from \"../types.js\";\nimport { verifyWebhookSignature } from \"../verify.js\";\nimport type {\n ReplayStoreResolver,\n SecretResolver,\n UniversalWebhookHandler,\n WebhookErrorMeta,\n WebhookHandlerOptions,\n WebhookHandlerReturn,\n WebhookRequestInput,\n} from \"./types.js\";\n\nconst JSON_CONTENT_TYPE = \"application/json; charset=utf-8\";\n\nconst DEFAULT_SUCCESS_BODY = JSON.stringify({ received: true });\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,159 @@
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. **Strict Digest & Signature Format Validation**:
69
+ - Validate signature structure and length before cryptographic checks (e.g., GitHub signatures must be exactly 64 hex characters `^[0-9a-fA-F]{64}$`; Shopify signatures must be 44 Base64 characters).
70
+ - Hex decoding (`hexToBytes`) must strictly reject non-hex characters with a `TypeError` rather than silently zero-filling or coercing.
71
+ 4. **Replay Attack Mitigation & Idempotency**:
72
+ - Always enforce a timestamp tolerance check when the webhook provider includes a timestamp header (e.g., Stripe `t=...`).
73
+ - Default tolerance: 300 seconds (5 minutes).
74
+ - Both `timestamp` and `toleranceSeconds` must be positive, finite numbers. Reject `NaN`, `Infinity`, or negative values immediately with `TimestampToleranceError`.
75
+ - For deduplication across retries, pass `store: createInMemoryReplayStore()` (includes atomic `setNx` and bounded LRU eviction) or a `WebhookReplayStore` adapter (`has`, `set`, and optional atomic `setNx`) backed by Redis, Upstash, or Cloudflare KV. `webhook-kit` automatically serializes in-flight checks to prevent TOCTOU race conditions under high concurrency.
76
+ - Replay stores and `checkAndRecordReplay` must support both string and numeric `eventId` values (e.g. Shopify numeric order IDs) without skipping idempotency checks.
77
+
78
+ ---
79
+
80
+ ## 4. Framework Integration Recipes
81
+
82
+ ### Next.js App Router (`app/api/webhook/route.ts`)
83
+
84
+ Prefer `createWebhookHandler` for zero-boilerplate route handlers, or pass the Web Standard `Request` directly to `verifyWebhookSignature`:
85
+
86
+ ```ts
87
+ import { createWebhookHandler } from "@amirabaris/webhook-kit";
88
+
89
+ export const POST = createWebhookHandler({
90
+ provider: "stripe",
91
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
92
+ onEvent: async (event) => {
93
+ // event is strongly typed StripeWebhookEvent
94
+ console.log("Verified Stripe event:", event.id);
95
+ },
96
+ });
97
+ ```
98
+
99
+ ### Hono (Edge / Node / Bun / Cloudflare)
100
+
101
+ `createWebhookHandler` unwraps Hono's `Context` (`c.req.raw`) and resolves secrets from `c.env`:
102
+
103
+ ```ts
104
+ import { Hono } from "hono";
105
+ import { createWebhookHandler } from "@amirabaris/webhook-kit";
106
+
107
+ type Bindings = {
108
+ GITHUB_WEBHOOK_SECRET: string;
109
+ };
110
+
111
+ const app = new Hono<{ Bindings: Bindings }>();
112
+
113
+ app.post(
114
+ "/webhook",
115
+ createWebhookHandler({
116
+ provider: "github",
117
+ secret: (c) => c.env.GITHUB_WEBHOOK_SECRET,
118
+ onEvent: async (event) => {
119
+ console.log("Verified GitHub action:", event.action);
120
+ },
121
+ }),
122
+ );
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 5. Commit & Contribution Rules
128
+
129
+ When creating commits, PRs, or contributing code to `webhook-kit`:
130
+
131
+ - **Conventional Commits**: Format strictly as `type(scope): brief description in imperative mood`.
132
+ - **Allowed Types**:
133
+ - `feat`: New feature or provider implementation
134
+ - `fix`: Bug fix
135
+ - `docs`: Documentation only changes
136
+ - `test`: Adding or correcting tests
137
+ - `refactor`: Code change that neither fixes a bug nor adds a feature
138
+ - `chore`: Tooling, build config, dependency updates
139
+ - **Subject Line**:
140
+ - All lowercase type and scope.
141
+ - Imperative mood (e.g., `feat(crypto): add subtle timing-safe compare`, NOT `added` or `adds`).
142
+ - No trailing period.
143
+ - Commits must be atomic (one single logical concern).
144
+
145
+ ---
146
+
147
+ ## 6. Code Review Checklist for AI Agents
148
+
149
+ Before generating or approving code:
150
+
151
+ - [ ] Is raw body retained without intermediate JSON parsing?
152
+ - [ ] Are timing-safe comparisons used for all HMAC/signature checks?
153
+ - [ ] Is the code free from `node:crypto` imports in universal source code?
154
+ - [ ] Are signatures and digests strictly validated for length and character sets before comparison?
155
+ - [ ] Are timestamps and tolerance values validated as positive, finite numbers (rejecting `NaN` and `Infinity`)?
156
+ - [ ] Does replay protection and event ID resolution support both string and numeric IDs without skipping?
157
+ - [ ] Are TypeScript types strict (`noImplicitAny`, proper generics on `verifyWebhookSignature<T>`)?
158
+ - [ ] Is test coverage included with both valid signatures and tampered payload assertions?
159
+ - [ ] Does the commit message strictly follow Conventional Commits?
package/package.json CHANGED
@@ -1,6 +1,89 @@
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"
6
- }
3
+ "version": "0.1.2",
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
+ "scripts": {
58
+ "build": "tsdown --config-loader native",
59
+ "test": "vitest run",
60
+ "test:watch": "vitest",
61
+ "test:types": "tsd --files 'tests-d/**/*.test-d.ts'",
62
+ "test:smoke": "node ./tests/package-smoke.mjs",
63
+ "lint": "oxlint . && eslint . && prettier --check . && tsc -p tsconfig.lint.json --noEmit",
64
+ "format": "eslint --fix . && prettier --write .",
65
+ "prepare": "lefthook install",
66
+ "prepack": "node -e \"import('node:fs').then(fs => { if (!fs.existsSync('dist/index.mjs')) import('node:child_process').then(cp => cp.execSync('pnpm build', { stdio: 'inherit' })) })\""
67
+ },
68
+ "devDependencies": {
69
+ "@stylistic/eslint-plugin": "^5.10.0",
70
+ "@types/node": "^26.6.6",
71
+ "eslint": "^10.14.0",
72
+ "lefthook": "^2.2.2",
73
+ "oxlint": "^1.90.0",
74
+ "prettier": "^3.9.10",
75
+ "tsd": "^0.33.0",
76
+ "tsdown": "^0.23.4",
77
+ "typescript": "^6.0.3",
78
+ "typescript-eslint": "^8.72.1",
79
+ "unrun": "^0.3.1",
80
+ "vitest": "^2.1.9"
81
+ },
82
+ "engines": {
83
+ "node": ">=22.0.0"
84
+ },
85
+ "packageManager": "pnpm@9.15.9",
86
+ "publishConfig": {
87
+ "access": "public"
88
+ }
89
+ }