{"version":3,"sources":["../../src/utils.ts","../../src/lib/internal/static-content-cache.ts","../../src/lib/internal/http-header-utils.ts","../../src/lib/internal/response-compression.ts","../../src/lib/internal/html-utils/escape.ts","../../src/lib/internal/utils.ts"],"sourcesContent":["/**\n * Public utilities exported from unirend/utils\n *\n * This module exposes public utilities for static file caching, HTML escaping,\n * and runtime checks. Some are used internally by unirend, while others are\n * intended for use in consumer scripts.\n *\n * - StaticContentCache: Caching layer for static file serving with ETag support and LRU caching\n * - escapeHTML / escapeHTMLAttr: Safe HTML escaping for server-side HTML generation\n */\n\n// =============================================================================\n// Static Content Cache\n// =============================================================================\n// A caching layer for static file serving with ETag support, LRU caching,\n// and optimized file serving for Fastify applications.\n\nexport { StaticContentCache } from './lib/internal/static-content-cache';\n\n// Re-export types for StaticContentCache\nexport type {\n  GetFileOptions,\n  CreateStreamOptions,\n  ServeFileResult,\n  FileContent,\n  FileNotFoundResult,\n  FileErrorResult,\n  FileNotModifiedResult,\n  FileFoundResult,\n  FileResult,\n} from './lib/internal/static-content-cache';\n\nexport type { FolderConfig } from './lib/types';\n\n// =============================================================================\n// HTML Utilities\n// =============================================================================\n// Utility functions for safely handling HTML content\n\nexport { escapeHTML, escapeHTMLAttr } from './lib/internal/html-utils/escape';\n\n// Runtime detection helpers\nexport {\n  MINIMUM_SUPPORTED_NODE_MAJOR,\n  getRuntimeSupportInfo,\n  isSupportedRuntime,\n  assertSupportedRuntime,\n} from './lib/internal/utils';\n\nexport type { RuntimeName, RuntimeSupportInfo } from './lib/internal/utils';\n","import type { FastifyRequest, FastifyReply } from 'fastify';\nimport type { OutgoingHttpHeaders } from 'node:http';\nimport fs from 'fs';\nimport path from 'path';\nimport crypto from 'node:crypto';\nimport { pipeline } from 'node:stream/promises';\nimport { LRUCache, type LRUCacheChangeEvent } from 'lifecycleion/lru-cache';\nimport type { StaticContentRouterOptions, FolderConfig } from '../types';\nimport { addToVaryHeader } from './http-header-utils';\nimport {\n  buildEncodedETag,\n  compressPayload,\n  isCompressibleContentType,\n  matchesIfNoneMatch,\n  normalizeResponseCompressionOptions,\n  selectResponseEncoding,\n} from './response-compression';\n\n/**\n * Minimal stat info interface with only the properties we actually use\n */\ninterface MinimalStatInfo {\n  isFile: boolean;\n  size: number;\n  mtime: Date;\n  // eslint-disable-next-line @typescript-eslint/naming-convention\n  mtimeMs: number;\n}\n\n/**\n * Negative cache entry type (for 404s and access errors)\n */\ninterface NegativeCacheEntry {\n  notFound: true;\n}\n\ntype CompressedVariantState =\n  | {\n      kind: 'compressed';\n      data: Buffer;\n    }\n  | {\n      kind: 'not-worth-it';\n    }\n  | {\n      kind: 'tombstone';\n    };\n\n/**\n * Combined type for stat cache entries\n */\ntype StatCacheEntry = MinimalStatInfo | NegativeCacheEntry | null;\n\n/**\n * Options for getFile() method\n */\nexport interface GetFileOptions {\n  /** Whether to detect immutable assets for cache control decisions */\n  shouldDetectImmutable?: boolean;\n  /** Optional ETag from client's If-None-Match header (for 304 optimization) */\n  clientETag?: string;\n  /** Accepted content encodings from the request for representation selection */\n  acceptEncoding?: string | string[];\n}\n\n/**\n * Options for creating a read stream (for range requests)\n */\nexport interface CreateStreamOptions {\n  /** Start byte position (inclusive) */\n  start?: number;\n  /** End byte position (inclusive) */\n  end?: number;\n}\n\n/**\n * Result from serveFile() indicating what action was taken\n */\nexport type ServeFileResult =\n  | { served: false; reason: 'not-found' }\n  | { served: false; reason: 'error'; error: Error }\n  | {\n      served: true;\n      statusCode:\n        | 200 // Full file served\n        | 206 // Partial content served\n        | 304 // Not modified\n        | 400 // Invalid range request\n        | 416; // Range not satisfiable\n    };\n\n/**\n * File content discriminated union - either buffered in memory or needs streaming\n */\nexport type FileContent =\n  | {\n      /** Content is buffered in memory (small files) */\n      shouldStream: false;\n      /** The file content buffer */\n      data: Buffer;\n    }\n  | {\n      /** Content needs to be streamed from disk (large files) */\n      shouldStream: true;\n      /** Factory function to create a read stream with optional range support */\n      createStream: (options?: CreateStreamOptions) => fs.ReadStream;\n    };\n\n/**\n * Internal logger object used by static content helpers.\n */\nexport type StaticContentWarnLoggerObject = {\n  warn: (obj: object, msg: string) => void;\n};\n\n/**\n * Result when file is not found (404)\n */\nexport interface FileNotFoundResult {\n  status: 'not-found';\n}\n\n/**\n * Result when an unexpected error occurs (500)\n */\nexport interface FileErrorResult {\n  status: 'error';\n  error: Error;\n}\n\n/**\n * Result when client's ETag matches (304 Not Modified)\n */\nexport interface FileNotModifiedResult {\n  status: 'not-modified';\n  /** Generated ETag for the selected response representation */\n  etag: string;\n  /** Last-Modified date as HTTP header string */\n  lastModified: string;\n  /** Selected content encoding, if a compressed representation was chosen */\n  contentEncoding?: 'br' | 'gzip';\n  /** Whether the response should include `Vary: Accept-Encoding` */\n  varyByAcceptEncoding: boolean;\n}\n\n/**\n * Result when file is found and should be served (200)\n */\nexport interface FileFoundResult {\n  status: 'ok';\n  /** File stats (size, modification time, etc.) */\n  stat: MinimalStatInfo;\n  /** Generated ETag for the selected response representation */\n  etag: string;\n  /** Base file ETag before representation-specific encoding suffixes */\n  baseETag: string;\n  /** Last-Modified date as HTTP header string */\n  lastModified: string;\n  /** MIME type based on file extension */\n  mimeType: string;\n  /** File content - either buffered or needs streaming */\n  content: FileContent;\n  /** Selected content encoding, if a compressed representation was chosen */\n  contentEncoding?: 'br' | 'gzip';\n  /** Whether the response should include `Vary: Accept-Encoding` */\n  varyByAcceptEncoding: boolean;\n  /** Whether this file appears to be fingerprinted/immutable (for aggressive caching) */\n  isImmutableAsset: boolean;\n}\n\nfunction waitForReadStreamOpen(stream: fs.ReadStream): Promise<void> {\n  if (\n    stream.pending === false ||\n    typeof (stream as fs.ReadStream & { fd?: number }).fd === 'number'\n  ) {\n    return Promise.resolve();\n  }\n\n  return new Promise((resolve, reject) => {\n    const onOpen = () => {\n      cleanup();\n      resolve();\n    };\n\n    const onError = (error: Error) => {\n      cleanup();\n      reject(error);\n    };\n\n    const cleanup = () => {\n      stream.off('open', onOpen);\n      stream.off('error', onError);\n    };\n\n    stream.once('open', onOpen);\n    stream.once('error', onError);\n  });\n}\n\n/**\n * Union type for all possible getFile() results\n */\nexport type FileResult =\n  | FileNotFoundResult\n  | FileErrorResult\n  | FileNotModifiedResult\n  | FileFoundResult;\n\n/**\n * Parse a Range header into [start, end] byte offsets (both inclusive).\n *\n * Supports:\n *   bytes=0-499      explicit range\n *   bytes=500-       from offset to end of file\n *   bytes=-500       last 500 bytes (suffix range)\n *\n * Returns [start, end] on success.\n * Returns 'malformed' (→ 400) for syntactically invalid headers (no bytes= prefix, bad spec format).\n * Returns 'unsatisfiable' (→ 416) for multipart ranges or ranges that exceed the file size.\n */\nfunction parseRange(\n  header: string,\n  fileSize: number,\n): [number, number] | 'malformed' | 'unsatisfiable' {\n  if (!header.startsWith('bytes=')) {\n    return 'malformed';\n  }\n\n  const spec = header.slice(6);\n\n  // Reject multipart ranges (satisfiable syntax, but unsupported)\n  if (spec.includes(',')) {\n    return 'unsatisfiable';\n  }\n\n  const match = /^(\\d*)-(\\d*)$/.exec(spec);\n\n  if (!match) {\n    return 'malformed';\n  }\n\n  const startStr = match[1];\n  const endStr = match[2];\n\n  if (startStr === '' && endStr === '') {\n    return 'malformed';\n  }\n\n  let start: number;\n  let end: number;\n\n  if (startStr === '') {\n    // Suffix range: bytes=-500 → last 500 bytes\n    const suffix = parseInt(endStr, 10);\n    start = Math.max(0, fileSize - suffix);\n    end = fileSize - 1;\n  } else if (endStr === '') {\n    // Open-ended range: bytes=500-\n    start = parseInt(startStr, 10);\n    end = fileSize - 1;\n  } else {\n    start = parseInt(startStr, 10);\n    end = parseInt(endStr, 10);\n  }\n\n  // Validate: start must be within file, start must not exceed end\n  if (start >= fileSize || start > end) {\n    return 'unsatisfiable';\n  }\n\n  // Clamp end to last valid byte\n  end = Math.min(end, fileSize - 1);\n\n  return [start, end];\n}\n\n/**\n * Encapsulates caching and serving of static content files.\n *\n * This class manages:\n * - Multiple LRU caches (ETag, file content, and file stats)\n * - Configuration for single asset and folder mappings\n * - Optimized file serving with HTTP caching headers\n * - Content-based ETags for small files, weak ETags for large files\n * - Automatic detection of immutable assets (fingerprinted files)\n *\n * Each instance maintains its own independent caches, allowing\n * multiple instances with different configurations.\n */\nexport class StaticContentCache {\n  // Normalized mappings (mutable to allow runtime updates)\n  private singleAssetMap: Map<string, string>; // URL path → filesystem path\n  private folderMap: Map<string, FolderConfig>; // URL prefix → folder config\n\n  // Cache configuration\n  private readonly smallFileMaxSize: number;\n  private readonly cacheControl: string;\n  private readonly immutableCacheControl: string;\n  private readonly negativeCacheTtl: number;\n  private readonly positiveCacheTtl: number;\n  private readonly compression: ReturnType<\n    typeof normalizeResponseCompressionOptions\n  >;\n\n  // LRU caches (all keyed by filesystem path)\n  private readonly etagCache: LRUCache<string, string>; // fs path → ETag\n  private readonly contentCache: LRUCache<string, Buffer>; // fs path → file content\n  // Keyed by fs path + BASE file ETag + encoding.\n  // The stored ETag component is the uncompressed file's identity; the\n  // representation-specific HTTP ETag is derived later by suffixing the\n  // encoding (e.g. \"--gzip\", \"--br\") when sending the response.\n  private readonly compressedVariantCache: LRUCache<\n    string,\n    CompressedVariantState\n  >; // fs path + base etag + encoding → compressed variant state\n  private readonly statCache: LRUCache<string, StatCacheEntry>; // fs path → file stats\n  // Reverse index of filesystem path -> compressed cache keys for that file.\n  // This lets invalidateFile() clear every cached compressed representation for\n  // a path without needing to know which base ETag variants are currently live.\n  private readonly compressedContentIndex: Map<string, Set<string>> = new Map();\n\n  // Optional logger\n  private readonly logger?: StaticContentWarnLoggerObject;\n\n  /**\n   * Creates a new StaticContentCache instance\n   *\n   * @param options Static content configuration (file mappings, cache settings, etc.)\n   * @param logger Optional logger (e.g., fastify.log) for error logging\n   */\n  constructor(\n    options: StaticContentRouterOptions,\n    logger?: StaticContentWarnLoggerObject,\n  ) {\n    const {\n      singleAssetMap = {},\n      folderMap = {},\n      smallFileMaxSize = 5 * 1024 * 1024, // 5 MB\n      cacheEntries = 100,\n      contentCacheMaxSize = 50 * 1024 * 1024, // 50 MB\n      statCacheEntries = 250,\n      negativeCacheTtl = 30 * 1000, // 30 seconds\n      positiveCacheTtl = 60 * 60 * 1000, // 1 hour\n      cacheControl = 'public, max-age=0, must-revalidate',\n      immutableCacheControl = 'public, max-age=31536000, immutable',\n      compression = true,\n    } = options;\n\n    this.smallFileMaxSize = smallFileMaxSize;\n    this.cacheControl = cacheControl;\n    this.immutableCacheControl = immutableCacheControl;\n    this.negativeCacheTtl = negativeCacheTtl;\n    this.positiveCacheTtl = positiveCacheTtl;\n    this.compression = normalizeResponseCompressionOptions(compression);\n    this.logger = logger;\n\n    // Normalize singleAssetMap\n    this.singleAssetMap = this.normalizeSingleAssetMap(singleAssetMap);\n\n    // Normalize folderMap\n    this.folderMap = this.normalizeFolderMap(folderMap);\n\n    // Initialize LRU caches\n    const defaultTtl = positiveCacheTtl > 0 ? positiveCacheTtl : undefined;\n\n    this.etagCache = new LRUCache<string, string>(cacheEntries, { defaultTtl });\n    this.contentCache = new LRUCache<string, Buffer>(cacheEntries, {\n      defaultTtl,\n      maxSize: contentCacheMaxSize,\n    });\n\n    this.compressedVariantCache = new LRUCache<string, CompressedVariantState>(\n      cacheEntries,\n      {\n        defaultTtl,\n        maxSize: contentCacheMaxSize,\n        // Keep the reverse index aligned when compressed variants disappear from\n        // the LRU on their own, not just when StaticContentCache deletes them.\n        onChange: (event) => this.handleCompressedVariantCacheChange(event),\n        onChangeReasons: ['evict', 'expired', 'delete', 'clear'],\n      },\n    );\n    this.statCache = new LRUCache<string, StatCacheEntry>(statCacheEntries, {\n      defaultTtl,\n    });\n  }\n\n  /**\n   * Gets file metadata and content with optimized caching\n   *\n   * This method handles all the core file operations and caching:\n   * - File stats caching to avoid repeated filesystem operations\n   * - ETag generation and caching (content-based for small files, weak for large files)\n   * - Small file content caching in memory for performance\n   * - Proper MIME type detection\n   * - Immutable asset detection for cache control decisions\n   * - Optional short-circuit if client ETag matches (for 304 responses)\n   *\n   * Useful for both HTTP serving (via serveFile) and programmatic access\n   *\n   * @param resolvedPath The absolute path to the file\n   * @param options Optional configuration for file retrieval\n   * @returns Result with status: 'not-found', 'error', 'not-modified', or 'ok'\n   */\n  public async getFile(\n    resolvedPath: string,\n    options?: GetFileOptions,\n  ): Promise<FileResult> {\n    // Wrap entire operation in try-catch to return errors instead of throwing\n    try {\n      const {\n        shouldDetectImmutable = false,\n        clientETag,\n        acceptEncoding,\n      } = options || {};\n\n      // Step 1: Resolve file metadata, preferably from the stat cache.\n      // Try to get file stats from cache to avoid filesystem operations\n      const cachedStat = this.statCache.get(resolvedPath);\n\n      // Variable that will hold our file information\n      let stat: MinimalStatInfo | null = null;\n\n      // Handle cached entries (LRU handles TTL expiration internally)\n      if (cachedStat) {\n        if ('notFound' in cachedStat) {\n          // File is known to not exist\n          return { status: 'not-found' };\n        } else if (cachedStat !== null) {\n          // We have a valid cached stat, use it\n          stat = cachedStat;\n        }\n      }\n\n      // If stats aren't cached, retrieve them from filesystem\n      if (!stat) {\n        try {\n          const fullStat = await fs.promises.stat(resolvedPath);\n\n          // Only serve regular files, not directories or special files\n          if (!fullStat.isFile()) {\n            // Cache as negative entry with specific TTL\n            this.statCache.set(\n              resolvedPath,\n              { notFound: true },\n              this.negativeCacheTtl,\n            );\n\n            return { status: 'not-found' };\n          }\n\n          // Extract only the properties we need to minimize memory usage\n          stat = {\n            isFile: true, // We know it's a file at this point\n            size: fullStat.size,\n            mtime: fullStat.mtime,\n            // eslint-disable-next-line @typescript-eslint/naming-convention\n            mtimeMs: fullStat.mtimeMs,\n          };\n\n          // Cache the minimal stats for future requests\n          // The TTL was already set when creating the cache\n          this.statCache.set(resolvedPath, stat);\n        } catch (error) {\n          // File doesn't exist or can't be accessed\n          // Cache as negative entry with specific TTL\n          this.statCache.set(\n            resolvedPath,\n            { notFound: true },\n            this.negativeCacheTtl,\n          );\n\n          // Log unexpected errors (like permission issues) but not 'file not found' errors\n          // ENOENT is expected for files that don't exist and shouldn't be logged\n          if (\n            error instanceof Error &&\n            'code' in error &&\n            (error as NodeJS.ErrnoException).code !== 'ENOENT' &&\n            this.logger\n          ) {\n            this.logger.warn(\n              {\n                err: error,\n                path: resolvedPath,\n              },\n              'Unexpected error accessing static file',\n            );\n          }\n\n          return { status: 'not-found' };\n        }\n      }\n\n      // Generate Last-Modified header from file modification time\n      const lastModified = stat.mtime.toUTCString();\n\n      // Step 2: Derive the base file validator used for identity responses and\n      // as the source for encoding-specific ETags.\n      // Try to get ETag from cache\n      let etag = this.etagCache.get(resolvedPath);\n\n      // Generate a new ETag if not cached\n      if (!etag) {\n        // For small files: create content-based strong ETag using SHA-256\n        if (stat.size <= this.smallFileMaxSize) {\n          // Try to get file content from cache for ETag generation\n          let buf = this.contentCache.get(resolvedPath);\n\n          // If content not cached, read and cache it\n          if (!buf) {\n            try {\n              buf = await fs.promises.readFile(resolvedPath);\n              this.contentCache.set(resolvedPath, buf);\n            } catch (error) {\n              // Log unexpected errors when reading file content\n              // Cast to NodeJS.ErrnoException to access error codes if needed\n              const fsError = error as NodeJS.ErrnoException;\n\n              if (this.logger) {\n                this.logger.warn(\n                  {\n                    err: fsError,\n                    path: resolvedPath,\n                    code: fsError.code,\n                  },\n                  'Error reading static file content',\n                );\n              }\n\n              // Re-throw to be handled by outer error handling\n              throw error;\n            }\n          }\n\n          // Generate a strong hash-based ETag from file content\n          const hash = crypto.createHash('sha256').update(buf).digest('base64');\n          etag = `\"${hash}\"`;\n        } else {\n          // For large files: create a weak ETag based on size and modification time\n          // Using W/ prefix to indicate a weak validator per RFC specs\n          etag = `W/\"${stat.size}-${Number(stat.mtimeMs)}\"`;\n        }\n\n        // Cache the ETag for future requests\n        this.etagCache.set(resolvedPath, etag);\n      }\n\n      // Determine if we should use immutable cache headers based on the filename pattern\n      // A fingerprinted file typically has a name like main.a1b2c3.js or chunk-5a7d9c8b.js\n      const isImmutableAsset =\n        shouldDetectImmutable && this.isImmutableAsset(resolvedPath);\n\n      // Get MIME type based on file extension\n      const mimeType = this.getMimeType(resolvedPath);\n\n      // Step 3: Load the file body as either a cached/buffered payload or a\n      // stream factory, depending on size.\n      // Build content discriminated union based on file size\n      // Small files: buffered in memory (get from cache or read from disk as fallback)\n      // Large files: must be streamed from disk with factory function that supports ranges\n      let fileContent: FileContent;\n\n      if (stat.size <= this.smallFileMaxSize) {\n        // Try to get content from cache first\n        let content = this.contentCache.get(resolvedPath);\n\n        // If not in cache, read from disk\n        if (!content) {\n          try {\n            content = await fs.promises.readFile(resolvedPath);\n            this.contentCache.set(resolvedPath, content);\n          } catch (error) {\n            // File disappeared or became inaccessible\n            const fsError = error as NodeJS.ErrnoException;\n\n            // If file no longer exists, treat as not-found\n            if (fsError.code === 'ENOENT') {\n              // Invalidate caches since file disappeared\n              this.statCache.set(\n                resolvedPath,\n                { notFound: true },\n                this.negativeCacheTtl,\n              );\n\n              this.etagCache.delete(resolvedPath);\n              this.contentCache.delete(resolvedPath);\n\n              return { status: 'not-found' };\n            }\n\n            // Other errors - log and re-throw\n            if (this.logger) {\n              this.logger.warn(\n                {\n                  err: fsError,\n                  path: resolvedPath,\n                  code: fsError.code,\n                },\n                'Error reading static file content',\n              );\n            }\n\n            throw error;\n          }\n        }\n\n        fileContent = { shouldStream: false, data: content };\n      } else {\n        fileContent = {\n          shouldStream: true,\n          createStream: (options) => fs.createReadStream(resolvedPath, options),\n        };\n      }\n\n      // Step 4: Choose the response representation before checking\n      // If-None-Match so gzip/br variants get their own ETags and 304 behavior.\n      const shouldVaryByAcceptEncoding =\n        this.compression.enabled &&\n        !fileContent.shouldStream &&\n        isCompressibleContentType(mimeType) &&\n        fileContent.data.length >= this.compression.threshold;\n\n      const selectedEncoding = shouldVaryByAcceptEncoding\n        ? selectResponseEncoding(acceptEncoding, this.compression.preferBrotli)\n        : null;\n      let responseEncoding: 'br' | 'gzip' | undefined;\n\n      // Step 5: For buffered responses, reuse or build a compressed variant if\n      // the negotiated encoding is smaller than the original bytes.\n      if (!fileContent.shouldStream && selectedEncoding) {\n        const compressedCacheKey = this.getCompressedCacheKey(\n          resolvedPath,\n          etag,\n          selectedEncoding,\n        );\n\n        const cachedVariant =\n          this.compressedVariantCache.get(compressedCacheKey);\n\n        let compressed =\n          cachedVariant?.kind === 'compressed' ? cachedVariant.data : undefined;\n        const isCompressedNotWorthIt = cachedVariant?.kind === 'not-worth-it';\n        const isCompressedTombstone = cachedVariant?.kind === 'tombstone';\n\n        if (!cachedVariant || isCompressedTombstone) {\n          // A plain cache miss may leave behind a stale reverse-index entry, so\n          // clean that up before recomputing. Tombstones stay tracked on\n          // purpose: they still represent a live variant key that should block\n          // immediate reinsertion after invalidateFile().\n          if (!isCompressedTombstone) {\n            this.untrackCompressedVariant(resolvedPath, compressedCacheKey);\n          }\n\n          compressed = await compressPayload(\n            fileContent.data,\n            selectedEncoding,\n            this.compression,\n          );\n        }\n\n        // Only keep an encoded variant if it is actually smaller than the\n        // original bytes. Otherwise prefer the identity response and clear any\n        // stale cached compressed entry for this representation.\n        if (compressed && compressed.length < fileContent.data.length) {\n          responseEncoding = selectedEncoding;\n\n          // Only store compressed bytes if we do not already have them cached\n          // and this exact variant key is not still inside the invalidation\n          // tombstone window from a recent invalidateFile() call.\n          if (\n            !this.compressedVariantCache.get(compressedCacheKey) &&\n            !isCompressedTombstone\n          ) {\n            // invalidateFile() leaves a short-lived tombstone for the old\n            // path + base ETag + encoding key so an older in-flight request\n            // cannot immediately repopulate a stale compressed variant.\n            this.compressedVariantCache.set(compressedCacheKey, {\n              kind: 'compressed',\n              data: compressed,\n            });\n\n            // The reverse index groups all compressed variants for a file path\n            // so invalidateFile() can clear them without knowing the current\n            // base ETag or encoding ahead of time.\n            const existingCompressedKeys =\n              this.compressedContentIndex.get(resolvedPath);\n\n            if (existingCompressedKeys) {\n              existingCompressedKeys.add(compressedCacheKey);\n            } else {\n              this.compressedContentIndex.set(\n                resolvedPath,\n                new Set([compressedCacheKey]),\n              );\n            }\n          }\n\n          fileContent = {\n            shouldStream: false,\n            data: compressed,\n          };\n        } else {\n          // Only record a fresh negative result. Reuse existing tombstones and\n          // prior \"not worth it\" decisions instead of resetting their state.\n          if (!isCompressedNotWorthIt && !isCompressedTombstone) {\n            // Record that this exact variant key negotiated successfully but\n            // did not beat the identity response, so future requests can skip\n            // recompressing until the file version changes or the entry expires.\n            this.compressedVariantCache.delete(compressedCacheKey);\n            this.compressedVariantCache.set(compressedCacheKey, {\n              kind: 'not-worth-it',\n            });\n\n            // Track negative variant decisions in the same reverse index so\n            // invalidateFile() can clear all per-variant state for the path.\n            const existingVariantKeys =\n              this.compressedContentIndex.get(resolvedPath);\n\n            if (existingVariantKeys) {\n              existingVariantKeys.add(compressedCacheKey);\n            } else {\n              this.compressedContentIndex.set(\n                resolvedPath,\n                new Set([compressedCacheKey]),\n              );\n            }\n          }\n        }\n      }\n\n      const responseETag = responseEncoding\n        ? buildEncodedETag(etag, responseEncoding)\n        : etag;\n\n      if (clientETag && matchesIfNoneMatch(clientETag, responseETag)) {\n        return {\n          status: 'not-modified',\n          etag: responseETag,\n          lastModified,\n          contentEncoding: responseEncoding,\n          varyByAcceptEncoding: shouldVaryByAcceptEncoding,\n        };\n      }\n\n      return {\n        status: 'ok',\n        stat,\n        etag: responseETag,\n        baseETag: etag,\n        lastModified,\n        mimeType,\n        content: fileContent,\n        contentEncoding: responseEncoding,\n        varyByAcceptEncoding: shouldVaryByAcceptEncoding,\n        isImmutableAsset,\n      };\n    } catch (error) {\n      // Return error status for unexpected errors\n      return {\n        status: 'error',\n        error: error instanceof Error ? error : new Error(String(error)),\n      };\n    }\n  }\n\n  /**\n   * Serves a static file via HTTP with conditional responses\n   *\n   * This is a thin HTTP wrapper around getFile() that handles:\n   * - HTTP 304 Not Modified responses when client cache is valid (If-None-Match)\n   * - HTTP 206 Partial Content responses for range requests\n   * - Proper HTTP headers (Cache-Control, ETag, Content-Type, Last-Modified, etc.)\n   * - Streaming large files vs sending cached buffers for small files\n   *\n   * The heavy lifting (file I/O, caching, ETag generation) is done by getFile()\n   *\n   * @param req The Fastify request object\n   * @param reply The Fastify reply object\n   * @param resolvedPath The absolute path to the file to be served\n   * @param options Optional configuration for file serving\n   * @returns Information about whether the file was served and what status code\n   */\n  public async serveFile(\n    req: FastifyRequest,\n    reply: FastifyReply,\n    resolvedPath: string,\n    options?: GetFileOptions,\n  ): Promise<ServeFileResult> {\n    // Raw static responses use reply.hijack() + writeHead(), which bypasses\n    // Fastify's normal onSend pipeline. Built-in CORS exposes a request-scoped\n    // helper so hijacked paths can still apply the same actual-response headers\n    // before we snapshot reply.getHeaders(). Keep this ahead of hijack so a\n    // CORS/config failure still propagates through normal Fastify error\n    // handling instead of failing after raw ownership has been taken.\n\n    // Get file with all metadata and caching\n    const result = await this.getFile(resolvedPath, {\n      ...options,\n      // Extract client cache validation header (ETag-based validation).\n      clientETag: req.headers['if-none-match'],\n      // Pass through Accept-Encoding so getFile() can choose the response\n      // representation before doing ETag/304 handling.\n      acceptEncoding: req.headers['accept-encoding'],\n    });\n\n    // Handle different result statuses\n    if (result.status === 'not-found') {\n      // File not found, return early (let hook fall through to 404)\n      return { served: false, reason: 'not-found' };\n    } else if (result.status === 'error') {\n      // Unexpected error occurred, return error info\n      return { served: false, reason: 'error', error: result.error };\n    }\n\n    // Mark only when static content is about to take ownership of the response.\n    // This keeps onResponse/access-log consumers from seeing failed pre-hijack\n    // stream opens as static asset responses.\n    const markStaticAsset = () => {\n      (req as { isStaticAsset?: boolean }).isStaticAsset = true;\n    };\n\n    if (result.status === 'not-modified') {\n      // Client's cache is still valid, send 304.\n      // Return HTTP 304 Not Modified response (no body). This saves bandwidth\n      // because the client reuses its cached representation.\n      //\n      // reply.hijack() bypasses Fastify's onSend pipeline (including the generic\n      // response-compression hook), so we write directly to the raw socket.\n      // onResponse hooks still fire because setupResponseListeners attaches to\n      // reply.raw.on('finish', ...) before any hooks run.\n\n      // Representation selection depends on Accept-Encoding, so advertise that\n      // caches must keep separate variants when compression is in play.\n      if (result.varyByAcceptEncoding) {\n        addToVaryHeader(reply, 'Accept-Encoding');\n      }\n\n      // A 304 carries metadata for the representation the client validated, so\n      // keep Content-Encoding aligned with the selected cached variant.\n      if (result.contentEncoding) {\n        reply.header('Content-Encoding', result.contentEncoding);\n      }\n\n      reply\n        .code(304)\n        .header('ETag', result.etag)\n        .header('Last-Modified', result.lastModified);\n\n      await req.applyCORSHeaders?.(reply);\n      markStaticAsset();\n      reply.hijack();\n      reply.raw.writeHead(304, reply.getHeaders() as OutgoingHttpHeaders);\n      reply.raw.end();\n\n      return { served: true, statusCode: 304 };\n    }\n\n    // File found (status === 'ok'), proceed with serving.\n    //\n    // reply.hijack() bypasses Fastify's onSend pipeline entirely, preventing\n    // the generic response-compression hook from re-processing a response whose\n    // representation (identity/gzip/br) and ETag were already finalized by\n    // getFile(). Without hijack(), the compression hook could re-compress an\n    // identity response and mutate the ETag the client already validated against.\n\n    // Determine Cache-Control header based on immutability\n    const headerCacheControl = result.isImmutableAsset\n      ? this.immutableCacheControl\n      : this.cacheControl;\n\n    // Representation selection depends on Accept-Encoding, so advertise that\n    // caches must keep separate variants when compression is in play.\n    if (result.varyByAcceptEncoding) {\n      addToVaryHeader(reply, 'Accept-Encoding');\n    }\n\n    // Only encoded representations send Content-Encoding; identity responses\n    // intentionally omit it even when compression was considered.\n    if (result.contentEncoding) {\n      reply.header('Content-Encoding', result.contentEncoding);\n    }\n\n    // Set common headers\n    reply\n      .header('Last-Modified', result.lastModified)\n      .header('ETag', result.etag)\n      .header('Cache-Control', headerCacheControl)\n      .type(result.mimeType);\n\n    // Only advertise range support for streaming files\n    // (files larger than smallFileMaxSize that are streamed from disk)\n    // Buffered files in memory don't support range requests\n    if (result.content.shouldStream) {\n      reply.header('Accept-Ranges', 'bytes');\n    }\n\n    // Check for Range header to handle partial content requests\n    const rangeHeader = req.headers.range;\n\n    // Handle range requests if present and file is being streamed\n    if (rangeHeader && result.content.shouldStream) {\n      const range = parseRange(rangeHeader, result.stat.size);\n\n      if (range === 'malformed') {\n        // Bad Request — syntactically invalid Range header\n        const body = JSON.stringify({ error: 'Invalid Range header' });\n        reply\n          .code(400)\n          .header('Cache-Control', 'no-store')\n          .type('application/json')\n          .header('Content-Length', String(Buffer.byteLength(body)));\n        await req.applyCORSHeaders?.(reply);\n        markStaticAsset();\n        reply.hijack();\n        reply.raw.writeHead(400, reply.getHeaders() as OutgoingHttpHeaders);\n        reply.raw.end(req.method === 'HEAD' ? undefined : body);\n        return { served: true, statusCode: 400 };\n      } else if (range === 'unsatisfiable') {\n        const body = JSON.stringify({ error: 'Range not satisfiable' });\n        reply\n          .code(416)\n          .header('Cache-Control', 'no-store')\n          .type('application/json')\n          .header('Content-Range', `bytes */${result.stat.size}`)\n          .header('Content-Length', String(Buffer.byteLength(body)));\n        await req.applyCORSHeaders?.(reply);\n        markStaticAsset();\n        reply.hijack();\n        reply.raw.writeHead(416, reply.getHeaders() as OutgoingHttpHeaders);\n        reply.raw.end(req.method === 'HEAD' ? undefined : body);\n        return { served: true, statusCode: 416 };\n      }\n\n      const [start, end] = range;\n      const chunkSize = end - start + 1;\n      const rangeStream = result.content.createStream({ start, end });\n\n      await waitForReadStreamOpen(rangeStream);\n\n      // Set headers for partial content response\n      reply\n        .code(206) // Partial Content\n        .header('Content-Range', `bytes ${start}-${end}/${result.stat.size}`)\n        .header('Content-Length', chunkSize.toString());\n\n      await req.applyCORSHeaders?.(reply);\n      markStaticAsset();\n      reply.hijack();\n      reply.raw.writeHead(206, reply.getHeaders() as OutgoingHttpHeaders);\n\n      // HEAD — headers are set; skip stream creation entirely (no fd opened, no disk I/O)\n      if (req.method === 'HEAD') {\n        reply.raw.end();\n        return { served: true, statusCode: 206 };\n      }\n\n      // Stream the requested range using factory function with range options\n      await pipeline(rangeStream, reply.raw);\n      return { served: true, statusCode: 206 };\n    }\n\n    // HEAD — set Content-Length from stat, then end without a body\n    if (req.method === 'HEAD') {\n      // When the response would be compressed, report the compressed size so\n      // the Content-Length matches what a GET would actually transfer.\n      // Compressed responses are always buffered (!shouldStream), so narrow first.\n      const headContentLength =\n        result.contentEncoding && !result.content.shouldStream\n          ? result.content.data.length\n          : result.stat.size;\n      reply.header('Content-Length', headContentLength.toString());\n      await req.applyCORSHeaders?.(reply);\n      markStaticAsset();\n      reply.hijack();\n      reply.raw.writeHead(\n        reply.statusCode,\n        reply.getHeaders() as OutgoingHttpHeaders,\n      );\n      reply.raw.end();\n      return { served: true, statusCode: 200 };\n    }\n\n    // Serve full file based on whether streaming is needed\n    const fullFileStream = result.content.shouldStream\n      ? result.content.createStream()\n      : null;\n\n    if (fullFileStream) {\n      await waitForReadStreamOpen(fullFileStream);\n    }\n\n    await req.applyCORSHeaders?.(reply);\n\n    if (!result.content.shouldStream) {\n      // reply.raw.end(buffer) does not get Fastify's normal Content-Length\n      // inference, so buffered 200 responses must set it explicitly here.\n      reply.header('Content-Length', result.content.data.length.toString());\n    }\n\n    markStaticAsset();\n    reply.hijack();\n    reply.raw.writeHead(200, reply.getHeaders() as OutgoingHttpHeaders);\n\n    if (result.content.shouldStream) {\n      // Large file — stream from disk directly to the socket.\n      // pipeline() propagates backpressure and destroys the stream on error.\n      await pipeline(fullFileStream as fs.ReadStream, reply.raw);\n    } else {\n      // Small file — send the buffered bytes in a single write.\n      reply.raw.end(result.content.data);\n    }\n\n    return { served: true, statusCode: 200 };\n  }\n\n  /**\n   * Replaces routing maps and clears all file caches in one shot.\n   *\n   * Use this after a full build has completed. Unlike `updateConfig`, this method\n   * makes no attempt at smart per-path invalidation — it simply replaces\n   * whichever maps you provide and wipes the content, stat, and ETag caches\n   * unconditionally, guaranteeing fresh reads for the next request.\n   *\n   * You may pass `singleAssetMap`, `folderMap`, or both. Omitted sections retain\n   * their current routing configuration. Pass an empty object (`{}`) for a\n   * section to clear all mappings in that section. All file caches are always\n   * cleared, regardless of which sections are provided — even when only\n   * `singleAssetMap` is passed, the rebuilt HTML pages likely reference JS/CSS\n   * bundles served from `folderMap` directories that were also regenerated in\n   * the same build step, so preserving folder caches would risk serving stale\n   * assets alongside fresh pages.\n   *\n   * For targeted cache invalidation (when URL-to-path mappings changed but\n   * file contents at those paths are unchanged), use `updateConfig` instead.\n   * Note: `updateConfig` does not detect in-place file content changes — it\n   * only tracks which filesystem paths entered or left the map.\n   *\n   * @param newConfig Sections to replace (at least one should be provided)\n   *\n   * @example\n   * ```typescript\n   * // After an SSG build completes (page map only):\n   * cache.replaceConfig({ singleAssetMap: await loadPageMap() });\n   *\n   * // After a build that changes both pages and asset folders:\n   * cache.replaceConfig({\n   *   singleAssetMap: await loadPageMap(),\n   *   folderMap: { '/assets/': { path: './dist/assets', detectImmutableAssets: true } },\n   * });\n   * ```\n   */\n  public replaceConfig(newConfig: {\n    singleAssetMap?: Record<string, string>;\n    folderMap?: Record<string, string | FolderConfig>;\n  }): void {\n    if (newConfig.singleAssetMap !== undefined) {\n      this.singleAssetMap = this.normalizeSingleAssetMap(\n        newConfig.singleAssetMap,\n      );\n    }\n\n    if (newConfig.folderMap !== undefined) {\n      this.folderMap = this.normalizeFolderMap(newConfig.folderMap);\n    }\n\n    // Always clear all caches — no smart invalidation.\n    // A build can change file contents in-place without renaming files,\n    // so preserving any cached content or stat data would risk stale reads.\n    this.clearCaches();\n  }\n\n  /**\n   * Evicts a single file's cached content, stat, and ETag without touching\n   * any URL-to-path mappings.\n   *\n   * Use this when you know a specific file changed on disk and want to force\n   * a fresh read on the next request — without flushing the entire cache.\n   * Works for files served via `singleAssetMap` or `folderMap`.\n   *\n   * The parameter is the **filesystem path** (as it appears in the cache key),\n   * not a URL.\n   *\n   * For `singleAssetMap` entries these are the absolute paths you\n   * provided.\n   *\n   * For folder-served files the cache key is the absolute path\n   * resolved at request time.\n   *\n   * @param fsPath Absolute filesystem path of the file to evict\n   *\n   * @example\n   * ```typescript\n   * // A file watcher detected /dist/about.html was rewritten:\n   * cache.invalidateFile('/dist/about.html');\n   * ```\n   */\n  public invalidateFile(fsPath: string): void {\n    this.etagCache.delete(fsPath);\n    this.contentCache.delete(fsPath);\n    this.statCache.delete(fsPath);\n    this.invalidateCompressedVariants(fsPath);\n  }\n\n  /**\n   * Clears all caches (useful for testing or cache invalidation)\n   */\n  public clearCaches(): void {\n    this.etagCache.clear();\n    this.contentCache.clear();\n    this.compressedVariantCache.clear();\n    this.statCache.clear();\n    this.compressedContentIndex.clear();\n  }\n\n  /**\n   * Gets statistics about cache usage\n   */\n  public getCacheStats() {\n    return {\n      etag: {\n        items: this.etagCache.size,\n        byteSize: this.etagCache.byteSize,\n      },\n      content: {\n        items: this.contentCache.size,\n        byteSize: this.contentCache.byteSize,\n      },\n      compressedVariants: {\n        items: this.compressedVariantCache.size,\n        byteSize: this.compressedVariantCache.byteSize,\n      },\n      stat: {\n        items: this.statCache.size,\n        byteSize: this.statCache.byteSize,\n      },\n    };\n  }\n\n  /**\n   * Updates the static content configuration at runtime with targeted cache\n   * invalidation — only evicting entries whose URL-to-path mapping changed.\n   *\n   * Use this when URL routing is changing but file contents at existing paths\n   * are unchanged (e.g., adding or removing pages without rebuilding assets).\n   * For post-build reloads where file contents may have changed, use\n   * `replaceConfig` instead.\n   *\n   * **Important:** When providing a section, you must provide the COMPLETE mapping for that section.\n   * - If you provide `singleAssetMap`, it replaces the entire single asset map\n   * - If you provide `folderMap`, it replaces the entire folder map\n   * - You can update one section, the other, or both\n   * - Omitted sections remain unchanged\n   *\n   * **Cache invalidation strategy:**\n   * - `singleAssetMap` changes: Only invalidates filesystem paths whose URL-to-path\n   *   *mapping* changed (added, removed, or pointed to a different file). Paths whose\n   *   mapping is unchanged are not evicted — this method has no visibility into whether\n   *   the file content on disk changed. If files were rebuilt in-place, use\n   *   `replaceConfig` instead.\n   * - `folderMap` changes: Clears all caches (folder changes are structural)\n   *\n   * @param newConfig Complete mapping(s) for the section(s) you want to update\n   *\n   * @example Update only single asset mappings\n   * ```typescript\n   * cache.updateConfig({\n   *   singleAssetMap: {\n   *     '/': './dist/index.html',\n   *     '/blog/new-post': './dist/blog/new-post.html'\n   *   }\n   * });\n   * ```\n   *\n   * @example Update only folder mappings\n   * ```typescript\n   * cache.updateConfig({\n   *   folderMap: {\n   *     '/assets': { path: './dist/assets', detectImmutableAssets: true }\n   *   }\n   * });\n   * ```\n   *\n   * @example Update both sections\n   * ```typescript\n   * cache.updateConfig({\n   *   singleAssetMap: { '/': './dist/index.html' },\n   *   folderMap: { '/assets': './dist/assets' }\n   * });\n   * ```\n   */\n  public updateConfig(newConfig: {\n    singleAssetMap?: Record<string, string>;\n    folderMap?: Record<string, string | FolderConfig>;\n  }): void {\n    // Handle singleAssetMap - smart invalidation of specific filesystem paths\n    if (newConfig.singleAssetMap !== undefined) {\n      const newMap = this.normalizeSingleAssetMap(newConfig.singleAssetMap);\n\n      // Track filesystem paths that need cache invalidation\n      const pathsToInvalidate = new Set<string>();\n\n      // Loop 1: Iterate over OLD map to find filesystem paths that are no longer in use\n      // Example: '/page' used to point to '/dist/old.html', now points to '/dist/new.html'\n      // Result: Invalidate '/dist/old.html' (the old file's cache is stale)\n      for (const [url, oldFsPath] of this.singleAssetMap.entries()) {\n        const newFsPath = newMap.get(url);\n\n        // If URL was removed OR now points to a different file, invalidate OLD filesystem path\n        if (newFsPath === undefined || newFsPath !== oldFsPath) {\n          pathsToInvalidate.add(oldFsPath);\n        }\n      }\n\n      // Loop 2: Iterate over NEW map to find filesystem paths that changed\n      // Example: '/page' used to point to '/dist/old.html', now points to '/dist/new.html'\n      // Result: Invalidate '/dist/new.html' (ensure fresh read from disk)\n      // IMPORTANT: This prevents serving stale cached data if the new file was already cached\n      // from a previous mapping (e.g., same file was previously mapped to a different URL)\n      for (const [url, newFsPath] of newMap.entries()) {\n        const oldFsPath = this.singleAssetMap.get(url);\n\n        // Only invalidate NEW filesystem path if URL existed before AND now points to different file\n        if (oldFsPath !== undefined && oldFsPath !== newFsPath) {\n          pathsToInvalidate.add(newFsPath);\n        }\n      }\n\n      // Replace the map\n      this.singleAssetMap = newMap;\n\n      // Invalidate caches for affected filesystem paths only\n      for (const fsPath of pathsToInvalidate) {\n        this.etagCache.delete(fsPath);\n        this.contentCache.delete(fsPath);\n        this.statCache.delete(fsPath);\n        this.invalidateCompressedVariants(fsPath);\n      }\n    }\n\n    // Handle folderMap - clear all caches only if it changed\n    if (newConfig.folderMap !== undefined) {\n      const newFolderMap = this.normalizeFolderMap(newConfig.folderMap);\n\n      // Check if folderMap actually changed\n      // Note: Can't just compare size - could have same number of folders but different prefixes/configs\n      let hasFolderMapChanged = false;\n\n      // Quick check: if sizes differ, it definitely changed\n      if (this.folderMap.size !== newFolderMap.size) {\n        hasFolderMapChanged = true;\n      } else {\n        // Sizes match - need to check if any prefix or config changed\n        for (const [prefix, config] of newFolderMap.entries()) {\n          const oldConfig = this.folderMap.get(prefix);\n\n          if (!oldConfig || !this.isSameFolderConfig(oldConfig, config)) {\n            hasFolderMapChanged = true;\n            break;\n          }\n        }\n      }\n\n      this.folderMap = newFolderMap;\n\n      // Only clear all caches if folderMap actually changed\n      // Folder mapping changes are rare and structural - clearing everything is safe\n      if (hasFolderMapChanged) {\n        this.clearCaches();\n      }\n    }\n  }\n\n  /**\n   * Handles an HTTP request by resolving the URL to a file path and serving it\n   *\n   * This is a convenience method that combines URL resolution with file serving.\n   * If no file matches the URL, it returns without sending a response (lets the hook fall through).\n   *\n   * @param rawURL The raw request URL (may include query string or hash)\n   * @param req The Fastify request object\n   * @param reply The Fastify reply object\n   * @returns Information about whether a file was served\n   */\n  public async handleRequest(\n    rawURL: string,\n    req: FastifyRequest,\n    reply: FastifyReply,\n  ): Promise<ServeFileResult> {\n    // Strip off query string, hash, etc., and ensure a single leading slash for matching\n    const cleanedURL = rawURL.split('?')[0].split('#')[0];\n    const url = cleanedURL.startsWith('/') ? cleanedURL : '/' + cleanedURL;\n\n    // Security: Reject URLs containing null bytes to prevent path truncation attacks\n    if (url.includes('\\0')) {\n      return { served: false, reason: 'not-found' };\n    }\n\n    let resolved = '';\n    let shouldDetectImmutable = false;\n\n    // 1. Try singleAssetMap first (exact URL → file)\n    if (this.singleAssetMap.has(url)) {\n      resolved = this.singleAssetMap.get(url) as string;\n    }\n    // 2. If not matched, try folderMap (URL prefix → directory)\n    else {\n      const folder = Array.from(this.folderMap.keys()).find((prefix) =>\n        url.startsWith(prefix),\n      );\n\n      if (folder) {\n        // Get resolved base folder and config\n        const folderConfig = this.folderMap.get(folder);\n\n        if (folderConfig) {\n          // Calculate file path relative to the matched prefix\n          const relativePath = url.slice(folder.length);\n\n          // Guard against absolute path behavior if a leading slash sneaks in\n          const safeRelativePath = relativePath.startsWith('/')\n            ? relativePath.slice(1)\n            : relativePath;\n\n          // Only allow files that don't contain '..' to prevent directory traversal\n          if (\n            !safeRelativePath.includes('../') &&\n            !safeRelativePath.includes('..\\\\')\n          ) {\n            resolved = path.join(folderConfig.path, safeRelativePath);\n            shouldDetectImmutable = folderConfig.detectImmutableAssets ?? false;\n          }\n        }\n      }\n    }\n\n    // If we found a file to serve, serve it\n    // otherwise: return not-found (let hook fall through)\n    if (resolved) {\n      return this.serveFile(req, reply, resolved, { shouldDetectImmutable });\n    }\n\n    return { served: false, reason: 'not-found' };\n  }\n\n  /**\n   * Normalizes single asset map keys to ensure leading slash\n   * Also validates against null bytes to prevent path injection\n   */\n  private normalizeSingleAssetMap(\n    singleAssetMap: Record<string, string>,\n  ): Map<string, string> {\n    const normalized = new Map<string, string>();\n\n    for (const [key, value] of Object.entries(singleAssetMap)) {\n      // Security: Skip entries with null bytes to prevent path truncation attacks\n      if (key.includes('\\0') || value.includes('\\0')) {\n        if (this.logger) {\n          this.logger.warn(\n            { key, value },\n            'Skipping singleAssetMap entry with null byte',\n          );\n        }\n\n        continue;\n      }\n\n      const normalizedKey = key.startsWith('/') ? key : '/' + key;\n      normalized.set(normalizedKey, value);\n    }\n\n    return normalized;\n  }\n\n  /**\n   * Normalizes folder map with proper prefix formatting\n   * Also validates against null bytes to prevent path injection\n   *\n   * Handles two config formats:\n   * 1. String shorthand: { \"/assets/\": \"/path/to/assets\" }\n   * 2. Full config object: { \"/assets/\": { path: \"/path/to/assets\", detectImmutableAssets: true } }\n   */\n  private normalizeFolderMap(\n    folderMap: Record<string, string | FolderConfig>,\n  ): Map<string, FolderConfig> {\n    const normalized = new Map<string, FolderConfig>();\n\n    for (const [prefix, config] of Object.entries(folderMap)) {\n      const normalizedPrefix = this.normalizePrefix(prefix);\n\n      // Security: Skip entries with null bytes to prevent path truncation attacks\n      const configPath = typeof config === 'string' ? config : config.path;\n      if (prefix.includes('\\0') || configPath.includes('\\0')) {\n        if (this.logger) {\n          this.logger.warn(\n            { prefix, configPath },\n            'Skipping folderMap entry with null byte',\n          );\n        }\n\n        continue;\n      }\n\n      // Handle string shorthand: just a directory path\n      if (typeof config === 'string') {\n        normalized.set(normalizedPrefix, {\n          path: config,\n          detectImmutableAssets: false,\n        });\n      } else {\n        // Handle full config object with optional detectImmutableAssets\n        normalized.set(normalizedPrefix, {\n          path: config.path,\n          detectImmutableAssets: config.detectImmutableAssets ?? false,\n        });\n      }\n    }\n\n    return normalized;\n  }\n\n  /**\n   * Normalizes URL prefix: ensures leading and trailing slash, collapses multiple slashes\n   */\n  private normalizePrefix(prefix: string): string {\n    let p = prefix || '/';\n\n    // Collapse multiple consecutive slashes into a single slash\n    p = p.replace(/\\/+/g, '/');\n\n    if (!p.startsWith('/')) {\n      p = '/' + p;\n    }\n\n    if (!p.endsWith('/')) {\n      p = p + '/';\n    }\n\n    return p;\n  }\n\n  /**\n   * Gets the MIME type for a file based on its extension\n   */\n  private getMimeType(filePath: string): string {\n    // Strip the leading dot from the extension\n    const ext = path.extname(filePath).toLowerCase().replace(/^\\./, '');\n\n    // Map common extensions to MIME types (alphabetical order)\n    const mimeTypes: Record<string, string> = {\n      css: 'text/css',\n      gif: 'image/gif',\n      html: 'text/html',\n      ico: 'image/x-icon',\n      jpeg: 'image/jpeg',\n      jpg: 'image/jpeg',\n      js: 'application/javascript',\n      json: 'application/json',\n      mp4: 'video/mp4',\n      pdf: 'application/pdf',\n      png: 'image/png',\n      svg: 'image/svg+xml',\n      txt: 'text/plain',\n      webmanifest: 'application/manifest+json',\n      xml: 'application/xml',\n    };\n\n    return mimeTypes[ext] || 'application/octet-stream';\n  }\n\n  /**\n   * Compares two FolderConfig objects for equality\n   * Dynamically checks all properties so we don't need to update this if FolderConfig changes\n   */\n  private isSameFolderConfig(a: FolderConfig, b: FolderConfig): boolean {\n    const keysA = Object.keys(a) as (keyof FolderConfig)[];\n    const keysB = Object.keys(b) as (keyof FolderConfig)[];\n\n    // Different number of keys means they're not equal\n    if (keysA.length !== keysB.length) {\n      return false;\n    }\n\n    // Check all keys from a (sufficient now since lengths match)\n    return keysA.every((key) => a[key] === b[key]);\n  }\n\n  /**\n   * Checks if a file appears to be fingerprinted/immutable based on filename\n   *\n   * Detects common build tool fingerprinting patterns:\n   * - .{hash}.{ext} format (e.g., main.a1b2c3d4.js, styles.CTpDmzGw.css)\n   * - -{hash}.{ext} format (e.g., chunk-a1b2c3d4.js, vendor-5f8e9a2b.js)\n   *\n   * Hash must be at least 6 alphanumeric characters\n   *\n   * @param filePath The file path to check\n   * @returns True if the file appears to be fingerprinted\n   */\n  private isImmutableAsset(filePath: string): boolean {\n    const fileBasename = path.basename(filePath);\n\n    // Check for fingerprint patterns:\n    // 1. .{hash}.{ext} pattern (e.g., main.CTpDmzGw.js)\n    // 2. -{hash}.{ext} pattern (e.g., chunk-CTpDmzGw.js)\n    return (\n      /\\.[A-Za-z0-9]{6,}\\./.test(fileBasename) ||\n      /-[A-Za-z0-9]{6,}\\./.test(fileBasename)\n    );\n  }\n\n  private getCompressedCacheKey(\n    resolvedPath: string,\n    etag: string,\n    encoding: string,\n  ): string {\n    return `${resolvedPath}::${etag}::${encoding}`;\n  }\n\n  private invalidateCompressedVariants(fsPath: string): void {\n    // Leave a short-lived tombstone for each invalidated compressed variant so\n    // an older in-flight request cannot immediately repopulate the same stale\n    // path + base ETag + encoding entry after invalidateFile() runs.\n    const keys = this.compressedContentIndex.get(fsPath);\n\n    if (!keys) {\n      return;\n    }\n\n    for (const key of keys) {\n      // Replace the current variant state with a short-lived tombstone so an\n      // older in-flight request cannot immediately repopulate the same stale\n      // compressed variant after invalidateFile() runs.\n      this.compressedVariantCache.set(key, { kind: 'tombstone' }, 5 * 1000);\n    }\n\n    this.compressedContentIndex.delete(fsPath);\n  }\n\n  private handleCompressedVariantCacheChange(\n    event: LRUCacheChangeEvent<string, CompressedVariantState>,\n  ): void {\n    if (\n      event.reason !== 'evict' &&\n      event.reason !== 'expired' &&\n      event.reason !== 'delete' &&\n      event.reason !== 'clear'\n    ) {\n      return;\n    }\n\n    // The compressed LRU is keyed by path + base ETag + encoding, but the\n    // reverse index is keyed only by filesystem path.\n    this.untrackCompressedVariantByKey(event.key);\n  }\n\n  private untrackCompressedVariantByKey(cacheKey: string): void {\n    // Compressed cache keys are stored as path + base ETag + encoding, so drop\n    // the final two segments to recover the filesystem path used by the index.\n    const keyParts = cacheKey.split('::');\n    const fsPath = keyParts.slice(0, -2).join('::');\n\n    this.untrackCompressedVariant(fsPath, cacheKey);\n  }\n\n  private untrackCompressedVariant(fsPath: string, cacheKey: string): void {\n    // Missing index state is harmless here. This map only helps invalidate all\n    // variant keys for a file path later, it is not consulted when choosing\n    // what bytes or variant state to serve for the current request.\n    const existing = this.compressedContentIndex.get(fsPath);\n\n    if (!existing) {\n      return;\n    }\n\n    existing.delete(cacheKey);\n\n    // Remove the path entry entirely once no compressed variants remain for it.\n    if (existing.size === 0) {\n      this.compressedContentIndex.delete(fsPath);\n    }\n  }\n}\n","import type { FastifyReply } from 'fastify';\n\n/**\n * Add one or more values to the Vary header without duplicates.\n */\nexport function addToVaryHeader(\n  reply: FastifyReply,\n  ...values: string[]\n): void {\n  const existing = reply.getHeader('Vary');\n  const current = Array.isArray(existing)\n    ? existing.join(', ')\n    : ((existing ?? '') as string);\n\n  const vary = new Set(\n    current\n      .split(',')\n      .map((header) => header.trim())\n      .filter(Boolean),\n  );\n\n  for (const value of values) {\n    vary.add(value);\n  }\n\n  reply.header('Vary', Array.from(vary).join(', '));\n}\n","import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';\nimport { brotliCompress, constants as zlibConstants, gzip } from 'node:zlib';\nimport { promisify } from 'node:util';\nimport type { ResponseCompressionOptions } from '../types';\nimport { addToVaryHeader } from './http-header-utils';\n\nconst gzipAsync = promisify(gzip);\nconst brotliCompressAsync = promisify(brotliCompress);\n\nexport type ResponseEncoding = 'br' | 'gzip';\n\nexport interface NormalizedResponseCompressionOptions {\n  enabled: boolean;\n  threshold: number;\n  preferBrotli: boolean;\n  brotliQuality: number;\n  gzipLevel: number;\n}\n\nconst DEFAULT_OPTIONS: NormalizedResponseCompressionOptions = {\n  enabled: true,\n  threshold: 1024,\n  preferBrotli: true,\n  brotliQuality: 4,\n  gzipLevel: 6,\n};\n\nconst COMPRESSIBLE_CONTENT_TYPE_PREFIXES = [\n  'text/',\n  'application/json',\n  'application/ld+json',\n  'application/manifest+json',\n  'application/javascript',\n  'text/javascript',\n  'application/xml',\n  'text/xml',\n  'application/xhtml+xml',\n  'application/rss+xml',\n  'application/atom+xml',\n  'image/svg+xml',\n];\n\n/**\n * Normalize the public boolean/object config into one internal shape so the\n * hot path does not need to branch on user-facing option forms.\n */\nexport function normalizeResponseCompressionOptions(\n  options: boolean | ResponseCompressionOptions | undefined,\n): NormalizedResponseCompressionOptions {\n  if (options === false) {\n    return {\n      ...DEFAULT_OPTIONS,\n      enabled: false,\n    };\n  }\n\n  if (options === true || options === undefined) {\n    return { ...DEFAULT_OPTIONS };\n  }\n\n  return {\n    enabled: options.enabled ?? DEFAULT_OPTIONS.enabled,\n    threshold: options.threshold ?? DEFAULT_OPTIONS.threshold,\n    preferBrotli: options.preferBrotli ?? DEFAULT_OPTIONS.preferBrotli,\n    brotliQuality: options.brotliQuality ?? DEFAULT_OPTIONS.brotliQuality,\n    gzipLevel: options.gzipLevel ?? DEFAULT_OPTIONS.gzipLevel,\n  };\n}\n\n/**\n * Minimal content-type allowlist for generic response compression.\n *\n * Static file handling uses MIME metadata from the file layer, but the\n * server-level onSend hook needs a straightforward check based only on the\n * response header that was already chosen.\n */\nexport function isCompressibleContentType(\n  contentType: string | undefined,\n): boolean {\n  if (!contentType) {\n    return false;\n  }\n\n  const normalized = contentType.split(';')[0]?.trim().toLowerCase();\n\n  if (!normalized) {\n    return false;\n  }\n\n  return COMPRESSIBLE_CONTENT_TYPE_PREFIXES.some((prefix) =>\n    normalized.startsWith(prefix),\n  );\n}\n\n/**\n * Parse Accept-Encoding into a simple encoding -> q-value map.\n *\n * This intentionally supports the small subset we need: direct `br`, `gzip`,\n * and wildcard fallback. We do not need full RFC-grade preference sorting for\n * the current use case.\n */\nfunction parseAcceptEncoding(\n  acceptEncoding: string | string[] | undefined,\n): Map<string, number> {\n  const header = Array.isArray(acceptEncoding)\n    ? acceptEncoding.join(',')\n    : acceptEncoding;\n  const values = new Map<string, number>();\n\n  if (!header) {\n    return values;\n  }\n\n  for (const part of header.split(',')) {\n    const [encodingRaw, ...params] = part.trim().split(';');\n\n    if (!encodingRaw) {\n      continue;\n    }\n\n    let quality = 1;\n\n    for (const param of params) {\n      const [key, value] = param.trim().split('=');\n\n      if (key === 'q' && value) {\n        const parsed = Number.parseFloat(value);\n\n        if (!Number.isNaN(parsed)) {\n          quality = parsed;\n        }\n      }\n    }\n\n    values.set(encodingRaw.toLowerCase(), quality);\n  }\n\n  return values;\n}\n\n/**\n * Choose the best supported encoding for the current request.\n *\n * Preference is controlled by config (`preferBrotli`), while q-values still\n * gate whether an encoding is allowed at all.\n */\nexport function selectResponseEncoding(\n  acceptEncoding: string | string[] | undefined,\n  shouldPreferBrotli: boolean,\n): ResponseEncoding | null {\n  const parsed = parseAcceptEncoding(acceptEncoding);\n  const brQuality = parsed.get('br') ?? parsed.get('*') ?? 0;\n  const gzipQuality = parsed.get('gzip') ?? parsed.get('*') ?? 0;\n\n  if (brQuality <= 0 && gzipQuality <= 0) {\n    return null;\n  }\n\n  if (brQuality > gzipQuality) {\n    return 'br';\n  }\n\n  if (gzipQuality > brQuality) {\n    return 'gzip';\n  }\n\n  return shouldPreferBrotli\n    ? brQuality > 0\n      ? 'br'\n      : 'gzip'\n    : gzipQuality > 0\n      ? 'gzip'\n      : 'br';\n}\n\n/**\n * Derive a representation-specific ETag from a base file/response ETag.\n *\n * This lets compressed and identity responses vary independently while still\n * preserving the original validator as the underlying content identity.\n */\nexport function buildEncodedETag(\n  etag: string,\n  encoding: ResponseEncoding,\n): string {\n  const isWeakPrefix = etag.startsWith('W/');\n  const quoted = isWeakPrefix ? etag.slice(2) : etag;\n\n  if (quoted.startsWith('\"') && quoted.endsWith('\"')) {\n    return `${isWeakPrefix ? 'W/' : ''}\"${quoted.slice(1, -1)}--${encoding}\"`;\n  }\n\n  return `${etag}--${encoding}`;\n}\n\n/**\n * Simple If-None-Match matcher for the exact representation ETag we are about\n * to send. Wildcard `*` is supported because callers may use it to indicate\n * \"any current representation\".\n */\nexport function matchesIfNoneMatch(\n  ifNoneMatchHeader: string | string[] | undefined,\n  etag: string,\n): boolean {\n  const header = Array.isArray(ifNoneMatchHeader)\n    ? ifNoneMatchHeader.join(',')\n    : ifNoneMatchHeader;\n\n  if (!header) {\n    return false;\n  }\n\n  const normalizeWeakETag = (value: string): string =>\n    value.startsWith('W/') ? value.slice(2) : value;\n  const normalizedETag = normalizeWeakETag(etag);\n\n  return header\n    .split(',')\n    .map((value) => value.trim())\n    .some(\n      (value) => value === '*' || normalizeWeakETag(value) === normalizedETag,\n    );\n}\n\nexport async function compressPayload(\n  payload: Buffer,\n  encoding: ResponseEncoding,\n  options: NormalizedResponseCompressionOptions,\n): Promise<Buffer> {\n  if (encoding === 'br') {\n    return brotliCompressAsync(payload, {\n      params: {\n        [zlibConstants.BROTLI_PARAM_QUALITY]: options.brotliQuality,\n      },\n    });\n  }\n\n  return gzipAsync(payload, {\n    level: options.gzipLevel,\n  });\n}\n\n/**\n * Compress a non-streaming reply payload when it is safe and worthwhile.\n *\n * Important constraints:\n * - skips replies that already selected a representation (Content-Encoding)\n * - skips ranged responses because byte ranges and on-the-fly compression do\n *   not compose cleanly\n * - skips small payloads to avoid wasting CPU and bytes\n *\n * The hook is deliberately representation-aware:\n * - once we choose gzip/br, we must treat the compressed bytes as a distinct\n *   HTTP representation with their own validator (`ETag`)\n * - `HEAD` must negotiate the same representation metadata as `GET`, even\n *   though no body will actually be written to the socket\n * - `If-None-Match` must be evaluated against the representation we are about\n *   to send, not the original uncompressed bytes\n */\nexport async function compressReplyPayload(\n  request: FastifyRequest,\n  reply: FastifyReply,\n  payload: unknown,\n  options: boolean | ResponseCompressionOptions | undefined,\n): Promise<unknown> {\n  const normalized = normalizeResponseCompressionOptions(options);\n\n  if (!normalized.enabled) {\n    return payload;\n  }\n\n  // Safety guard: if headers are already written (e.g. hijacked reply or a\n  // race in edge cases), adding compression headers would be a no-op at best\n  // and would corrupt the response at worst. Bail out early.\n  if (reply.sent || reply.raw?.headersSent) {\n    return payload;\n  }\n\n  if (\n    reply.statusCode < 200 ||\n    reply.statusCode === 204 ||\n    reply.statusCode === 304\n  ) {\n    return payload;\n  }\n\n  if (request.headers.range || reply.hasHeader('Content-Range')) {\n    return payload;\n  }\n\n  if (reply.hasHeader('Content-Encoding')) {\n    return payload;\n  }\n\n  const contentType = reply.getHeader('Content-Type') as string | undefined;\n\n  if (!isCompressibleContentType(contentType)) {\n    return payload;\n  }\n\n  // This generic hook only handles fully materialized payloads. Streamed\n  // responses stay on their existing path because on-the-fly compression would\n  // need different range/backpressure semantics.\n  if (typeof payload !== 'string' && !Buffer.isBuffer(payload)) {\n    return payload;\n  }\n\n  const bufferPayload =\n    typeof payload === 'string' ? Buffer.from(payload) : payload;\n\n  // Match the static-content behavior: small bodies usually are not worth the\n  // extra CPU, header bytes, and cache fragmentation that compression adds.\n  if (bufferPayload.length < normalized.threshold) {\n    return payload;\n  }\n\n  // Only add Vary when the payload is otherwise eligible for compression.\n  // That keeps the header tied to actual response-shape variation.\n  addToVaryHeader(reply, 'Accept-Encoding');\n\n  const encoding = selectResponseEncoding(\n    request.headers['accept-encoding'],\n    normalized.preferBrotli,\n  );\n\n  if (!encoding) {\n    return payload;\n  }\n\n  // Compression is attempted before mutating representation headers so we can\n  // bail out cleanly when the encoded bytes are not actually smaller.\n  const compressed = await compressPayload(bufferPayload, encoding, normalized);\n\n  if (compressed.length >= bufferPayload.length) {\n    return payload;\n  }\n\n  // If upstream already attached an ETag to the identity response, convert it\n  // into a representation-specific validator before the reply leaves the hook.\n  // Otherwise caches would see the same ETag for both compressed and\n  // uncompressed bytes and could serve or validate the wrong representation.\n  const existingETag = reply.getHeader('ETag');\n  const currentETag = Array.isArray(existingETag)\n    ? existingETag[0]\n    : existingETag;\n\n  if (typeof currentETag === 'string' && currentETag.length > 0) {\n    reply.header('ETag', buildEncodedETag(currentETag, encoding));\n  }\n\n  const encodedETag = reply.getHeader('ETag');\n  const responseETag =\n    typeof encodedETag === 'string'\n      ? encodedETag\n      : Array.isArray(encodedETag) && typeof encodedETag[0] === 'string'\n        ? encodedETag[0]\n        : undefined;\n\n  reply.header('Content-Encoding', encoding);\n\n  // Conditional GET/HEAD must be checked against the final representation\n  // validator, not the base identity ETag. If the client already has the\n  // encoded variant we can return 304 without sending the compressed body.\n  if (\n    responseETag &&\n    matchesIfNoneMatch(request.headers['if-none-match'], responseETag)\n  ) {\n    reply.code(304);\n    reply.removeHeader('Content-Length');\n    return '';\n  }\n\n  if (request.method === 'HEAD') {\n    // HEAD still needs to advertise the metadata of the representation that a\n    // GET would have produced after negotiation.\n    //\n    // Fastify still uses the payload returned from onSend to derive outgoing\n    // metadata for HEAD responses. Returning the compressed buffer keeps the\n    // wire-level Content-Length aligned with the corresponding GET, while\n    // Fastify itself suppresses the actual response body for HEAD.\n    reply.header('Content-Length', compressed.length.toString());\n    return compressed;\n  }\n\n  // We already know the exact compressed byte length here, so set it\n  // explicitly instead of relying on later framework inference.\n  reply.header('Content-Length', compressed.length.toString());\n\n  return compressed;\n}\n\n/**\n * Register the generic response-compression hook for dynamic API/web replies.\n *\n * Static file serving uses its own representation-selection path so it can keep\n * ETags, range requests, and cache invalidation tied to concrete file variants.\n *\n * This hook uses an async buffer approach (brotli/gzip in memory) rather than a\n * streaming Transform. The async approach preserves Content-Length, ETag\n * representation variants, and If-None-Match 304 responses.\n *\n * Safety note: async onSend hooks interact with Fastify 5's wrapThenable mechanism.\n * When an async route handler calls reply.send() and returns undefined,\n * wrapThenable can fire a second reply.send(undefined) while the async onSend\n * hook is still pending (reply.sent may remain false until headers are written).\n *\n * To avoid this race, Unirend async route handlers return the payload directly\n * instead of calling reply.send() manually. That lets wrapThenable make exactly\n * one reply.send() call.\n */\nexport function registerResponseCompression(\n  fastifyInstance: FastifyInstance,\n  options: boolean | ResponseCompressionOptions | undefined,\n): void {\n  const normalized = normalizeResponseCompressionOptions(options);\n\n  if (!normalized.enabled) {\n    return;\n  }\n\n  fastifyInstance.addHook('onSend', async (request, reply, payload) => {\n    try {\n      return await compressReplyPayload(request, reply, payload, normalized);\n    } catch (error) {\n      // Compression failure must not take down the server — fall back to the\n      // uncompressed payload so the request still completes.\n      request.log.error(\n        { error },\n        'Response compression failed; sending uncompressed',\n      );\n      return payload;\n    }\n  });\n}\n","/**\n * Escapes HTML special characters to prevent XSS attacks\n *\n * Converts the following characters to HTML entities:\n * - & → &amp;\n * - < → &lt;\n * - > → &gt;\n * - \" → &quot;\n * - ' → &#39;\n *\n * @param str - The string to escape\n * @returns The escaped string safe for insertion into HTML\n *\n * @example\n * ```ts\n * escapeHTML('<script>alert(\"xss\")</script>');\n * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'\n * ```\n */\nexport function escapeHTML(str: string): string {\n  return str\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&#39;');\n}\n\n/**\n * Escapes a string for safe insertion into double-quoted HTML attributes.\n *\n * Converts the following characters to HTML entities:\n * - & → &amp;\n * - \" → &quot;\n * - < → &lt;\n * - > → &gt;\n *\n * @param str - The string to escape\n * @returns The escaped string safe for insertion into HTML attributes\n */\nexport function escapeHTMLAttr(str: string): string {\n  return str\n    .replace(/&/g, '&amp;')\n    .replace(/\"/g, '&quot;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;');\n}\n","/**\n * Recursively freezes an object and all nested objects, making the entire\n * structure immutable (deep freeze, vs Object.freeze which is shallow).\n *\n * Pure utility with no dependencies — safe to import in both server and\n * client code.\n *\n * Used to freeze public app config clones (so they cannot be mutated\n * within a request, even on nested sub-objects) and debug context snapshots\n * returned by useRequestContextObjectRaw(). The source object is never affected.\n */\nexport function deepFreeze<T>(obj: T): T {\n  if (obj === null || typeof obj !== 'object') {\n    return obj;\n  }\n\n  Object.freeze(obj);\n\n  for (const value of Object.values(obj as object)) {\n    if (value && typeof value === 'object' && !Object.isFrozen(value)) {\n      deepFreeze(value);\n    }\n  }\n\n  return obj;\n}\n\nexport const MINIMUM_SUPPORTED_NODE_MAJOR = 25;\n\nexport type RuntimeName = 'bun' | 'node' | 'unknown';\n\nexport interface RuntimeSupportInfo {\n  runtime: RuntimeName;\n  isSupported: boolean;\n  minimumNodeMajor: number;\n  nodeVersion?: string;\n  bunVersion?: string;\n}\n\ninterface RuntimeEnvironmentLike {\n  Bun?: unknown;\n  process?: {\n    versions?: Partial<Record<'node' | 'bun', string>>;\n  };\n}\n\nfunction parseMajorVersion(version?: string): number | undefined {\n  if (!version) {\n    return undefined;\n  }\n\n  const [majorPart] = version.split('.');\n  const major = Number.parseInt(majorPart, 10);\n\n  return Number.isFinite(major) ? major : undefined;\n}\n\n/**\n * Detect the current JavaScript runtime and whether it satisfies Unirend's\n * runtime requirement. Bun is treated as supported even if it reports an older\n * Node compatibility version via `process.versions.node`.\n */\nexport function getRuntimeSupportInfo(\n  minimumNodeMajor = MINIMUM_SUPPORTED_NODE_MAJOR,\n  environment: RuntimeEnvironmentLike = globalThis as RuntimeEnvironmentLike,\n): RuntimeSupportInfo {\n  const versions = environment.process?.versions;\n  const nodeVersion =\n    typeof versions?.node === 'string' ? versions.node : undefined;\n  const bunVersion =\n    typeof versions?.bun === 'string' ? versions.bun : undefined;\n  const isBun =\n    typeof environment.Bun !== 'undefined' || typeof bunVersion === 'string';\n\n  if (isBun) {\n    return {\n      runtime: 'bun',\n      isSupported: true,\n      minimumNodeMajor,\n      nodeVersion,\n      bunVersion,\n    };\n  }\n\n  if (!nodeVersion) {\n    return {\n      runtime: 'unknown',\n      isSupported: false,\n      minimumNodeMajor,\n    };\n  }\n\n  const nodeMajor = parseMajorVersion(nodeVersion);\n\n  return {\n    runtime: 'node',\n    isSupported: typeof nodeMajor === 'number' && nodeMajor >= minimumNodeMajor,\n    minimumNodeMajor,\n    nodeVersion,\n  };\n}\n\n/**\n * Convenience boolean check for Unirend's runtime requirement.\n */\nexport function isSupportedRuntime(\n  minimumNodeMajor = MINIMUM_SUPPORTED_NODE_MAJOR,\n  environment?: RuntimeEnvironmentLike,\n): boolean {\n  return getRuntimeSupportInfo(minimumNodeMajor, environment).isSupported;\n}\n\n/**\n * Throw a descriptive error when the current runtime does not satisfy\n * Unirend's runtime requirement.\n */\nexport function assertSupportedRuntime(\n  minimumNodeMajor = MINIMUM_SUPPORTED_NODE_MAJOR,\n  environment?: RuntimeEnvironmentLike,\n): void {\n  const runtimeInfo = getRuntimeSupportInfo(minimumNodeMajor, environment);\n\n  if (runtimeInfo.isSupported) {\n    return;\n  }\n\n  const detectedVersion = runtimeInfo.nodeVersion ?? 'unknown';\n\n  throw new Error(\n    `Unirend requires Node >= ${minimumNodeMajor} or Bun. Detected ${runtimeInfo.runtime} runtime with Node version ${detectedVersion}.`,\n  );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,gBAAe;AACf,kBAAiB;AACjB,yBAAmB;AACnB,sBAAyB;AACzB,uBAAmD;;;ACD5C,SAAS,gBACd,UACG,QACG;AACN,QAAM,WAAW,MAAM,UAAU,MAAM;AACvC,QAAM,UAAU,MAAM,QAAQ,QAAQ,IAClC,SAAS,KAAK,IAAI,IAChB,YAAY;AAElB,QAAM,OAAO,IAAI;AAAA,IACf,QACG,MAAM,GAAG,EACT,IAAI,CAAC,WAAW,OAAO,KAAK,CAAC,EAC7B,OAAO,OAAO;AAAA,EACnB;AAEA,aAAW,SAAS,QAAQ;AAC1B,SAAK,IAAI,KAAK;AAAA,EAChB;AAEA,QAAM,OAAO,QAAQ,MAAM,KAAK,IAAI,EAAE,KAAK,IAAI,CAAC;AAClD;;;ACzBA,uBAAiE;AACjE,uBAA0B;AAI1B,IAAM,gBAAY,4BAAU,qBAAI;AAChC,IAAM,0BAAsB,4BAAU,+BAAc;AAYpD,IAAM,kBAAwD;AAAA,EAC5D,SAAS;AAAA,EACT,WAAW;AAAA,EACX,cAAc;AAAA,EACd,eAAe;AAAA,EACf,WAAW;AACb;AAEA,IAAM,qCAAqC;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMO,SAAS,oCACd,SACsC;AACtC,MAAI,YAAY,OAAO;AACrB,WAAO;AAAA,MACL,GAAG;AAAA,MACH,SAAS;AAAA,IACX;AAAA,EACF;AAEA,MAAI,YAAY,QAAQ,YAAY,QAAW;AAC7C,WAAO,EAAE,GAAG,gBAAgB;AAAA,EAC9B;AAEA,SAAO;AAAA,IACL,SAAS,QAAQ,WAAW,gBAAgB;AAAA,IAC5C,WAAW,QAAQ,aAAa,gBAAgB;AAAA,IAChD,cAAc,QAAQ,gBAAgB,gBAAgB;AAAA,IACtD,eAAe,QAAQ,iBAAiB,gBAAgB;AAAA,IACxD,WAAW,QAAQ,aAAa,gBAAgB;AAAA,EAClD;AACF;AASO,SAAS,0BACd,aACS;AACT,MAAI,CAAC,aAAa;AAChB,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,YAAY,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,YAAY;AAEjE,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AAEA,SAAO,mCAAmC;AAAA,IAAK,CAAC,WAC9C,WAAW,WAAW,MAAM;AAAA,EAC9B;AACF;AASA,SAAS,oBACP,gBACqB;AACrB,QAAM,SAAS,MAAM,QAAQ,cAAc,IACvC,eAAe,KAAK,GAAG,IACvB;AACJ,QAAM,SAAS,oBAAI,IAAoB;AAEvC,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AAEA,aAAW,QAAQ,OAAO,MAAM,GAAG,GAAG;AACpC,UAAM,CAAC,aAAa,GAAG,MAAM,IAAI,KAAK,KAAK,EAAE,MAAM,GAAG;AAEtD,QAAI,CAAC,aAAa;AAChB;AAAA,IACF;AAEA,QAAI,UAAU;AAEd,eAAW,SAAS,QAAQ;AAC1B,YAAM,CAAC,KAAK,KAAK,IAAI,MAAM,KAAK,EAAE,MAAM,GAAG;AAE3C,UAAI,QAAQ,OAAO,OAAO;AACxB,cAAM,SAAS,OAAO,WAAW,KAAK;AAEtC,YAAI,CAAC,OAAO,MAAM,MAAM,GAAG;AACzB,oBAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAEA,WAAO,IAAI,YAAY,YAAY,GAAG,OAAO;AAAA,EAC/C;AAEA,SAAO;AACT;AAQO,SAAS,uBACd,gBACA,oBACyB;AACzB,QAAM,SAAS,oBAAoB,cAAc;AACjD,QAAM,YAAY,OAAO,IAAI,IAAI,KAAK,OAAO,IAAI,GAAG,KAAK;AACzD,QAAM,cAAc,OAAO,IAAI,MAAM,KAAK,OAAO,IAAI,GAAG,KAAK;AAE7D,MAAI,aAAa,KAAK,eAAe,GAAG;AACtC,WAAO;AAAA,EACT;AAEA,MAAI,YAAY,aAAa;AAC3B,WAAO;AAAA,EACT;AAEA,MAAI,cAAc,WAAW;AAC3B,WAAO;AAAA,EACT;AAEA,SAAO,qBACH,YAAY,IACV,OACA,SACF,cAAc,IACZ,SACA;AACR;AAQO,SAAS,iBACd,MACA,UACQ;AACR,QAAM,eAAe,KAAK,WAAW,IAAI;AACzC,QAAM,SAAS,eAAe,KAAK,MAAM,CAAC,IAAI;AAE9C,MAAI,OAAO,WAAW,GAAG,KAAK,OAAO,SAAS,GAAG,GAAG;AAClD,WAAO,GAAG,eAAe,OAAO,EAAE,IAAI,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK,QAAQ;AAAA,EACxE;AAEA,SAAO,GAAG,IAAI,KAAK,QAAQ;AAC7B;AAOO,SAAS,mBACd,mBACA,MACS;AACT,QAAM,SAAS,MAAM,QAAQ,iBAAiB,IAC1C,kBAAkB,KAAK,GAAG,IAC1B;AAEJ,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AAEA,QAAM,oBAAoB,CAAC,UACzB,MAAM,WAAW,IAAI,IAAI,MAAM,MAAM,CAAC,IAAI;AAC5C,QAAM,iBAAiB,kBAAkB,IAAI;AAE7C,SAAO,OACJ,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAC3B;AAAA,IACC,CAAC,UAAU,UAAU,OAAO,kBAAkB,KAAK,MAAM;AAAA,EAC3D;AACJ;AAEA,eAAsB,gBACpB,SACA,UACA,SACiB;AACjB,MAAI,aAAa,MAAM;AACrB,WAAO,oBAAoB,SAAS;AAAA,MAClC,QAAQ;AAAA,QACN,CAAC,iBAAAA,UAAc,oBAAoB,GAAG,QAAQ;AAAA,MAChD;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO,UAAU,SAAS;AAAA,IACxB,OAAO,QAAQ;AAAA,EACjB,CAAC;AACH;;;AFtEA,SAAS,sBAAsB,QAAsC;AACnE,MACE,OAAO,YAAY,SACnB,OAAQ,OAA2C,OAAO,UAC1D;AACA,WAAO,QAAQ,QAAQ;AAAA,EACzB;AAEA,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAS,MAAM;AACnB,cAAQ;AACR,cAAQ;AAAA,IACV;AAEA,UAAM,UAAU,CAAC,UAAiB;AAChC,cAAQ;AACR,aAAO,KAAK;AAAA,IACd;AAEA,UAAM,UAAU,MAAM;AACpB,aAAO,IAAI,QAAQ,MAAM;AACzB,aAAO,IAAI,SAAS,OAAO;AAAA,IAC7B;AAEA,WAAO,KAAK,QAAQ,MAAM;AAC1B,WAAO,KAAK,SAAS,OAAO;AAAA,EAC9B,CAAC;AACH;AAuBA,SAAS,WACP,QACA,UACkD;AAClD,MAAI,CAAC,OAAO,WAAW,QAAQ,GAAG;AAChC,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,OAAO,MAAM,CAAC;AAG3B,MAAI,KAAK,SAAS,GAAG,GAAG;AACtB,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,gBAAgB,KAAK,IAAI;AAEvC,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,MAAM,CAAC;AACxB,QAAM,SAAS,MAAM,CAAC;AAEtB,MAAI,aAAa,MAAM,WAAW,IAAI;AACpC,WAAO;AAAA,EACT;AAEA,MAAI;AACJ,MAAI;AAEJ,MAAI,aAAa,IAAI;AAEnB,UAAM,SAAS,SAAS,QAAQ,EAAE;AAClC,YAAQ,KAAK,IAAI,GAAG,WAAW,MAAM;AACrC,UAAM,WAAW;AAAA,EACnB,WAAW,WAAW,IAAI;AAExB,YAAQ,SAAS,UAAU,EAAE;AAC7B,UAAM,WAAW;AAAA,EACnB,OAAO;AACL,YAAQ,SAAS,UAAU,EAAE;AAC7B,UAAM,SAAS,QAAQ,EAAE;AAAA,EAC3B;AAGA,MAAI,SAAS,YAAY,QAAQ,KAAK;AACpC,WAAO;AAAA,EACT;AAGA,QAAM,KAAK,IAAI,KAAK,WAAW,CAAC;AAEhC,SAAO,CAAC,OAAO,GAAG;AACpB;AAeO,IAAM,qBAAN,MAAyB;AAAA;AAAA,EAEtB;AAAA;AAAA,EACA;AAAA;AAAA;AAAA,EAGS;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAKA;AAAA;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA;AAAA,EAIA;AAAA;AAAA;AAAA;AAAA;AAAA,EAIA,yBAAmD,oBAAI,IAAI;AAAA;AAAA,EAG3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQjB,YACE,SACA,QACA;AACA,UAAM;AAAA,MACJ,iBAAiB,CAAC;AAAA,MAClB,YAAY,CAAC;AAAA,MACb,mBAAmB,IAAI,OAAO;AAAA;AAAA,MAC9B,eAAe;AAAA,MACf,sBAAsB,KAAK,OAAO;AAAA;AAAA,MAClC,mBAAmB;AAAA,MACnB,mBAAmB,KAAK;AAAA;AAAA,MACxB,mBAAmB,KAAK,KAAK;AAAA;AAAA,MAC7B,eAAe;AAAA,MACf,wBAAwB;AAAA,MACxB,cAAc;AAAA,IAChB,IAAI;AAEJ,SAAK,mBAAmB;AACxB,SAAK,eAAe;AACpB,SAAK,wBAAwB;AAC7B,SAAK,mBAAmB;AACxB,SAAK,mBAAmB;AACxB,SAAK,cAAc,oCAAoC,WAAW;AAClE,SAAK,SAAS;AAGd,SAAK,iBAAiB,KAAK,wBAAwB,cAAc;AAGjE,SAAK,YAAY,KAAK,mBAAmB,SAAS;AAGlD,UAAM,aAAa,mBAAmB,IAAI,mBAAmB;AAE7D,SAAK,YAAY,IAAI,0BAAyB,cAAc,EAAE,WAAW,CAAC;AAC1E,SAAK,eAAe,IAAI,0BAAyB,cAAc;AAAA,MAC7D;AAAA,MACA,SAAS;AAAA,IACX,CAAC;AAED,SAAK,yBAAyB,IAAI;AAAA,MAChC;AAAA,MACA;AAAA,QACE;AAAA,QACA,SAAS;AAAA;AAAA;AAAA,QAGT,UAAU,CAAC,UAAU,KAAK,mCAAmC,KAAK;AAAA,QAClE,iBAAiB,CAAC,SAAS,WAAW,UAAU,OAAO;AAAA,MACzD;AAAA,IACF;AACA,SAAK,YAAY,IAAI,0BAAiC,kBAAkB;AAAA,MACtE;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,MAAa,QACX,cACA,SACqB;AAErB,QAAI;AACF,YAAM;AAAA,QACJ,wBAAwB;AAAA,QACxB;AAAA,QACA;AAAA,MACF,IAAI,WAAW,CAAC;AAIhB,YAAM,aAAa,KAAK,UAAU,IAAI,YAAY;AAGlD,UAAI,OAA+B;AAGnC,UAAI,YAAY;AACd,YAAI,cAAc,YAAY;AAE5B,iBAAO,EAAE,QAAQ,YAAY;AAAA,QAC/B,WAAW,eAAe,MAAM;AAE9B,iBAAO;AAAA,QACT;AAAA,MACF;AAGA,UAAI,CAAC,MAAM;AACT,YAAI;AACF,gBAAM,WAAW,MAAM,UAAAC,QAAG,SAAS,KAAK,YAAY;AAGpD,cAAI,CAAC,SAAS,OAAO,GAAG;AAEtB,iBAAK,UAAU;AAAA,cACb;AAAA,cACA,EAAE,UAAU,KAAK;AAAA,cACjB,KAAK;AAAA,YACP;AAEA,mBAAO,EAAE,QAAQ,YAAY;AAAA,UAC/B;AAGA,iBAAO;AAAA,YACL,QAAQ;AAAA;AAAA,YACR,MAAM,SAAS;AAAA,YACf,OAAO,SAAS;AAAA;AAAA,YAEhB,SAAS,SAAS;AAAA,UACpB;AAIA,eAAK,UAAU,IAAI,cAAc,IAAI;AAAA,QACvC,SAAS,OAAO;AAGd,eAAK,UAAU;AAAA,YACb;AAAA,YACA,EAAE,UAAU,KAAK;AAAA,YACjB,KAAK;AAAA,UACP;AAIA,cACE,iBAAiB,SACjB,UAAU,SACT,MAAgC,SAAS,YAC1C,KAAK,QACL;AACA,iBAAK,OAAO;AAAA,cACV;AAAA,gBACE,KAAK;AAAA,gBACL,MAAM;AAAA,cACR;AAAA,cACA;AAAA,YACF;AAAA,UACF;AAEA,iBAAO,EAAE,QAAQ,YAAY;AAAA,QAC/B;AAAA,MACF;AAGA,YAAM,eAAe,KAAK,MAAM,YAAY;AAK5C,UAAI,OAAO,KAAK,UAAU,IAAI,YAAY;AAG1C,UAAI,CAAC,MAAM;AAET,YAAI,KAAK,QAAQ,KAAK,kBAAkB;AAEtC,cAAI,MAAM,KAAK,aAAa,IAAI,YAAY;AAG5C,cAAI,CAAC,KAAK;AACR,gBAAI;AACF,oBAAM,MAAM,UAAAA,QAAG,SAAS,SAAS,YAAY;AAC7C,mBAAK,aAAa,IAAI,cAAc,GAAG;AAAA,YACzC,SAAS,OAAO;AAGd,oBAAM,UAAU;AAEhB,kBAAI,KAAK,QAAQ;AACf,qBAAK,OAAO;AAAA,kBACV;AAAA,oBACE,KAAK;AAAA,oBACL,MAAM;AAAA,oBACN,MAAM,QAAQ;AAAA,kBAChB;AAAA,kBACA;AAAA,gBACF;AAAA,cACF;AAGA,oBAAM;AAAA,YACR;AAAA,UACF;AAGA,gBAAM,OAAO,mBAAAC,QAAO,WAAW,QAAQ,EAAE,OAAO,GAAG,EAAE,OAAO,QAAQ;AACpE,iBAAO,IAAI,IAAI;AAAA,QACjB,OAAO;AAGL,iBAAO,MAAM,KAAK,IAAI,IAAI,OAAO,KAAK,OAAO,CAAC;AAAA,QAChD;AAGA,aAAK,UAAU,IAAI,cAAc,IAAI;AAAA,MACvC;AAIA,YAAM,mBACJ,yBAAyB,KAAK,iBAAiB,YAAY;AAG7D,YAAM,WAAW,KAAK,YAAY,YAAY;AAO9C,UAAI;AAEJ,UAAI,KAAK,QAAQ,KAAK,kBAAkB;AAEtC,YAAI,UAAU,KAAK,aAAa,IAAI,YAAY;AAGhD,YAAI,CAAC,SAAS;AACZ,cAAI;AACF,sBAAU,MAAM,UAAAD,QAAG,SAAS,SAAS,YAAY;AACjD,iBAAK,aAAa,IAAI,cAAc,OAAO;AAAA,UAC7C,SAAS,OAAO;AAEd,kBAAM,UAAU;AAGhB,gBAAI,QAAQ,SAAS,UAAU;AAE7B,mBAAK,UAAU;AAAA,gBACb;AAAA,gBACA,EAAE,UAAU,KAAK;AAAA,gBACjB,KAAK;AAAA,cACP;AAEA,mBAAK,UAAU,OAAO,YAAY;AAClC,mBAAK,aAAa,OAAO,YAAY;AAErC,qBAAO,EAAE,QAAQ,YAAY;AAAA,YAC/B;AAGA,gBAAI,KAAK,QAAQ;AACf,mBAAK,OAAO;AAAA,gBACV;AAAA,kBACE,KAAK;AAAA,kBACL,MAAM;AAAA,kBACN,MAAM,QAAQ;AAAA,gBAChB;AAAA,gBACA;AAAA,cACF;AAAA,YACF;AAEA,kBAAM;AAAA,UACR;AAAA,QACF;AAEA,sBAAc,EAAE,cAAc,OAAO,MAAM,QAAQ;AAAA,MACrD,OAAO;AACL,sBAAc;AAAA,UACZ,cAAc;AAAA,UACd,cAAc,CAACE,aAAY,UAAAF,QAAG,iBAAiB,cAAcE,QAAO;AAAA,QACtE;AAAA,MACF;AAIA,YAAM,6BACJ,KAAK,YAAY,WACjB,CAAC,YAAY,gBACb,0BAA0B,QAAQ,KAClC,YAAY,KAAK,UAAU,KAAK,YAAY;AAE9C,YAAM,mBAAmB,6BACrB,uBAAuB,gBAAgB,KAAK,YAAY,YAAY,IACpE;AACJ,UAAI;AAIJ,UAAI,CAAC,YAAY,gBAAgB,kBAAkB;AACjD,cAAM,qBAAqB,KAAK;AAAA,UAC9B;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAEA,cAAM,gBACJ,KAAK,uBAAuB,IAAI,kBAAkB;AAEpD,YAAI,aACF,eAAe,SAAS,eAAe,cAAc,OAAO;AAC9D,cAAM,yBAAyB,eAAe,SAAS;AACvD,cAAM,wBAAwB,eAAe,SAAS;AAEtD,YAAI,CAAC,iBAAiB,uBAAuB;AAK3C,cAAI,CAAC,uBAAuB;AAC1B,iBAAK,yBAAyB,cAAc,kBAAkB;AAAA,UAChE;AAEA,uBAAa,MAAM;AAAA,YACjB,YAAY;AAAA,YACZ;AAAA,YACA,KAAK;AAAA,UACP;AAAA,QACF;AAKA,YAAI,cAAc,WAAW,SAAS,YAAY,KAAK,QAAQ;AAC7D,6BAAmB;AAKnB,cACE,CAAC,KAAK,uBAAuB,IAAI,kBAAkB,KACnD,CAAC,uBACD;AAIA,iBAAK,uBAAuB,IAAI,oBAAoB;AAAA,cAClD,MAAM;AAAA,cACN,MAAM;AAAA,YACR,CAAC;AAKD,kBAAM,yBACJ,KAAK,uBAAuB,IAAI,YAAY;AAE9C,gBAAI,wBAAwB;AAC1B,qCAAuB,IAAI,kBAAkB;AAAA,YAC/C,OAAO;AACL,mBAAK,uBAAuB;AAAA,gBAC1B;AAAA,gBACA,oBAAI,IAAI,CAAC,kBAAkB,CAAC;AAAA,cAC9B;AAAA,YACF;AAAA,UACF;AAEA,wBAAc;AAAA,YACZ,cAAc;AAAA,YACd,MAAM;AAAA,UACR;AAAA,QACF,OAAO;AAGL,cAAI,CAAC,0BAA0B,CAAC,uBAAuB;AAIrD,iBAAK,uBAAuB,OAAO,kBAAkB;AACrD,iBAAK,uBAAuB,IAAI,oBAAoB;AAAA,cAClD,MAAM;AAAA,YACR,CAAC;AAID,kBAAM,sBACJ,KAAK,uBAAuB,IAAI,YAAY;AAE9C,gBAAI,qBAAqB;AACvB,kCAAoB,IAAI,kBAAkB;AAAA,YAC5C,OAAO;AACL,mBAAK,uBAAuB;AAAA,gBAC1B;AAAA,gBACA,oBAAI,IAAI,CAAC,kBAAkB,CAAC;AAAA,cAC9B;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,YAAM,eAAe,mBACjB,iBAAiB,MAAM,gBAAgB,IACvC;AAEJ,UAAI,cAAc,mBAAmB,YAAY,YAAY,GAAG;AAC9D,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,MAAM;AAAA,UACN;AAAA,UACA,iBAAiB;AAAA,UACjB,sBAAsB;AAAA,QACxB;AAAA,MACF;AAEA,aAAO;AAAA,QACL,QAAQ;AAAA,QACR;AAAA,QACA,MAAM;AAAA,QACN,UAAU;AAAA,QACV;AAAA,QACA;AAAA,QACA,SAAS;AAAA,QACT,iBAAiB;AAAA,QACjB,sBAAsB;AAAA,QACtB;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AAEd,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,MAAa,UACX,KACA,OACA,cACA,SAC0B;AAS1B,UAAM,SAAS,MAAM,KAAK,QAAQ,cAAc;AAAA,MAC9C,GAAG;AAAA;AAAA,MAEH,YAAY,IAAI,QAAQ,eAAe;AAAA;AAAA;AAAA,MAGvC,gBAAgB,IAAI,QAAQ,iBAAiB;AAAA,IAC/C,CAAC;AAGD,QAAI,OAAO,WAAW,aAAa;AAEjC,aAAO,EAAE,QAAQ,OAAO,QAAQ,YAAY;AAAA,IAC9C,WAAW,OAAO,WAAW,SAAS;AAEpC,aAAO,EAAE,QAAQ,OAAO,QAAQ,SAAS,OAAO,OAAO,MAAM;AAAA,IAC/D;AAKA,UAAM,kBAAkB,MAAM;AAC5B,MAAC,IAAoC,gBAAgB;AAAA,IACvD;AAEA,QAAI,OAAO,WAAW,gBAAgB;AAYpC,UAAI,OAAO,sBAAsB;AAC/B,wBAAgB,OAAO,iBAAiB;AAAA,MAC1C;AAIA,UAAI,OAAO,iBAAiB;AAC1B,cAAM,OAAO,oBAAoB,OAAO,eAAe;AAAA,MACzD;AAEA,YACG,KAAK,GAAG,EACR,OAAO,QAAQ,OAAO,IAAI,EAC1B,OAAO,iBAAiB,OAAO,YAAY;AAE9C,YAAM,IAAI,mBAAmB,KAAK;AAClC,sBAAgB;AAChB,YAAM,OAAO;AACb,YAAM,IAAI,UAAU,KAAK,MAAM,WAAW,CAAwB;AAClE,YAAM,IAAI,IAAI;AAEd,aAAO,EAAE,QAAQ,MAAM,YAAY,IAAI;AAAA,IACzC;AAWA,UAAM,qBAAqB,OAAO,mBAC9B,KAAK,wBACL,KAAK;AAIT,QAAI,OAAO,sBAAsB;AAC/B,sBAAgB,OAAO,iBAAiB;AAAA,IAC1C;AAIA,QAAI,OAAO,iBAAiB;AAC1B,YAAM,OAAO,oBAAoB,OAAO,eAAe;AAAA,IACzD;AAGA,UACG,OAAO,iBAAiB,OAAO,YAAY,EAC3C,OAAO,QAAQ,OAAO,IAAI,EAC1B,OAAO,iBAAiB,kBAAkB,EAC1C,KAAK,OAAO,QAAQ;AAKvB,QAAI,OAAO,QAAQ,cAAc;AAC/B,YAAM,OAAO,iBAAiB,OAAO;AAAA,IACvC;AAGA,UAAM,cAAc,IAAI,QAAQ;AAGhC,QAAI,eAAe,OAAO,QAAQ,cAAc;AAC9C,YAAM,QAAQ,WAAW,aAAa,OAAO,KAAK,IAAI;AAEtD,UAAI,UAAU,aAAa;AAEzB,cAAM,OAAO,KAAK,UAAU,EAAE,OAAO,uBAAuB,CAAC;AAC7D,cACG,KAAK,GAAG,EACR,OAAO,iBAAiB,UAAU,EAClC,KAAK,kBAAkB,EACvB,OAAO,kBAAkB,OAAO,OAAO,WAAW,IAAI,CAAC,CAAC;AAC3D,cAAM,IAAI,mBAAmB,KAAK;AAClC,wBAAgB;AAChB,cAAM,OAAO;AACb,cAAM,IAAI,UAAU,KAAK,MAAM,WAAW,CAAwB;AAClE,cAAM,IAAI,IAAI,IAAI,WAAW,SAAS,SAAY,IAAI;AACtD,eAAO,EAAE,QAAQ,MAAM,YAAY,IAAI;AAAA,MACzC,WAAW,UAAU,iBAAiB;AACpC,cAAM,OAAO,KAAK,UAAU,EAAE,OAAO,wBAAwB,CAAC;AAC9D,cACG,KAAK,GAAG,EACR,OAAO,iBAAiB,UAAU,EAClC,KAAK,kBAAkB,EACvB,OAAO,iBAAiB,WAAW,OAAO,KAAK,IAAI,EAAE,EACrD,OAAO,kBAAkB,OAAO,OAAO,WAAW,IAAI,CAAC,CAAC;AAC3D,cAAM,IAAI,mBAAmB,KAAK;AAClC,wBAAgB;AAChB,cAAM,OAAO;AACb,cAAM,IAAI,UAAU,KAAK,MAAM,WAAW,CAAwB;AAClE,cAAM,IAAI,IAAI,IAAI,WAAW,SAAS,SAAY,IAAI;AACtD,eAAO,EAAE,QAAQ,MAAM,YAAY,IAAI;AAAA,MACzC;AAEA,YAAM,CAAC,OAAO,GAAG,IAAI;AACrB,YAAM,YAAY,MAAM,QAAQ;AAChC,YAAM,cAAc,OAAO,QAAQ,aAAa,EAAE,OAAO,IAAI,CAAC;AAE9D,YAAM,sBAAsB,WAAW;AAGvC,YACG,KAAK,GAAG,EACR,OAAO,iBAAiB,SAAS,KAAK,IAAI,GAAG,IAAI,OAAO,KAAK,IAAI,EAAE,EACnE,OAAO,kBAAkB,UAAU,SAAS,CAAC;AAEhD,YAAM,IAAI,mBAAmB,KAAK;AAClC,sBAAgB;AAChB,YAAM,OAAO;AACb,YAAM,IAAI,UAAU,KAAK,MAAM,WAAW,CAAwB;AAGlE,UAAI,IAAI,WAAW,QAAQ;AACzB,cAAM,IAAI,IAAI;AACd,eAAO,EAAE,QAAQ,MAAM,YAAY,IAAI;AAAA,MACzC;AAGA,gBAAM,0BAAS,aAAa,MAAM,GAAG;AACrC,aAAO,EAAE,QAAQ,MAAM,YAAY,IAAI;AAAA,IACzC;AAGA,QAAI,IAAI,WAAW,QAAQ;AAIzB,YAAM,oBACJ,OAAO,mBAAmB,CAAC,OAAO,QAAQ,eACtC,OAAO,QAAQ,KAAK,SACpB,OAAO,KAAK;AAClB,YAAM,OAAO,kBAAkB,kBAAkB,SAAS,CAAC;AAC3D,YAAM,IAAI,mBAAmB,KAAK;AAClC,sBAAgB;AAChB,YAAM,OAAO;AACb,YAAM,IAAI;AAAA,QACR,MAAM;AAAA,QACN,MAAM,WAAW;AAAA,MACnB;AACA,YAAM,IAAI,IAAI;AACd,aAAO,EAAE,QAAQ,MAAM,YAAY,IAAI;AAAA,IACzC;AAGA,UAAM,iBAAiB,OAAO,QAAQ,eAClC,OAAO,QAAQ,aAAa,IAC5B;AAEJ,QAAI,gBAAgB;AAClB,YAAM,sBAAsB,cAAc;AAAA,IAC5C;AAEA,UAAM,IAAI,mBAAmB,KAAK;AAElC,QAAI,CAAC,OAAO,QAAQ,cAAc;AAGhC,YAAM,OAAO,kBAAkB,OAAO,QAAQ,KAAK,OAAO,SAAS,CAAC;AAAA,IACtE;AAEA,oBAAgB;AAChB,UAAM,OAAO;AACb,UAAM,IAAI,UAAU,KAAK,MAAM,WAAW,CAAwB;AAElE,QAAI,OAAO,QAAQ,cAAc;AAG/B,gBAAM,0BAAS,gBAAiC,MAAM,GAAG;AAAA,IAC3D,OAAO;AAEL,YAAM,IAAI,IAAI,OAAO,QAAQ,IAAI;AAAA,IACnC;AAEA,WAAO,EAAE,QAAQ,MAAM,YAAY,IAAI;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsCO,cAAc,WAGZ;AACP,QAAI,UAAU,mBAAmB,QAAW;AAC1C,WAAK,iBAAiB,KAAK;AAAA,QACzB,UAAU;AAAA,MACZ;AAAA,IACF;AAEA,QAAI,UAAU,cAAc,QAAW;AACrC,WAAK,YAAY,KAAK,mBAAmB,UAAU,SAAS;AAAA,IAC9D;AAKA,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2BO,eAAe,QAAsB;AAC1C,SAAK,UAAU,OAAO,MAAM;AAC5B,SAAK,aAAa,OAAO,MAAM;AAC/B,SAAK,UAAU,OAAO,MAAM;AAC5B,SAAK,6BAA6B,MAAM;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA,EAKO,cAAoB;AACzB,SAAK,UAAU,MAAM;AACrB,SAAK,aAAa,MAAM;AACxB,SAAK,uBAAuB,MAAM;AAClC,SAAK,UAAU,MAAM;AACrB,SAAK,uBAAuB,MAAM;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKO,gBAAgB;AACrB,WAAO;AAAA,MACL,MAAM;AAAA,QACJ,OAAO,KAAK,UAAU;AAAA,QACtB,UAAU,KAAK,UAAU;AAAA,MAC3B;AAAA,MACA,SAAS;AAAA,QACP,OAAO,KAAK,aAAa;AAAA,QACzB,UAAU,KAAK,aAAa;AAAA,MAC9B;AAAA,MACA,oBAAoB;AAAA,QAClB,OAAO,KAAK,uBAAuB;AAAA,QACnC,UAAU,KAAK,uBAAuB;AAAA,MACxC;AAAA,MACA,MAAM;AAAA,QACJ,OAAO,KAAK,UAAU;AAAA,QACtB,UAAU,KAAK,UAAU;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsDO,aAAa,WAGX;AAEP,QAAI,UAAU,mBAAmB,QAAW;AAC1C,YAAM,SAAS,KAAK,wBAAwB,UAAU,cAAc;AAGpE,YAAM,oBAAoB,oBAAI,IAAY;AAK1C,iBAAW,CAAC,KAAK,SAAS,KAAK,KAAK,eAAe,QAAQ,GAAG;AAC5D,cAAM,YAAY,OAAO,IAAI,GAAG;AAGhC,YAAI,cAAc,UAAa,cAAc,WAAW;AACtD,4BAAkB,IAAI,SAAS;AAAA,QACjC;AAAA,MACF;AAOA,iBAAW,CAAC,KAAK,SAAS,KAAK,OAAO,QAAQ,GAAG;AAC/C,cAAM,YAAY,KAAK,eAAe,IAAI,GAAG;AAG7C,YAAI,cAAc,UAAa,cAAc,WAAW;AACtD,4BAAkB,IAAI,SAAS;AAAA,QACjC;AAAA,MACF;AAGA,WAAK,iBAAiB;AAGtB,iBAAW,UAAU,mBAAmB;AACtC,aAAK,UAAU,OAAO,MAAM;AAC5B,aAAK,aAAa,OAAO,MAAM;AAC/B,aAAK,UAAU,OAAO,MAAM;AAC5B,aAAK,6BAA6B,MAAM;AAAA,MAC1C;AAAA,IACF;AAGA,QAAI,UAAU,cAAc,QAAW;AACrC,YAAM,eAAe,KAAK,mBAAmB,UAAU,SAAS;AAIhE,UAAI,sBAAsB;AAG1B,UAAI,KAAK,UAAU,SAAS,aAAa,MAAM;AAC7C,8BAAsB;AAAA,MACxB,OAAO;AAEL,mBAAW,CAAC,QAAQ,MAAM,KAAK,aAAa,QAAQ,GAAG;AACrD,gBAAM,YAAY,KAAK,UAAU,IAAI,MAAM;AAE3C,cAAI,CAAC,aAAa,CAAC,KAAK,mBAAmB,WAAW,MAAM,GAAG;AAC7D,kCAAsB;AACtB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,WAAK,YAAY;AAIjB,UAAI,qBAAqB;AACvB,aAAK,YAAY;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAa,cACX,QACA,KACA,OAC0B;AAE1B,UAAM,aAAa,OAAO,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC;AACpD,UAAM,MAAM,WAAW,WAAW,GAAG,IAAI,aAAa,MAAM;AAG5D,QAAI,IAAI,SAAS,IAAI,GAAG;AACtB,aAAO,EAAE,QAAQ,OAAO,QAAQ,YAAY;AAAA,IAC9C;AAEA,QAAI,WAAW;AACf,QAAI,wBAAwB;AAG5B,QAAI,KAAK,eAAe,IAAI,GAAG,GAAG;AAChC,iBAAW,KAAK,eAAe,IAAI,GAAG;AAAA,IACxC,OAEK;AACH,YAAM,SAAS,MAAM,KAAK,KAAK,UAAU,KAAK,CAAC,EAAE;AAAA,QAAK,CAAC,WACrD,IAAI,WAAW,MAAM;AAAA,MACvB;AAEA,UAAI,QAAQ;AAEV,cAAM,eAAe,KAAK,UAAU,IAAI,MAAM;AAE9C,YAAI,cAAc;AAEhB,gBAAM,eAAe,IAAI,MAAM,OAAO,MAAM;AAG5C,gBAAM,mBAAmB,aAAa,WAAW,GAAG,IAChD,aAAa,MAAM,CAAC,IACpB;AAGJ,cACE,CAAC,iBAAiB,SAAS,KAAK,KAChC,CAAC,iBAAiB,SAAS,MAAM,GACjC;AACA,uBAAW,YAAAC,QAAK,KAAK,aAAa,MAAM,gBAAgB;AACxD,oCAAwB,aAAa,yBAAyB;AAAA,UAChE;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAIA,QAAI,UAAU;AACZ,aAAO,KAAK,UAAU,KAAK,OAAO,UAAU,EAAE,sBAAsB,CAAC;AAAA,IACvE;AAEA,WAAO,EAAE,QAAQ,OAAO,QAAQ,YAAY;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,wBACN,gBACqB;AACrB,UAAM,aAAa,oBAAI,IAAoB;AAE3C,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,cAAc,GAAG;AAEzD,UAAI,IAAI,SAAS,IAAI,KAAK,MAAM,SAAS,IAAI,GAAG;AAC9C,YAAI,KAAK,QAAQ;AACf,eAAK,OAAO;AAAA,YACV,EAAE,KAAK,MAAM;AAAA,YACb;AAAA,UACF;AAAA,QACF;AAEA;AAAA,MACF;AAEA,YAAM,gBAAgB,IAAI,WAAW,GAAG,IAAI,MAAM,MAAM;AACxD,iBAAW,IAAI,eAAe,KAAK;AAAA,IACrC;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUQ,mBACN,WAC2B;AAC3B,UAAM,aAAa,oBAAI,IAA0B;AAEjD,eAAW,CAAC,QAAQ,MAAM,KAAK,OAAO,QAAQ,SAAS,GAAG;AACxD,YAAM,mBAAmB,KAAK,gBAAgB,MAAM;AAGpD,YAAM,aAAa,OAAO,WAAW,WAAW,SAAS,OAAO;AAChE,UAAI,OAAO,SAAS,IAAI,KAAK,WAAW,SAAS,IAAI,GAAG;AACtD,YAAI,KAAK,QAAQ;AACf,eAAK,OAAO;AAAA,YACV,EAAE,QAAQ,WAAW;AAAA,YACrB;AAAA,UACF;AAAA,QACF;AAEA;AAAA,MACF;AAGA,UAAI,OAAO,WAAW,UAAU;AAC9B,mBAAW,IAAI,kBAAkB;AAAA,UAC/B,MAAM;AAAA,UACN,uBAAuB;AAAA,QACzB,CAAC;AAAA,MACH,OAAO;AAEL,mBAAW,IAAI,kBAAkB;AAAA,UAC/B,MAAM,OAAO;AAAA,UACb,uBAAuB,OAAO,yBAAyB;AAAA,QACzD,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,gBAAgB,QAAwB;AAC9C,QAAI,IAAI,UAAU;AAGlB,QAAI,EAAE,QAAQ,QAAQ,GAAG;AAEzB,QAAI,CAAC,EAAE,WAAW,GAAG,GAAG;AACtB,UAAI,MAAM;AAAA,IACZ;AAEA,QAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACpB,UAAI,IAAI;AAAA,IACV;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAY,UAA0B;AAE5C,UAAM,MAAM,YAAAA,QAAK,QAAQ,QAAQ,EAAE,YAAY,EAAE,QAAQ,OAAO,EAAE;AAGlE,UAAM,YAAoC;AAAA,MACxC,KAAK;AAAA,MACL,KAAK;AAAA,MACL,MAAM;AAAA,MACN,KAAK;AAAA,MACL,MAAM;AAAA,MACN,KAAK;AAAA,MACL,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAEA,WAAO,UAAU,GAAG,KAAK;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,mBAAmB,GAAiB,GAA0B;AACpE,UAAM,QAAQ,OAAO,KAAK,CAAC;AAC3B,UAAM,QAAQ,OAAO,KAAK,CAAC;AAG3B,QAAI,MAAM,WAAW,MAAM,QAAQ;AACjC,aAAO;AAAA,IACT;AAGA,WAAO,MAAM,MAAM,CAAC,QAAQ,EAAE,GAAG,MAAM,EAAE,GAAG,CAAC;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcQ,iBAAiB,UAA2B;AAClD,UAAM,eAAe,YAAAA,QAAK,SAAS,QAAQ;AAK3C,WACE,sBAAsB,KAAK,YAAY,KACvC,qBAAqB,KAAK,YAAY;AAAA,EAE1C;AAAA,EAEQ,sBACN,cACA,MACA,UACQ;AACR,WAAO,GAAG,YAAY,KAAK,IAAI,KAAK,QAAQ;AAAA,EAC9C;AAAA,EAEQ,6BAA6B,QAAsB;AAIzD,UAAM,OAAO,KAAK,uBAAuB,IAAI,MAAM;AAEnD,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AAEA,eAAW,OAAO,MAAM;AAItB,WAAK,uBAAuB,IAAI,KAAK,EAAE,MAAM,YAAY,GAAG,IAAI,GAAI;AAAA,IACtE;AAEA,SAAK,uBAAuB,OAAO,MAAM;AAAA,EAC3C;AAAA,EAEQ,mCACN,OACM;AACN,QACE,MAAM,WAAW,WACjB,MAAM,WAAW,aACjB,MAAM,WAAW,YACjB,MAAM,WAAW,SACjB;AACA;AAAA,IACF;AAIA,SAAK,8BAA8B,MAAM,GAAG;AAAA,EAC9C;AAAA,EAEQ,8BAA8B,UAAwB;AAG5D,UAAM,WAAW,SAAS,MAAM,IAAI;AACpC,UAAM,SAAS,SAAS,MAAM,GAAG,EAAE,EAAE,KAAK,IAAI;AAE9C,SAAK,yBAAyB,QAAQ,QAAQ;AAAA,EAChD;AAAA,EAEQ,yBAAyB,QAAgB,UAAwB;AAIvE,UAAM,WAAW,KAAK,uBAAuB,IAAI,MAAM;AAEvD,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AAEA,aAAS,OAAO,QAAQ;AAGxB,QAAI,SAAS,SAAS,GAAG;AACvB,WAAK,uBAAuB,OAAO,MAAM;AAAA,IAC3C;AAAA,EACF;AACF;;;AG1hDO,SAAS,WAAW,KAAqB;AAC9C,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAcO,SAAS,eAAe,KAAqB;AAClD,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM;AACzB;;;ACnBO,IAAM,+BAA+B;AAmB5C,SAAS,kBAAkB,SAAsC;AAC/D,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,EACT;AAEA,QAAM,CAAC,SAAS,IAAI,QAAQ,MAAM,GAAG;AACrC,QAAM,QAAQ,OAAO,SAAS,WAAW,EAAE;AAE3C,SAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAC1C;AAOO,SAAS,sBACd,mBAAmB,8BACnB,cAAsC,YAClB;AACpB,QAAM,WAAW,YAAY,SAAS;AACtC,QAAM,cACJ,OAAO,UAAU,SAAS,WAAW,SAAS,OAAO;AACvD,QAAM,aACJ,OAAO,UAAU,QAAQ,WAAW,SAAS,MAAM;AACrD,QAAM,QACJ,OAAO,YAAY,QAAQ,eAAe,OAAO,eAAe;AAElE,MAAI,OAAO;AACT,WAAO;AAAA,MACL,SAAS;AAAA,MACT,aAAa;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,aAAa;AAChB,WAAO;AAAA,MACL,SAAS;AAAA,MACT,aAAa;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,kBAAkB,WAAW;AAE/C,SAAO;AAAA,IACL,SAAS;AAAA,IACT,aAAa,OAAO,cAAc,YAAY,aAAa;AAAA,IAC3D;AAAA,IACA;AAAA,EACF;AACF;AAKO,SAAS,mBACd,mBAAmB,8BACnB,aACS;AACT,SAAO,sBAAsB,kBAAkB,WAAW,EAAE;AAC9D;AAMO,SAAS,uBACd,mBAAmB,8BACnB,aACM;AACN,QAAM,cAAc,sBAAsB,kBAAkB,WAAW;AAEvE,MAAI,YAAY,aAAa;AAC3B;AAAA,EACF;AAEA,QAAM,kBAAkB,YAAY,eAAe;AAEnD,QAAM,IAAI;AAAA,IACR,4BAA4B,gBAAgB,qBAAqB,YAAY,OAAO,8BAA8B,eAAe;AAAA,EACnI;AACF;","names":["zlibConstants","fs","crypto","options","path"]}