{"version":3,"sources":["../src/variables.ts","../src/functions.ts","../src/schema.ts","../src/renders.ts","../src/pixel.ts"],"names":["moduleDir","path","fileURLToPath","getAssetPath","filename","NOT_FOUND_IMAGE","NOT_FOUND_AVATAR","FALLBACKIMAGES","readFile","API_REGEX","allowedFormats","mimeTypes","safeOnError","hook","err","phase","src","MAX_SPECIFIED_PATH_LEN","isValidPath","basePath","specifiedPath","resolvedBase","resolvedPath","realBase","realPath","normalizedBase","isInside","relative","expandIPv6Hextets","ip","body","lastColon","tail","isIP","octets","hi","lo","halves","parseGroup","s","out","h","hextets","only","left","right","missing","isPrivateIp","family","parts","p","a","b","h0","h1","h2","h3","h4","h5","h6","h7","embeddedV4","highBitsZero","isPublicHost","hostname","stripped","addresses","resolvePinnedAddresses","resolvePinnedAddress","buildPinnedLookup","_hostname","options","callback","first","buildPinnedAgents","addressOrList","lookup","hostMatchesEntry","host","entry","suffix","isHostAllowed","allowedNetworkList","requestNoRedirect","timeoutMs","maxBytes","agents","axios","status","aerr","fetchFromNetwork","type","maxRedirects","onError","onFallback","fallback","currentUrl","hop","parsed","pinned","response","location","contentType","allowedMimeTypes","readLocalImage","filePath","baseDir","resolvedFile","stats","stripApiPrefix","pathname","apiRegex","apiPrefix","normalizeWebsiteHost","websiteURL","resolveInternalLocalPath","url","configuredHost","fetchImage","internalLocalPath","imageFormatEnum","z","imageTypeEnum","userDataSchema","value","ctx","got","val","lower","optionsSchema","arr","data","renderOptions","renderUserData","userData","bounds","clamp","min","max","reportError","context","safeOnComplete","elapsedMs","start","diff","whole","remainder","FILENAME_MAX_LEN","ASCII_FALLBACK_DEFAULT","DEFAULT_CACHE_CONTROL","FALLBACK_CACHE_CONTROL","buildFilename","rawSrc","outputFormat","ext","baseWithExt","baseNoExt","asciiBase","safeAsciiBase","maxBase","asciiFilename","utfBase","truncatedEncoded","c","lastPercent","byte","encodedFilename","buildSourceIdentifier","statLocalFile","localPath","resolved","C","buildDeterministicEtag","fields","sourceIdentifier","key","createHash","raceWithTimeout","promise","ms","label","timer","_","reject","isInsideRoot","rootDir","candidate","preResolvedRoot","realRoot","lexicalCandidate","realCandidate","resolveRootDir","DOCTYPE_SVG_ROOT","skipXmlProlog","i","skipWs","end","headLooksLikeSvg","head","looksLikeSvg","buf","isLe","sliceEnd","head16","beSrc","swapped","trimmed","serveImage","req","res","next","parsedOptions","cachedRealRoot","startedAt","requestedType","onComplete","observedSrc","observedUserId","parsedUserId","rawUserId","idTimeoutMs","handlerResult","folderPromise","dir","etag","servedSoftFallback","markSoftFallback","imageBuffer","processedImage","image","sharp","meta","resizeOptions","flushedError","fallbackType","fallbackFormat","fallbackError","registerServe","cacheResolved","pendingResolution","ensureCachedRealRoot","rootForRequest","pixel_default"],"mappings":"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","file":"index.mjs","sourcesContent":["import type { ImageFormat } from \"./types\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Get the directory path for the current module.\n * Uses import.meta.url for ESM (tsup provides shims for CJS compatibility).\n */\nconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\n\nconst getAssetPath = (filename: string): string => {\n  return path.join(moduleDir, \"assets\", filename);\n};\n\nconst NOT_FOUND_IMAGE = getAssetPath(\"noimage.jpg\");\nconst NOT_FOUND_AVATAR = getAssetPath(\"noavatar.png\");\n\nexport const FALLBACKIMAGES: Record<\n  \"normal\" | \"avatar\",\n  () => Promise<Buffer>\n> = {\n  normal: async (): Promise<Buffer> => readFile(NOT_FOUND_IMAGE),\n  avatar: async (): Promise<Buffer> => readFile(NOT_FOUND_AVATAR),\n};\n\nexport const API_REGEX: RegExp = /^\\/api\\/v1\\//;\n\nexport const allowedFormats: ImageFormat[] = [\n  \"jpeg\",\n  \"jpg\",\n  \"png\",\n  \"webp\",\n  \"gif\",\n  \"tiff\",\n  \"avif\",\n];\n\nexport const mimeTypes: Readonly<Record<string, string>> = {\n  jpeg: \"image/jpeg\",\n  jpg: \"image/jpeg\",\n  png: \"image/png\",\n  webp: \"image/webp\",\n  gif: \"image/gif\",\n  tiff: \"image/tiff\",\n  avif: \"image/avif\",\n};\n","import path from \"node:path\";\nimport * as fs from \"node:fs/promises\";\nimport * as dns from \"node:dns/promises\";\nimport * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport { isIP, type LookupFunction } from \"node:net\";\nimport axios, { AxiosError, AxiosResponse } from \"axios\";\nimport { FALLBACKIMAGES, mimeTypes } from \"./variables\";\nimport type { ImageType, PixelServeOnError } from \"./types\";\n\n/**\n * Internal helper that fires the user-supplied `onError` hook without ever\n * propagating a hook error back into the request pipeline. Mirrors the\n * dispatcher in `pixel.ts`; duplicated to avoid a circular import.\n */\nconst safeOnError = (\n  hook: PixelServeOnError | undefined,\n  err: unknown,\n  phase: string,\n  src?: string,\n): void => {\n  if (!hook) return;\n  try {\n    hook(err, { phase, src });\n  } catch {\n    // intentionally suppressed\n  }\n};\n\n/**\n * @typedef {(\"avatar\" | \"normal\")} ImageType\n * @description Defines the type of image being processed.\n */\n\n/**\n * Maximum length accepted for `specifiedPath`. Both Windows (260 by default,\n * 32767 with `\\\\?\\` LFN prefix) and POSIX (`PATH_MAX` is typically 4096) cap\n * absolute path lengths, but the input here is a relative segment joined to\n * `basePath`, so we cap defensively below the POSIX limit to avoid pathological\n * inputs forcing megabyte string allocations through `path.resolve`.\n */\nconst MAX_SPECIFIED_PATH_LEN = 4096;\n\n/**\n * Checks if a specified path is valid within a base path.\n *\n * Performs shape validation first (no null bytes, no control characters\n * including `DEL`/`\\x7F`, no backslashes on any platform, no absolute paths,\n * length cap), then resolves both `basePath` and the joined path via\n * `fs.realpath`, then asserts containment via `path.relative` plus a\n * prefix check.\n *\n * Cross-platform notes:\n *\n *   - Backslashes are rejected on **all** platforms (not just Windows). On\n *     POSIX, literal `\\\\` is a valid filename byte; on Windows it is a\n *     directory separator. Allowing the divergence silently is a security\n *     smell, so this guard rejects backslash universally to keep behavior\n *     consistent.\n *   - UNC paths (`\\\\server\\share\\...`) are caught by both the backslash\n *     check and `path.isAbsolute` on Windows.\n *   - `\\x7F` (DEL) is part of the control-character regex so request paths\n *     containing it are rejected (the `Content-Disposition` sanitizer in\n *     `pixel.ts` also strips `\\x7F`; keeping the two consistent here matters).\n *\n * TOCTOU caveat: this function calls `fs.realpath` to validate containment,\n * but `pixel.ts` later re-resolves the path and calls `fs.readFile`\n * independently. Between those two calls the filesystem could change (for\n * example, a symlink target could be swapped). For an image-serving pipeline\n * the resulting worst case is a fallback image being returned; for higher\n * security workloads, callers should mount images on a read-only filesystem\n * or run the process with `fs.open` + atime-checked file handles. This is\n * accepted risk for the default deployment model.\n *\n * @param {string} basePath - The base directory to resolve paths.\n * @param {string} specifiedPath - The path to check.\n * @returns {Promise<boolean>} True if the path is valid, false otherwise.\n */\nexport const isValidPath = async (\n  basePath: string,\n  specifiedPath: string,\n): Promise<boolean> => {\n  try {\n    if (!basePath || !specifiedPath) return false;\n    if (typeof specifiedPath !== \"string\") return false;\n    if (specifiedPath.length > MAX_SPECIFIED_PATH_LEN) return false;\n    if (specifiedPath.includes(\"\\0\")) return false;\n    // Reject backslash on ALL platforms. On POSIX this is technically a\n    // legal filename byte, but on Windows it is a path separator that can\n    // be used for traversal. Rejecting unconditionally keeps cross-platform\n    // behavior identical and removes a silent divergence.\n    if (specifiedPath.includes(\"\\\\\")) return false;\n    if (path.isAbsolute(specifiedPath)) return false;\n    // Reject every control character (`\\x00`–`\\x1F`) and `\\x7F` (DEL).\n    // eslint-disable-next-line no-control-regex\n    if (!/^[^\\x00-\\x1F\\x7F]+$/.test(specifiedPath)) return false;\n\n    const resolvedBase = path.resolve(basePath);\n    const resolvedPath = path.resolve(resolvedBase, specifiedPath);\n\n    const [realBase, realPath] = await Promise.all([\n      fs.realpath(resolvedBase),\n      fs.realpath(resolvedPath),\n    ]);\n\n    const baseStats = await fs.stat(realBase);\n    if (!baseStats.isDirectory()) return false;\n\n    // The resolved path must be a regular file (or symlink to one), not a\n    // directory. Image serving never returns directory listings, and\n    // rejecting directories prevents callers from accidentally short-\n    // circuiting fallback logic when an attacker references the root.\n    const pathStats = await fs.stat(realPath);\n    if (!pathStats.isFile()) return false;\n\n    const normalizedBase = realBase + path.sep;\n    const normalizedPath = realPath + path.sep;\n\n    const isInside =\n      normalizedPath.startsWith(normalizedBase) || realPath === realBase;\n\n    const relative = path.relative(realBase, realPath);\n    return !relative.startsWith(\"..\") && !path.isAbsolute(relative) && isInside;\n  } catch {\n    return false;\n  }\n};\n\n/**\n * A fully-expanded IPv6 address: its 8 constituent 16-bit hextets, in order.\n */\ntype Hextets = [number, number, number, number, number, number, number, number];\n\n/**\n * Expands any syntactically valid IPv6 literal (already confirmed via\n * `isIP(ip) === 6`) into its 8 constituent hextets. Handles `::` zero-run\n * compression at any position and the optional trailing IPv4 dotted-quad\n * tail (RFC 4291 §2.2 item 3, e.g. `\"64:ff9b::192.0.2.1\"`).\n *\n * Used so NAT64 prefix detection below is a numeric comparison rather than\n * a `startsWith(\"64:ff9b::\")` string match, which would miss a fully\n * expanded or partially compressed equivalent representing the exact same\n * address — a real bypass vector for an attacker-supplied IP literal that\n * never goes through DNS. Returns `null` if the (already-validated) string\n * does not decompose into exactly 8 hextets, which should not happen in\n * practice given the `isIP` precondition.\n */\nconst expandIPv6Hextets = (ip: string): Hextets | null => {\n  let body = ip;\n  const lastColon = body.lastIndexOf(\":\");\n  const tail = lastColon >= 0 ? body.slice(lastColon + 1) : body;\n  if (isIP(tail) === 4) {\n    const octets = tail.split(\".\").map(Number);\n    const hi = ((octets[0]! << 8) | octets[1]!).toString(16);\n    const lo = ((octets[2]! << 8) | octets[3]!).toString(16);\n    body = body.slice(0, lastColon + 1) + hi + \":\" + lo;\n  }\n\n  const halves = body.split(\"::\");\n  if (halves.length > 2) return null;\n\n  // Parse a colon-separated run of hextets, failing closed (returning null) on\n  // any group that is not a clean 1-4 digit hex value. Without this validation\n  // `parseInt` would silently truncate a malformed group — e.g. an RFC 4007\n  // zone-id suffix leaves the tail \"127.0.0.1%eth0\", which `parseInt(_, 16)`\n  // collapses to `0x127` — producing a plausible-but-wrong tuple that shifts\n  // the classifier's markers out of position and could report a private\n  // address as public. `net.isIP` accepts a zone-id'd literal as valid IPv6,\n  // so this parser must reject anything that is not a bare address explicitly.\n  const parseGroup = (s: string): number[] | null => {\n    if (s === \"\") return [];\n    const out: number[] = [];\n    for (const h of s.split(\":\")) {\n      if (!/^[0-9a-f]{1,4}$/i.test(h)) return null;\n      out.push(parseInt(h, 16));\n    }\n    return out;\n  };\n\n  let hextets: number[];\n  if (halves.length === 1) {\n    const only = parseGroup(halves[0]!);\n    if (only === null) return null;\n    hextets = only;\n  } else {\n    const left = parseGroup(halves[0]!);\n    const right = parseGroup(halves[1]!);\n    if (left === null || right === null) return null;\n    const missing = 8 - left.length - right.length;\n    if (missing < 0) return null;\n    hextets = [...left, ...Array<number>(missing).fill(0), ...right];\n  }\n\n  return hextets.length === 8 ? (hextets as Hextets) : null;\n};\n\n/**\n * Determines if an IP address (v4 or v6) is private, loopback, link-local,\n * unique-local, multicast, broadcast, or otherwise unsafe to issue requests to.\n *\n * @param {string} ip - The IP address to check.\n * @returns {boolean} True if the IP is considered private/internal.\n */\nexport const isPrivateIp = (ip: string): boolean => {\n  const family = isIP(ip);\n  if (family === 0) return true; // not a valid IP — treat as unsafe\n\n  if (family === 4) {\n    const parts = ip.split(\".\").map((p) => Number(p));\n    if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) return true;\n    const [a, b] = parts;\n    // 0.0.0.0/8\n    if (a === 0) return true;\n    // 10.0.0.0/8\n    if (a === 10) return true;\n    // 100.64.0.0/10 RFC 6598 shared address space (CGNAT, cloud-internal)\n    if (a === 100 && b >= 64 && b <= 127) return true;\n    // 127.0.0.0/8 (loopback)\n    if (a === 127) return true;\n    // 169.254.0.0/16 (link-local, AWS IMDS)\n    if (a === 169 && b === 254) return true;\n    // 172.16.0.0/12\n    if (a === 172 && b >= 16 && b <= 31) return true;\n    // 192.168.0.0/16\n    if (a === 192 && b === 168) return true;\n    // 192.0.0.0/24 (IETF Protocol Assignments) and 192.0.2.0/24 (TEST-NET-1)\n    if (a === 192 && b === 0) return true;\n    // 192.88.99.0/24 RFC 3068 6to4 anycast relay (deprecated)\n    if (a === 192 && b === 88 && parts[2] === 99) return true;\n    // 198.18.0.0/15 (benchmarking)\n    if (a === 198 && (b === 18 || b === 19)) return true;\n    // 198.51.100.0/24 (TEST-NET-2)\n    if (a === 198 && b === 51 && parts[2] === 100) return true;\n    // 203.0.113.0/24 (TEST-NET-3)\n    if (a === 203 && b === 0 && parts[2] === 113) return true;\n    // 224.0.0.0/4 (multicast)\n    if (a >= 224 && a <= 239) return true;\n    // 240.0.0.0/4 (reserved / 255.255.255.255 broadcast)\n    if (a >= 240) return true;\n    return false;\n  }\n\n  // IPv6 — classify from the fully-expanded numeric hextets rather than from\n  // textual prefixes. Textual matching was unsound in BOTH directions: in the\n  // \"allow\" direction the loopback ::1 written uncompressed (\"0:0:0:0:0:0:0:1\"),\n  // the unspecified :: written uncompressed, and an uncompressed IPv4-mapped\n  // address (\"0:0:0:0:0:ffff:7f00:1\") all slipped past the exact-string /\n  // `startsWith` checks and were wrongly treated as public — an SSRF bypass in\n  // the exported `isPrivateIp` guard and in the DNS-validation path, since a\n  // resolver or a caller can legitimately hand us an uncompressed literal; in\n  // the \"block\" direction \"fe8:…\" (numeric 0x0fe8 — unrelated reserved space,\n  // not link-local) matched the old fe80::/10 regex. Expanding to the 8 numeric\n  // hextets first (via `expandIPv6Hextets`, which also folds a trailing\n  // dotted-quad IPv4 tail) makes every textual representation of the same\n  // address classify identically. `isIP(ip) === 6` guarantees a parse; fail\n  // closed (treat as unsafe) on the defensive `null` branch.\n  const hextets = expandIPv6Hextets(ip.toLowerCase());\n  if (!hextets) return true;\n  const [h0, h1, h2, h3, h4, h5, h6, h7] = hextets;\n  const embeddedV4 = (hi: number, lo: number): string =>\n    `${hi >> 8}.${hi & 0xff}.${lo >> 8}.${lo & 0xff}`;\n  const highBitsZero = h0 === 0 && h1 === 0 && h2 === 0 && h3 === 0 && h4 === 0;\n\n  // ::/96 low block — the unspecified address (::), loopback (::1), and the\n  // deprecated IPv4-compatible ::a.b.c.d form (RFC 4291 §2.5.5.1, \"MUST NOT be\n  // assigned to any node\"). Classify by the embedded low-32-bit IPv4: :: maps\n  // to 0.0.0.0 and ::1 to 0.0.0.1 (both in the blocked 0.0.0.0/8 range), and an\n  // embedded private/loopback/link-local v4 (e.g. ::127.0.0.1, ::10.0.0.1,\n  // ::169.254.169.254) is blocked, while an embedded — deprecated, non-routable\n  // — public v4 passes through, mirroring the ::ffff: and NAT64 /96 branches.\n  if (highBitsZero && h5 === 0) return isPrivateIp(embeddedV4(h6, h7));\n  // IPv4-mapped ::ffff:a.b.c.d (RFC 4291) — recurse on the embedded IPv4 so a\n  // mapped private/loopback v4 is blocked while a mapped public v4 passes.\n  if (highBitsZero && h5 === 0xffff) return isPrivateIp(embeddedV4(h6, h7));\n  // NAT64 (RFC 6052 / RFC 8215) — addresses that embed an IPv4 address.\n  if (h0 === 0x64 && h1 === 0xff9b) {\n    if (h2 === 0 && h3 === 0 && h4 === 0 && h5 === 0) {\n      // 64:ff9b::/96 (RFC 6052 Well-Known Prefix) — the embedded IPv4\n      // occupies the low 32 bits contiguously. Recurse so a NAT64-wrapped\n      // private v4 is blocked while a NAT64-wrapped public v4 passes.\n      return isPrivateIp(embeddedV4(h6, h7));\n    }\n    if (h2 === 1) {\n      // 64:ff9b:1::/48 (RFC 8215 Local-Use Prefix) — intentionally NOT\n      // unwrapped. RFC 8215 explicitly exempts this prefix from the\n      // Well-Known Prefix's \"embedded address must be public\" restriction\n      // (it may legitimately carry private IPv4 addresses) and it exists\n      // only for an operator's own limited/local NAT64 domain — a\n      // general-purpose fetch middleware has no legitimate reason to ever\n      // see it in the wild. Its embedded IPv4 is also split\n      // non-contiguously around a reserved zero octet at different bit\n      // offsets than the /96 form (RFC 6052 §2.2), so a hand-rolled\n      // extraction here would be new, untested, security-critical\n      // bit-splicing logic. Two independent judge reviews converged on\n      // blocking the whole range rather than risk a subtly wrong decode\n      // silently opening an SSRF bypass.\n      return true;\n    }\n  }\n  // 6to4 2002::/16 (RFC 3056) — embeds a 32-bit IPv4 address in h1:h2\n  // (`2002:<hi>:<lo>::/48`). Unlike the NAT64 well-known prefix above, 6to4\n  // legitimately tunnels arbitrary *public* IPv4 traffic, so — mirroring the\n  // NAT64 /96 handling — unwrap the embedded address and recurse rather than\n  // blocking the whole range outright: a 6to4-wrapped private/loopback v4\n  // (e.g. 2002:a00:1:: embedding 10.0.0.1) is blocked, while a 6to4-wrapped\n  // public v4 (e.g. 2002:808:808:: embedding 8.8.8.8) still passes.\n  if (h0 === 0x2002) return isPrivateIp(embeddedV4(h1, h2));\n  // link-local fe80::/10 (0xfe80–0xfebf)\n  if (h0 >= 0xfe80 && h0 <= 0xfebf) return true;\n  // deprecated site-local fec0::/10 (0xfec0–0xfeff, RFC 3879)\n  if (h0 >= 0xfec0 && h0 <= 0xfeff) return true;\n  // unique-local fc00::/7 (0xfc00–0xfdff)\n  if (h0 >= 0xfc00 && h0 <= 0xfdff) return true;\n  // multicast ff00::/8 (0xff00–0xffff)\n  if (h0 >= 0xff00 && h0 <= 0xffff) return true;\n  return false;\n};\n\n/**\n * Resolves a hostname via DNS and verifies every returned address is a public\n * (non-private/loopback/link-local) IP. If the hostname is already an IP\n * literal, validates that directly without a DNS lookup.\n *\n * @param {string} hostname - The hostname to validate.\n * @returns {Promise<boolean>} True if the hostname only resolves to public IPs.\n */\nexport const isPublicHost = async (hostname: string): Promise<boolean> => {\n  if (!hostname) return false;\n  // strip brackets that URL.hostname leaves around IPv6 literals\n  const stripped = hostname.replace(/^\\[|\\]$/g, \"\");\n  if (isIP(stripped) !== 0) return !isPrivateIp(stripped);\n\n  try {\n    const addresses = await dns.lookup(stripped, { all: true, verbatim: true });\n    if (!addresses.length) return false;\n    return addresses.every((a) => !isPrivateIp(a.address));\n  } catch {\n    return false;\n  }\n};\n\n/** A validated, connectable address pinned into an agent's `lookup`. */\ntype PinnedAddress = { address: string; family: 4 | 6 };\n\n/**\n * Resolves a hostname once, validates that EVERY returned address is public,\n * and returns the full list of `{ address, family }` pairs. The list is then\n * pinned into an `http.Agent`/`https.Agent`'s `lookup` function so the TCP\n * socket can only connect to an address we validated — closing the DNS-\n * rebinding window between the validation lookup and axios' subsequent\n * resolve. Returning ALL validated addresses (rather than just the first)\n * lets Node's Happy-Eyeballs (`autoSelectFamily`, on by default since Node\n * 20) fail over across families/addresses, so a host whose first-returned\n * address is an unreachable IPv6 no longer times out the whole fetch. For IP\n * literals the input is returned verbatim (still subject to `isPrivateIp`)\n * and no DNS lookup is performed.\n *\n * Returns `null` when the host is empty, resolves to no addresses, resolves\n * to (or is) a private/loopback/link-local IP (conservatively: if ANY\n * resolved address is private the whole host is rejected), or DNS resolution\n * fails. Callers fall back to the regular failure path on `null`.\n */\nexport const resolvePinnedAddresses = async (\n  hostname: string,\n): Promise<PinnedAddress[] | null> => {\n  if (!hostname) return null;\n  const stripped = hostname.replace(/^\\[|\\]$/g, \"\");\n  if (isIP(stripped) !== 0) {\n    if (isPrivateIp(stripped)) return null;\n    const family = isIP(stripped) === 6 ? 6 : 4;\n    return [{ address: stripped, family }];\n  }\n  try {\n    const addresses = await dns.lookup(stripped, { all: true, verbatim: true });\n    if (!addresses.length) return null;\n    if (addresses.some((a) => isPrivateIp(a.address))) return null;\n    return addresses.map((a) => ({\n      address: a.address,\n      family: a.family === 6 ? 6 : 4,\n    }));\n  } catch {\n    return null;\n  }\n};\n\n/**\n * Backward-compatible single-address resolver: returns the FIRST validated\n * `{ address, family }` pair (or `null`). Retained because it is part of the\n * package's exported surface; prefer `resolvePinnedAddresses` internally so\n * Happy-Eyeballs can fail over across every validated address.\n */\nexport const resolvePinnedAddress = async (\n  hostname: string,\n): Promise<PinnedAddress | null> => {\n  const addresses = await resolvePinnedAddresses(hostname);\n  return addresses ? addresses[0]! : null;\n};\n\n/**\n * Internal alias for Node's own `lookup` callback shape\n * (`net.LookupFunction`), used by `http.Agent`/`https.Agent`'s `lookup`\n * option. Node invokes this with a `{ all: true }` options object —\n * expecting `callback(err, LookupAddress[])` — whenever `autoSelectFamily`\n * is enabled (the default on Node >=20) and no `family` is pinned on the\n * connect options; otherwise it uses the legacy single-address\n * `callback(err, address, family)` form. `buildPinnedLookup` below must\n * therefore handle both callback shapes. Module-private; tests reach the\n * built function indirectly via the constructed agent's `options.lookup`.\n */\ntype PinnedLookup = LookupFunction;\n\n/**\n * Builds a pinned `lookup` function that always resolves to the same\n * pre-validated address list. Used by `buildPinnedAgents` to force axios to\n * connect only to IPs we validated rather than re-resolving the hostname.\n *\n * Handles both calling conventions Node uses for an Agent's pinned `lookup`:\n * the `{ all: true }` shape (the default on Node >=20 when Happy-Eyeballs is\n * active) expects `callback(err, [{ address, family }, …])`; the legacy\n * single-address `callback(err, address, family)` form is used otherwise (it\n * receives the first validated address). Without the array branch, `net`\n * receives `undefined` from the single-address form and the socket connect\n * throws `ERR_INVALID_IP_ADDRESS`.\n */\nconst buildPinnedLookup =\n  (addresses: PinnedAddress[]): PinnedLookup =>\n  (_hostname, options, callback): void => {\n    if (options?.all) {\n      callback(null, addresses);\n    } else {\n      const first = addresses[0]!;\n      callback(null, first.address, first.family);\n    }\n  };\n\n/**\n * Builds `httpAgent` and `httpsAgent` instances whose internal `lookup`\n * function is pinned to a pre-validated address list. Passed to axios via the\n * per-request config so the kernel resolver is never consulted again after\n * our public-IP validation. Mitigates the classic DNS-rebinding exploit where\n * an attacker-controlled authoritative server answers the validation lookup\n * with a public IP and the subsequent connect-time lookup with\n * `127.0.0.1`/`169.254.169.254`/etc.\n *\n * Each call returns a new pair of agents (one per request); the agents are\n * not reused across requests so the pinning lifetime matches the redirect\n * loop hop that validated the IPs. Agents are not explicitly `destroy()`-ed\n * because Node garbage-collects unused agents once their sockets close.\n *\n * The agents deliberately do NOT set `autoSelectFamily: false` or pin a single\n * `family`: leaving Node's default Happy-Eyeballs enabled lets the socket\n * connect fail over across the validated addresses (e.g. try IPv4 when the\n * first-returned IPv6 is unreachable) instead of hanging until timeout. This\n * does not weaken the SSRF boundary — the socket can still only connect to an\n * address the pinned `lookup` returns, and every one of those was validated\n * public by `resolvePinnedAddresses`.\n *\n * Accepts either the legacy `(address, family)` pair (backward-compatible with\n * external callers) or a pre-validated `PinnedAddress[]` list.\n */\nexport function buildPinnedAgents(\n  address: string,\n  family: 4 | 6,\n): { httpAgent: http.Agent; httpsAgent: https.Agent };\nexport function buildPinnedAgents(addresses: PinnedAddress[]): {\n  httpAgent: http.Agent;\n  httpsAgent: https.Agent;\n};\nexport function buildPinnedAgents(\n  addressOrList: string | PinnedAddress[],\n  family?: 4 | 6,\n): { httpAgent: http.Agent; httpsAgent: https.Agent } {\n  const addresses: PinnedAddress[] = Array.isArray(addressOrList)\n    ? addressOrList\n    : [{ address: addressOrList, family: family as 4 | 6 }];\n  const lookup = buildPinnedLookup(addresses);\n  return {\n    httpAgent: new http.Agent({ lookup }),\n    httpsAgent: new https.Agent({ lookup }),\n  };\n}\n\n/**\n * Tests one candidate host string against one allowlist entry.\n *\n * - An exact entry (`picsum.photos`) matches only that host verbatim —\n *   identical to the previous exact-match behavior, so existing configs are\n *   unaffected.\n * - A wildcard entry (`*.picsum.photos`) matches the apex (`picsum.photos`)\n *   AND any subdomain (`fastly.picsum.photos`, `i.picsum.photos`, …). The\n *   leading dot in the suffix check (`.picsum.photos`) is load-bearing: it\n *   prevents a sibling-label bypass such as `evilpicsum.photos`, which does\n *   NOT end with `.picsum.photos`.\n *\n * SECURITY: a wildcard relaxes only the hostname allowlist. Every redirect\n * hop is still independently re-validated against the DNS public-IP guard\n * (`resolvePinnedAddresses` → `isPrivateIp`), so a wildcard can never open an\n * SSRF path to a private/loopback/link-local address.\n */\nconst hostMatchesEntry = (host: string, entry: string): boolean => {\n  if (entry.startsWith(\"*.\")) {\n    const suffix = entry.slice(1); // \"*.picsum.photos\" → \".picsum.photos\"\n    return host === entry.slice(2) || host.endsWith(suffix);\n  }\n  return host === entry;\n};\n\nconst isHostAllowed = (\n  hostname: string,\n  host: string,\n  allowedNetworkList: string[],\n): boolean =>\n  allowedNetworkList.some(\n    (entry) =>\n      hostMatchesEntry(hostname, entry) || hostMatchesEntry(host, entry),\n  );\n\n/**\n * Issues a single (non-redirecting) GET request and returns the axios response\n * or null on transport error / non-2xx (when redirects are present). The\n * caller supplies a pinned pair of `httpAgent`/`httpsAgent` so the TCP\n * connection targets the IP that was validated by `resolvePinnedAddress`\n * rather than whatever the kernel resolver returns at connect time.\n */\nconst requestNoRedirect = async (\n  src: string,\n  timeoutMs: number,\n  maxBytes: number,\n  agents: { httpAgent: http.Agent; httpsAgent: https.Agent },\n): Promise<AxiosResponse | null> => {\n  try {\n    return await axios.get(src, {\n      responseType: \"arraybuffer\",\n      timeout: timeoutMs,\n      maxContentLength: maxBytes,\n      maxBodyLength: maxBytes,\n      maxRedirects: 0,\n      httpAgent: agents.httpAgent,\n      httpsAgent: agents.httpsAgent,\n      // Disable axios' ambient HTTP(S)_PROXY / http(s)_proxy env-var proxy\n      // detection. The allowlist + public-IP validation + DNS pinning above\n      // are this request's entire security boundary, and an operator-machine\n      // env proxy would silently defeat all three: an IP-literal proxy\n      // connects the socket to a target we never validated, and a\n      // hostname-literal proxy re-resolves at connect time through the\n      // proxy's own resolver, undoing the pinned `lookup` that closes the\n      // DNS-rebinding window. `proxy: false` is axios' own documented\n      // mitigation for exactly this (see axios THREATMODEL.md \"Proxy\n      // environment variable hijack\"). An operator who needs an egress proxy\n      // must front this middleware with one at the network layer instead.\n      proxy: false,\n      validateStatus: (status) =>\n        (status >= 200 && status < 300) || (status >= 300 && status < 400),\n    });\n  } catch (err) {\n    // axios throws on 3xx because of maxRedirects: 0; pull response if present\n    const aerr = err as AxiosError;\n    if (aerr?.response) return aerr.response;\n    return null;\n  }\n};\n\n/**\n * Fetches an image from a network source with manual redirect handling.\n * Every hop re-validates the destination against the allowlist, restricts\n * the protocol to http/https, and verifies the destination hostname does\n * not resolve to a private/loopback/link-local IP (SSRF protection).\n *\n * @param {string} src - The URL of the image.\n * @param {ImageType} [type=\"normal\"] - Type of fallback image in case of an error.\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\n */\nconst fetchFromNetwork = async (\n  src: string,\n  type: ImageType = \"normal\",\n  {\n    timeoutMs,\n    maxBytes,\n    allowedNetworkList,\n    maxRedirects,\n    onError,\n    onFallback,\n  }: {\n    timeoutMs: number;\n    maxBytes: number;\n    allowedNetworkList: string[];\n    maxRedirects: number;\n    onError?: PixelServeOnError;\n    /**\n     * Optional callback fired whenever this call resolves to the bundled\n     * `FALLBACKIMAGES[type]()` placeholder rather than genuinely-fetched\n     * bytes (blocked host, SSRF-reject, non-2xx, disallowed MIME, transport\n     * failure, etc.). Trailing and optional — backward-compatible.\n     */\n    onFallback?: () => void;\n  },\n): Promise<Buffer> => {\n  const fallback = async (): Promise<Buffer> => {\n    onFallback?.();\n    return FALLBACKIMAGES[type]();\n  };\n  try {\n    let currentUrl = src;\n    for (let hop = 0; hop <= maxRedirects; hop++) {\n      let parsed: URL;\n      try {\n        parsed = new URL(currentUrl);\n      } catch (err) {\n        safeOnError(onError, err, \"fetch\", currentUrl);\n        return await fallback();\n      }\n      if (![\"http:\", \"https:\"].includes(parsed.protocol)) {\n        safeOnError(\n          onError,\n          new Error(`disallowed protocol ${parsed.protocol}`),\n          \"fetch\",\n          currentUrl,\n        );\n        return await fallback();\n      }\n      if (!isHostAllowed(parsed.hostname, parsed.host, allowedNetworkList)) {\n        safeOnError(\n          onError,\n          new Error(`host ${parsed.hostname} not in allowedNetworkList`),\n          \"fetch\",\n          currentUrl,\n        );\n        return await fallback();\n      }\n      // Resolve once and pin the validated addresses into the http(s) agent's\n      // `lookup` function so axios connects only to IPs we validated, NOT\n      // whatever the kernel resolver answers microseconds later. This closes\n      // the classic DNS-rebinding TOCTOU window across the manual redirect\n      // loop. The pinned-address helper also runs the `isPrivateIp`\n      // validation, so a separate `isPublicHost` call is redundant here.\n      // Pinning ALL validated addresses lets Happy-Eyeballs fail over across\n      // families instead of hanging on an unreachable first address.\n      const pinned = await resolvePinnedAddresses(parsed.hostname);\n      if (!pinned) {\n        safeOnError(\n          onError,\n          new Error(\n            `host ${parsed.hostname} resolves to a private IP or DNS lookup failed`,\n          ),\n          \"fetch\",\n          currentUrl,\n        );\n        return await fallback();\n      }\n\n      const agents = buildPinnedAgents(pinned);\n      const response = await requestNoRedirect(\n        currentUrl,\n        timeoutMs,\n        maxBytes,\n        agents,\n      );\n      if (!response) {\n        safeOnError(\n          onError,\n          new Error(\"network request returned no response\"),\n          \"fetch\",\n          currentUrl,\n        );\n        return await fallback();\n      }\n\n      if (response.status >= 300 && response.status < 400) {\n        const location = response.headers?.[\"location\"] as string | undefined;\n        if (!location) {\n          safeOnError(\n            onError,\n            new Error(\"redirect response missing Location header\"),\n            \"fetch\",\n            currentUrl,\n          );\n          return await fallback();\n        }\n        // resolve relative redirects against current URL\n        try {\n          currentUrl = new URL(location, currentUrl).toString();\n        } catch (err) {\n          safeOnError(onError, err, \"fetch\", location);\n          return await fallback();\n        }\n        continue;\n      }\n\n      if (response.status < 200 || response.status >= 300) {\n        safeOnError(\n          onError,\n          new Error(`non-2xx status ${response.status}`),\n          \"fetch\",\n          currentUrl,\n        );\n        return await fallback();\n      }\n\n      const contentType = (\n        response.headers?.[\"content-type\"] as string | undefined\n      )\n        ?.toLowerCase()\n        ?.split(\";\")[0]\n        ?.trim();\n      const allowedMimeTypes = Object.values(mimeTypes);\n\n      if (contentType && allowedMimeTypes.includes(contentType)) {\n        return Buffer.from(response.data as ArrayBuffer);\n      }\n      safeOnError(\n        onError,\n        new Error(\n          `disallowed content-type ${contentType ?? \"missing\"} for ${currentUrl}`,\n        ),\n        \"fetch\",\n        currentUrl,\n      );\n      return await fallback();\n    }\n    // exhausted redirect budget\n    safeOnError(\n      onError,\n      new Error(`exceeded maxRedirects=${maxRedirects}`),\n      \"fetch\",\n      src,\n    );\n    return await fallback();\n  } catch (err) {\n    safeOnError(onError, err, \"fetch\", src);\n    return await fallback();\n  }\n};\n\n/**\n * Reads an image from the local file system.\n *\n * @param {string} filePath - Path to the image file.\n * @param {string} baseDir - Base directory to resolve paths.\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\n * @param {number} [maxBytes] - Optional max file size; larger files fall back.\n * @param {PixelServeOnError} [onError] - Optional error observability hook.\n * @param {() => void} [onFallback] - Optional callback fired whenever this\n *   call resolves to the bundled `FALLBACKIMAGES[type]()` placeholder rather\n *   than the requested file's real bytes. Lets callers (namely `serveImage`)\n *   distinguish a genuinely-served image from a placeholder without\n *   re-deriving the same validity/size checks. Trailing and optional so the\n *   exported signature stays backward-compatible.\n * @returns {Promise<Buffer>} A buffer containing the image data.\n */\nexport const readLocalImage = async (\n  filePath: string,\n  baseDir: string,\n  type: ImageType = \"normal\",\n  maxBytes?: number,\n  onError?: PixelServeOnError,\n  onFallback?: () => void,\n): Promise<Buffer> => {\n  const fallback = async (): Promise<Buffer> => {\n    onFallback?.();\n    return FALLBACKIMAGES[type]();\n  };\n  const isValid = await isValidPath(baseDir, filePath);\n  if (!isValid) {\n    safeOnError(\n      onError,\n      new Error(`invalid local path: ${filePath}`),\n      \"fs\",\n      filePath,\n    );\n    return await fallback();\n  }\n  try {\n    const resolvedFile = path.resolve(baseDir, filePath);\n    if (maxBytes) {\n      const stats = await fs.stat(resolvedFile);\n      if (stats.size > maxBytes) {\n        safeOnError(\n          onError,\n          new Error(\n            `local file ${filePath} exceeds maxDownloadBytes (${stats.size} > ${maxBytes})`,\n          ),\n          \"fs\",\n          filePath,\n        );\n        return await fallback();\n      }\n    }\n    return await fs.readFile(resolvedFile);\n  } catch (err) {\n    safeOnError(onError, err, \"fs\", filePath);\n    return await fallback();\n  }\n};\n\n/**\n * Strips a leading prefix from `pathname` using either a literal-string\n * `apiPrefix` (preferred, ReDoS-free) or a user-supplied `apiRegex`. When\n * both are provided, `apiPrefix` wins — the regex is not evaluated at all,\n * so a vulnerable pattern in `apiRegex` cannot reach this code path.\n *\n * Exported for unit-testability of the precedence + prefix matching logic.\n */\nexport const stripApiPrefix = (\n  pathname: string,\n  apiRegex: RegExp,\n  apiPrefix: string | undefined,\n): string => {\n  if (apiPrefix !== undefined) {\n    return pathname.startsWith(apiPrefix)\n      ? pathname.slice(apiPrefix.length)\n      : pathname;\n  }\n  return pathname.replace(apiRegex, \"\");\n};\n\n/**\n * Normalizes a configured `websiteURL` (bare hostname, `host:port`, or a\n * full URL, all three accepted by `optionsSchema`) into the `{ hostname,\n * host }` pair `fetchImage` compares against `url.hostname`/`url.host`.\n *\n * `websiteURL` is parsed as a URL by prepending a placeholder `http://`\n * scheme whenever the configured value has no `://` of its own — otherwise\n * a bare `host:port` value like `\"localhost:3001\"` parses as the opaque-path\n * URL `{ protocol: \"localhost:\", pathname: \"3001\" }` instead of an authority\n * with a host (verified against `new URL()`'s WHATWG behavior), which is not\n * what an operator configuring `websiteURL: \"localhost:3001\"` means. The\n * scheme itself is discarded — only `hostname`/`host` are read.\n *\n * On parse failure (defensively — `optionsSchema` already restricts\n * `websiteURL` to values that parse cleanly this way), falls back to\n * comparing the raw configured string directly, matching this function's\n * pre-normalization behavior so a parse failure never makes a\n * previously-working exact-string config silently stop matching.\n *\n * Returns `null` when `websiteURL` is `undefined` (internal-host detection\n * disabled entirely, matching prior behavior).\n */\nconst normalizeWebsiteHost = (\n  websiteURL: string | undefined,\n): { hostname: string; host: string } | null => {\n  if (websiteURL === undefined) return null;\n  try {\n    const parsed = new URL(\n      websiteURL.includes(\"://\") ? websiteURL : `http://${websiteURL}`,\n    );\n    return { hostname: parsed.hostname, host: parsed.host };\n  } catch {\n    return { hostname: websiteURL, host: websiteURL };\n  }\n};\n\n/**\n * Resolves an `http(s)` URL `src` to an API-prefix-stripped local pathname\n * when its host matches the configured `websiteURL` (the \"internal host\"\n * case — the app's own image endpoint referencing itself by absolute URL).\n * Returns `null` when `src` does not parse as a URL, `websiteURL` is not\n * configured (`normalizeWebsiteHost` returns `null`), or the URL's host\n * matches neither the bare nor `www.`-prefixed configured hostname/host.\n *\n * Single source of truth for \"is this src actually a local file reachable\n * through our own internal host\" — shared by `fetchImage` (decides whether\n * to read locally or fetch over the network) and `buildSourceIdentifier`\n * (`pixel.ts`; keys the deterministic ETag on the underlying file's\n * `mtime:size` rather than the immutable URL string) so the two internal-\n * host detection rules cannot drift apart.\n */\nexport const resolveInternalLocalPath = (\n  src: string,\n  websiteURL: string | undefined,\n  apiRegex: RegExp,\n  apiPrefix: string | undefined,\n): string | null => {\n  let url: URL;\n  try {\n    url = new URL(src);\n  } catch {\n    return null;\n  }\n  const configuredHost = normalizeWebsiteHost(websiteURL);\n  const isInternal =\n    configuredHost !== null &&\n    ([configuredHost.hostname, `www.${configuredHost.hostname}`].includes(\n      url.hostname,\n    ) ||\n      [configuredHost.host, `www.${configuredHost.host}`].includes(url.host));\n  if (!isInternal) return null;\n  return stripApiPrefix(url.pathname, apiRegex, apiPrefix);\n};\n\n/**\n * Fetches an image from either a local file or a network source.\n *\n * @param {string} src - The URL or local path of the image.\n * @param {string} baseDir - Base directory to resolve local paths.\n * @param {string} websiteURL - The website's configured internal host —\n *   accepts a bare hostname (`\"example.com\"`), a `host:port` pair\n *   (`\"example.com:8080\"`), or a full URL (`\"https://example.com:8080\"`);\n *   all three are normalized to a hostname/host pair via\n *   `normalizeWebsiteHost` before comparison.\n * @param {ImageType} [type=\"normal\"] - Type of fallback image if the path is invalid.\n * @param {string[]} [allowedNetworkList=[]] - List of allowed network hosts.\n *\n * The trailing options object also accepts an optional `onFallback: () =>\n * void` field, fired whenever this call resolves to the bundled\n * `FALLBACKIMAGES[type]()` placeholder rather than genuinely-resolved bytes —\n * whether from the internal-local, network, or exception-recovery branch.\n * Optional and additive, so the exported signature stays backward-compatible.\n * @returns {Promise<Buffer>} A buffer containing the image data or a fallback image.\n */\nexport const fetchImage = (\n  src: string,\n  baseDir: string,\n  websiteURL: string | undefined,\n  type: ImageType = \"normal\",\n  apiRegex: RegExp,\n  allowedNetworkList: string[] = [],\n  {\n    timeoutMs,\n    maxBytes,\n    maxRedirects = 3,\n    onError,\n    apiPrefix,\n    onFallback,\n  }: {\n    timeoutMs: number;\n    maxBytes: number;\n    maxRedirects?: number;\n    onError?: PixelServeOnError;\n    apiPrefix?: string;\n    onFallback?: () => void;\n  },\n): Promise<Buffer> => {\n  try {\n    const internalLocalPath = resolveInternalLocalPath(\n      src,\n      websiteURL,\n      apiRegex,\n      apiPrefix,\n    );\n    if (internalLocalPath !== null) {\n      return readLocalImage(\n        internalLocalPath,\n        baseDir,\n        type,\n        maxBytes,\n        onError,\n        onFallback,\n      );\n    }\n\n    const url = new URL(src);\n    const allowedCondition = isHostAllowed(\n      url.hostname,\n      url.host,\n      allowedNetworkList,\n    );\n    if (!allowedCondition) {\n      safeOnError(\n        onError,\n        new Error(`host ${url.hostname} not in allowedNetworkList`),\n        \"fetch\",\n        src,\n      );\n      onFallback?.();\n      return FALLBACKIMAGES[type]();\n    }\n    if (![\"http:\", \"https:\"].includes(url.protocol)) {\n      safeOnError(\n        onError,\n        new Error(`disallowed protocol ${url.protocol}`),\n        \"fetch\",\n        src,\n      );\n      onFallback?.();\n      return FALLBACKIMAGES[type]();\n    }\n    return fetchFromNetwork(src, type, {\n      timeoutMs,\n      maxBytes,\n      allowedNetworkList,\n      maxRedirects,\n      onError,\n      onFallback,\n    });\n  } catch (err) {\n    safeOnError(onError, err, \"fetch\", src);\n    return readLocalImage(src, baseDir, type, maxBytes, onError, onFallback);\n  }\n};\n","import { z } from \"zod\";\nimport type {\n  ImageFormat,\n  PixelServeOnError,\n  PixelServeOnComplete,\n} from \"./types\";\nimport { API_REGEX, allowedFormats } from \"./variables\";\n\nconst imageFormatEnum = z.enum(allowedFormats as [string, ...string[]]);\nconst imageTypeEnum = z.enum([\"avatar\", \"normal\"]);\n\nexport const userDataSchema = z\n  .object({\n    src: z\n      // Reject arrays/objects/numbers/booleans with a clear error — a\n      // malicious or buggy client sending `?src[]=a&src[]=b` (which Express\n      // parses as an array) gets a meaningful rejection here. Anything that\n      // is not a primitive string or `undefined`/`null` falls into the outer\n      // pipeline catch and produces the standard fallback image rather than\n      // `[object Object]` being forwarded through downstream logic.\n      //\n      // `src` is intentionally truly optional with no `.min(1)` and no\n      // default — empty strings and `undefined` are both valid inputs at\n      // the schema layer, and `pixel.ts` handles them via the\n      // `if (!userData.src)` branch which serves the appropriate fallback\n      // image based on the requested `type`.\n      .preprocess((value, ctx) => {\n        if (value === undefined || value === null) return value;\n        if (typeof value === \"string\") return value;\n        const got = Array.isArray(value) ? \"array\" : typeof value;\n        ctx.addIssue({\n          code: z.ZodIssueCode.custom,\n          message: `src must be a string (received ${got})`,\n        });\n        return z.NEVER;\n      }, z.string().optional())\n      .optional(),\n    format: z\n      .string()\n      .optional()\n      .transform((val): ImageFormat | undefined => {\n        const lower = val?.toLowerCase();\n        return lower && imageFormatEnum.options.includes(lower)\n          ? (lower as ImageFormat)\n          : undefined;\n      })\n      .optional(),\n    width: z\n      .union([z.number(), z.string()])\n      .optional()\n      .transform((value) =>\n        value === undefined || value === null ? undefined : Number(value),\n      )\n      .pipe(\n        z\n          .number()\n          .int()\n          // Framework hard window is [1, 4000]. A request inside the window is\n          // then clamped to the operator's minWidth/maxWidth by\n          // renderUserData; a request below 1 or above 4000 is rejected here\n          // (→ fallback image). The floor was lowered from 50 to 1 so common\n          // small sizes (32px/48px avatars, 16px favicons, thumbnails) are\n          // servable when the operator opts in with a low minWidth; the\n          // previous floor turned every sub-50px request into a placeholder.\n          .min(1, \"width too small\")\n          .max(4000, \"width too large\")\n          .optional(),\n      ),\n    height: z\n      .union([z.number(), z.string()])\n      .optional()\n      .transform((value) =>\n        value === undefined || value === null ? undefined : Number(value),\n      )\n      .pipe(\n        z\n          .number()\n          .int()\n          .min(1, \"height too small\")\n          .max(4000, \"height too large\")\n          .optional(),\n      ),\n    // No `.default(80)` here (Phase 5): a hard-coded schema default always\n    // won, so `renderUserData`'s `parsed.quality ?? bounds.defaultQuality`\n    // fallback could never fire and the documented `defaultQuality` option\n    // was dead code. Leaving `quality` genuinely optional lets that fallback\n    // govern; `optionsSchema.defaultQuality`'s own `.default(80)` keeps the\n    // effective middleware default unchanged.\n    quality: z\n      .union([z.number(), z.string()])\n      .optional()\n      .transform((value) =>\n        value === undefined || value === null ? undefined : Number(value),\n      )\n      .pipe(z.number().int().min(1).max(100).optional()),\n    folder: z.enum([\"public\", \"private\"]).default(\"public\"),\n    type: imageTypeEnum.default(\"normal\"),\n    userId: z\n      .union([z.string(), z.number()])\n      .optional()\n      .transform((value) =>\n        value === undefined || value === null\n          ? undefined\n          : String(value).trim(),\n      )\n      .pipe(\n        z\n          .string()\n          .min(1, \"userId cannot be empty\")\n          .max(128, \"userId too long\")\n          .optional(),\n      ),\n  })\n  .strict();\n\nexport const optionsSchema = z\n  .object({\n    baseDir: z.string().min(1, \"baseDir is required\"),\n    idHandler: z\n      .custom<\n        (id: string) => string | Promise<string>\n      >((val) => typeof val === \"function\", { message: \"idHandler must be a function\" })\n      .optional(),\n    getUserFolder: z\n      .custom<\n        (req: unknown, id?: string) => Promise<string> | string\n      >((val) => typeof val === \"function\", { message: \"getUserFolder must be a function\" })\n      .optional(),\n    getUserFolderRootDir: z.string().min(1).optional(),\n    websiteURL: z\n      .union([\n        z.url(),\n        // Hostname grammar with NO nested quantifiers — every label is 1-63\n        // alphanumeric/hyphen chars, labels are dot-separated, and no label\n        // may start with `-`. The earlier `/^(?![-.])([\\w]+[-.]?)*[\\w]+$/`\n        // was ReDoS-vulnerable (catastrophic backtracking on inputs like\n        // `\"a\".repeat(50) + \"!\"`); this anchored pattern runs in linear time\n        // because the outer group is a flat `(\\.label)*` with no nested\n        // repetition. Single-label hostnames (`localhost`) and FQDNs both\n        // validate; the previous regex's underscore acceptance is dropped\n        // because RFC 1123 hostnames do not include `_`.\n        z\n          .string()\n          .regex(/^(?!-)[A-Za-z0-9-]{1,63}(\\.(?!-)[A-Za-z0-9-]{1,63})*$/),\n      ])\n      .optional(),\n    apiRegex: z.instanceof(RegExp).default(API_REGEX),\n    apiPrefix: z.string().min(1, \"apiPrefix cannot be empty\").optional(),\n    allowedNetworkList: z\n      .array(\n        z\n          .string()\n          // Trim FIRST so the regex/min-length checks below operate on the\n          // operator-intended hostname rather than incidental whitespace\n          // injected by `.env` files or CI config copies.\n          .transform((value) => value.trim())\n          .pipe(\n            z\n              .string()\n              .min(1, \"allowedNetworkList entries cannot be empty\")\n              // Hostnames only, with an OPTIONAL leading `*.` wildcard.\n              // Letters, digits, dots, hyphens for the host part; a single\n              // `*.` prefix opts the entry into subdomain matching (see\n              // `isHostAllowed` in functions.ts — the wildcard matches the\n              // apex AND any subdomain). Rejects whitespace-only entries\n              // (after trim → empty → caught by `.min(1)`) and any entry\n              // containing path/protocol/internal-whitespace characters. The\n              // regex is intentionally permissive on the host part (no FQDN\n              // structure enforcement) so single-label hosts and IDN-encoded\n              // punycode labels still parse.\n              .regex(\n                /^(\\*\\.)?[a-z0-9.-]+$/i,\n                \"allowedNetworkList entry is not a valid hostname\",\n              )\n              // Footgun guard: a wildcard entry must carry at least two\n              // NON-EMPTY labels after `*.` (e.g. `*.picsum.photos`), so an\n              // overly broad `*.com` / `*.` / `*` can never be configured.\n              // Empty labels are filtered before counting so a trailing or\n              // doubled dot cannot smuggle a too-broad entry past the count:\n              // `*.com.` would otherwise split to [\"com\", \"\"] (length 2) and\n              // be accepted, then match every `*.com.` FQDN — the WHATWG URL\n              // parser preserves a trailing root dot in `hostname`. Note the\n              // wildcard relaxes only the HOSTNAME check; the per-hop DNS\n              // public-IP guard still runs on every redirect, so a wildcard\n              // can never open an SSRF path to a private IP. Public-suffix\n              // families (`*.co.uk`) are not special-cased — an operator that\n              // lists such an entry accepts every host under it.\n              .refine(\n                (entry) =>\n                  !entry.startsWith(\"*.\") ||\n                  entry.slice(2).split(\".\").filter(Boolean).length >= 2,\n                {\n                  message:\n                    \"wildcard allowedNetworkList entry must have at least two labels after '*.' (e.g. *.example.com)\",\n                },\n              ),\n          ),\n      )\n      // Also lowercase so case-mismatched config still matches the WHATWG-URL-\n      // lowercased `url.hostname`. The trim above happens per-entry inside the\n      // inner schema; this transform finishes the normalisation. The `*.`\n      // prefix is unaffected by lowercasing.\n      .transform((arr) => arr.map((host) => host.toLowerCase()))\n      .default([]),\n    cacheControl: z.string().optional(),\n    etag: z.boolean().default(true),\n    minWidth: z.number().int().positive().default(50),\n    maxWidth: z.number().int().positive().default(4000),\n    minHeight: z.number().int().positive().default(50),\n    maxHeight: z.number().int().positive().default(4000),\n    defaultQuality: z.number().int().min(1).max(100).default(80),\n    requestTimeoutMs: z.number().int().positive().default(5000),\n    idHandlerTimeoutMs: z.number().int().positive().optional(),\n    maxDownloadBytes: z.number().int().positive().default(5_000_000),\n    maxRedirects: z.number().int().min(0).max(10).default(3),\n    maxInputPixels: z\n      .number()\n      .int()\n      .positive()\n      .default(16_000 * 16_000),\n    allowSvgInput: z.boolean().default(false),\n    onError: z\n      .custom<PixelServeOnError>((val) => typeof val === \"function\", {\n        message: \"onError must be a function\",\n      })\n      .optional(),\n    onComplete: z\n      .custom<PixelServeOnComplete>((val) => typeof val === \"function\", {\n        message: \"onComplete must be a function\",\n      })\n      .optional(),\n  })\n  .strict()\n  .refine((data) => data.minWidth <= data.maxWidth, {\n    message: \"minWidth must be less than or equal to maxWidth\",\n    path: [\"minWidth\"],\n  })\n  .refine((data) => data.minHeight <= data.maxHeight, {\n    message: \"minHeight must be less than or equal to maxHeight\",\n    path: [\"minHeight\"],\n  })\n  // userDataSchema hard-rejects any request width/height outside [1, 4000]\n  // (see above) before renderUserData's clamp() ever runs, so an operator\n  // maxWidth/maxHeight configured above 4000 is silently non-functional for\n  // the out-of-window portion of its range — e.g. maxWidth: 5000 can never\n  // satisfy a width:4500 request, since the schema throws \"width too large\"\n  // first. Fail loudly at registerServe() time instead of shipping a config\n  // that quietly does nothing. The lower bound needs no refinement: minWidth/\n  // minHeight are already `.positive()` (>= 1), which is exactly the window\n  // floor, so any valid config is representable.\n  .refine((data) => data.maxWidth <= 4000, {\n    message:\n      \"maxWidth must lie within the framework's hard [1, 4000] dimension window\",\n    path: [\"maxWidth\"],\n  })\n  .refine((data) => data.maxHeight <= 4000, {\n    message:\n      \"maxHeight must lie within the framework's hard [1, 4000] dimension window\",\n    path: [\"maxHeight\"],\n  });\n\nexport type ParsedUserData = z.infer<typeof userDataSchema>;\nexport type ParsedOptions = z.infer<typeof optionsSchema>;\n","import { optionsSchema, userDataSchema } from \"./schema\";\nimport type { ParsedOptions, ParsedUserData } from \"./schema\";\nimport type { ImageFormat, PixelServeOptions } from \"./types\";\n\n/**\n * @typedef {(\"avatar\" | \"normal\")} ImageType\n * @description Defines the type of image being processed.\n */\n\n/**\n * @typedef {(\"jpeg\" | \"jpg\" | \"png\" | \"webp\" | \"gif\" | \"tiff\" | \"avif\")} ImageFormat\n * @description Supported output formats. SVG is intentionally excluded because\n *   Sharp/libvips cannot re-encode SVG output.\n */\n\n/**\n * @typedef {Object} Options\n * @property {string} baseDir - The base directory for public image files.\n * @property {function(string): string} idHandler - A function to handle user IDs.\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\n */\n\n/**\n * @typedef {Object} UserData\n * @property {number|string} quality - Quality of the image (1–100).\n * @property {ImageFormat} format - Desired format of the image.\n * @property {string} [src] - Source path or URL for the image.\n * @property {string} [folder] - The folder type (\"public\" or \"private\").\n * @property {ImageType} [type] - Type of the image (\"avatar\" or \"normal\").\n * @property {string|null} [userId] - Optional user identifier.\n * @property {number|string} [width] - Desired image width.\n * @property {number|string} [height] - Desired image height.\n */\n\n/**\n * Renders the options object with default values and user-provided values.\n *\n * @param {Partial<Options>} options - The user-provided options.\n * @returns {Options} The rendered options object.\n */\nexport const renderOptions = (options: PixelServeOptions): ParsedOptions =>\n  optionsSchema.parse(options);\n\n/**\n * Renders the user data object with default values and user-provided values.\n *\n * @param {Partial<UserData>} userData - The user-provided data.\n * @returns {UserData} The rendered user data object.\n */\n/**\n * Result of `renderUserData`. Narrower than `ParsedUserData` (Zod-inferred):\n * `format` is guaranteed to be an `ImageFormat` (defaulting to `\"jpeg\"`),\n * and `quality` is guaranteed to be a number (defaulting to\n * `bounds.defaultQuality`). The remaining fields keep their Zod-inferred\n * types, so callers can drop ad-hoc `as ImageFormat` / `as ImageType`\n * casts in favor of the validated shape.\n */\nexport type RenderedUserData = Omit<ParsedUserData, \"format\" | \"quality\"> & {\n  format: ImageFormat;\n  quality: number;\n};\n\nexport const renderUserData = (\n  userData: unknown,\n  bounds: {\n    minWidth: number;\n    maxWidth: number;\n    minHeight: number;\n    maxHeight: number;\n    defaultQuality: number;\n  },\n): RenderedUserData => {\n  const parsed = userDataSchema.parse(userData);\n\n  const clamp = (\n    value: number | undefined,\n    min: number,\n    max: number,\n  ): number | undefined => {\n    if (value === undefined) return undefined;\n    return Math.min(Math.max(value, min), max);\n  };\n\n  return {\n    ...parsed,\n    width: clamp(parsed.width, bounds.minWidth, bounds.maxWidth),\n    height: clamp(parsed.height, bounds.minHeight, bounds.maxHeight),\n    quality: parsed.quality ?? bounds.defaultQuality,\n    format: parsed.format ?? \"jpeg\",\n  };\n};\n","import path from \"node:path\";\nimport * as fs from \"node:fs/promises\";\nimport { createHash } from \"node:crypto\";\nimport sharp, { FormatEnum, ResizeOptions } from \"sharp\";\nimport type { Request, Response, NextFunction } from \"express\";\nimport type {\n  PixelServeOptions,\n  ImageFormat,\n  ImageType,\n  PixelServeErrorContext,\n  PixelServeOnError,\n  PixelServeCompletionContext,\n  PixelServeOnComplete,\n} from \"./types\";\nimport {\n  allowedFormats,\n  API_REGEX,\n  FALLBACKIMAGES,\n  mimeTypes,\n} from \"./variables\";\nimport {\n  fetchImage,\n  isValidPath,\n  readLocalImage,\n  resolveInternalLocalPath,\n} from \"./functions\";\nimport { renderOptions, renderUserData } from \"./renders\";\nimport type { ParsedOptions } from \"./schema\";\n\n/**\n * Best-effort observability hook dispatcher. Swallows hook errors so a buggy\n * logger never crashes a request. Returns void.\n */\nconst reportError = (\n  hook: PixelServeOnError | undefined,\n  err: unknown,\n  context: PixelServeErrorContext,\n): void => {\n  if (!hook) return;\n  try {\n    hook(err, context);\n  } catch {\n    // intentionally suppressed — observability must never break the response\n  }\n};\n\n/**\n * Best-effort observability hook dispatcher for the success / 304 / hard-\n * fallback paths. Swallows hook errors so a buggy logger never crashes a\n * request. Returns void. Mirrors `reportError`'s contract so consumers can\n * rely on the same dispatch guarantees for both observability surfaces.\n */\nconst safeOnComplete = (\n  hook: PixelServeOnComplete | undefined,\n  context: PixelServeCompletionContext,\n): void => {\n  if (!hook) return;\n  try {\n    hook(context);\n  } catch {\n    // intentionally suppressed — observability must never break the response\n  }\n};\n\n/**\n * Computes the elapsed milliseconds between a `process.hrtime.bigint()`\n * checkpoint and now. Uses bigint math so monotonic-clock precision is\n * preserved (we lose precision when we convert back to a `Number`, but the\n * resulting ms float is plenty precise for APM latency reporting).\n */\nconst elapsedMs = (start: bigint): number => {\n  const diff = process.hrtime.bigint() - start;\n  // 1 ms = 1_000_000 ns. We do the division in bigint first to avoid\n  // overflowing through Number for absurdly long latencies, then attach the\n  // sub-millisecond remainder as a float.\n  const whole = Number(diff / 1_000_000n);\n  const remainder = Number(diff % 1_000_000n) / 1_000_000;\n  return whole + remainder;\n};\n\n/**\n * Derives an ASCII-safe filename and an RFC 5987 / RFC 6266\n * `filename*=UTF-8''<percent-encoded>` parameter for use in\n * `Content-Disposition`.\n *\n *  - Strips query strings and URL fragments before extracting basename.\n *  - Strips the existing extension and replaces it with the encoded output\n *    format extension supplied by the caller.\n *  - Maps every non-printable-ASCII / forbidden-header byte to `_` for the\n *    fallback `filename=` parameter (RFC 5987 prohibits raw non-ASCII bytes).\n *  - Percent-encodes the original (UTF-8) basename for the `filename*=`\n *    parameter so unicode names round-trip cleanly.\n *  - Caps both parameters to a sane length so absurd filenames cannot bloat\n *    the response header. The cap is applied AFTER the extension is appended\n *    so the extension is never truncated.\n */\nconst FILENAME_MAX_LEN = 100;\nconst ASCII_FALLBACK_DEFAULT = \"image\";\n\n/**\n * Shared Cache-Control values so the happy path, both fallback paths, and\n * (in a later phase) the 304 short-circuits cannot drift apart. A soft or\n * hard fallback serves a bundled placeholder, never the requested bytes, so\n * it must never inherit the long-lived, real-image cache policy — otherwise\n * a transient failure gets cached as if it were permanent.\n */\nconst DEFAULT_CACHE_CONTROL =\n  \"public, max-age=86400, stale-while-revalidate=604800\";\nconst FALLBACK_CACHE_CONTROL = \"public, max-age=60\";\n\nexport const buildFilename = (\n  rawSrc: string | undefined,\n  outputFormat: string,\n): { asciiFilename: string; encodedFilename: string } => {\n  const ext = outputFormat;\n  // strip query + fragment, then take basename (URL or path-shaped)\n  const stripped = (rawSrc ?? \"\").split(\"#\")[0]!.split(\"?\")[0]!;\n  const baseWithExt = path.basename(stripped);\n  const baseNoExt =\n    baseWithExt && baseWithExt !== \"/\" && baseWithExt !== \"\\\\\"\n      ? path.basename(baseWithExt, path.extname(baseWithExt))\n      : \"\";\n\n  // ASCII fallback: replace any byte outside the safe printable ASCII range,\n  // plus quote / backslash / control / DEL, with `_`. Collapse runs of `_`\n  // into one, then strip a single leading/trailing `_` via direct string\n  // ops — the regex form `/^_+|_+$/g` is flagged by CodeQL `js/polynomial-redos`\n  // even though the prior collapse guarantees a single underscore in a row.\n  let asciiBase = baseNoExt\n    .replace(/[^\\x20-\\x7E]/g, \"_\")\n    // eslint-disable-next-line no-control-regex\n    .replace(/[\"\\\\\\x00-\\x1F\\x7F]/g, \"_\")\n    .replace(/_+/g, \"_\");\n  if (asciiBase.startsWith(\"_\")) asciiBase = asciiBase.slice(1);\n  if (asciiBase.endsWith(\"_\")) asciiBase = asciiBase.slice(0, -1);\n\n  const safeAsciiBase =\n    asciiBase.length > 0 ? asciiBase : ASCII_FALLBACK_DEFAULT;\n  // Cap base length so the FULL filename stays under the limit including the\n  // extension. +1 accounts for the dot.\n  const maxBase = Math.max(1, FILENAME_MAX_LEN - ext.length - 1);\n  const truncatedAscii = safeAsciiBase.slice(0, maxBase);\n  const asciiFilename = `${truncatedAscii}.${ext}`;\n\n  // RFC 5987 encoded value: percent-encode the UTF-8 bytes. We use\n  // encodeURIComponent and then re-encode the few characters it leaves alone\n  // that are still illegal inside a quoted parameter value per RFC 5987\n  // (`'*` are reserved in attr-char; `()<>@,;:\\\"/[]?={}` are tspecials).\n  const utfBase = baseNoExt.length > 0 ? baseNoExt : ASCII_FALLBACK_DEFAULT;\n  const encodedBase = encodeURIComponent(utfBase).replace(\n    /['()*]/g,\n    (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n  );\n  // Apply the same overall length cap to the encoded form (percent-encoded\n  // bytes count toward the limit so very long unicode names stay bounded).\n  let truncatedEncoded = encodedBase.slice(0, maxBase);\n  // The slice may land INSIDE a `%XX` triplet (e.g., cutting `%E4%B8` to\n  // `%E4%B` for a long CJK name). Strict RFC 3986 / RFC 5987 parsers reject\n  // partial percent-encodings, so walk back to the nearest `%` and drop any\n  // incomplete trailing sequence. Triplets that fit (`len - lastPercent >= 3`)\n  // are kept verbatim.\n  const lastPercent = truncatedEncoded.lastIndexOf(\"%\");\n  if (lastPercent >= 0 && truncatedEncoded.length - lastPercent < 3) {\n    truncatedEncoded = truncatedEncoded.slice(0, lastPercent);\n  }\n  // The truncation may also leave an incomplete UTF-8 multi-byte sequence\n  // (e.g., `%E4` is a valid percent-encoded byte but `0xE4` alone is not\n  // valid UTF-8 — it is the lead byte of a 3-byte sequence). Walk back past\n  // any trailing UTF-8 lead bytes that lost their continuation bytes so the\n  // header value decodes cleanly under strict UTF-8 parsers. Each triplet\n  // occupies exactly 3 characters (`%XX`) so the lookback is bounded.\n  while (truncatedEncoded.length >= 3) {\n    const tail = truncatedEncoded.slice(-3);\n    if (tail[0] !== \"%\") break;\n    const byte = parseInt(tail.slice(1), 16);\n    // 0xC0-0xFD are UTF-8 lead bytes (2-byte through 6-byte sequences in the\n    // historical encoding; only 2-4 byte sequences are valid today). A lead\n    // byte at the very end has no continuation byte after it, so drop it.\n    // Continuation bytes (0x80-0xBF) sitting alone at the end without their\n    // preceding lead byte are also invalid and must be dropped — keep walking\n    // back until we find an ASCII byte or a complete multi-byte sequence.\n    if (byte >= 0xc0 && byte <= 0xfd) {\n      truncatedEncoded = truncatedEncoded.slice(0, -3);\n      break;\n    }\n    if (byte >= 0x80 && byte <= 0xbf) {\n      // Standalone continuation byte: drop it and re-evaluate the new tail.\n      truncatedEncoded = truncatedEncoded.slice(0, -3);\n      continue;\n    }\n    break;\n  }\n  const encodedFilename = `${truncatedEncoded}.${ext}`;\n\n  return { asciiFilename, encodedFilename };\n};\n\n/**\n * Returns a stable source identifier for the deterministic ETag.\n *\n *  - Local files contribute `mtimeMs:size`, so any edit to the underlying\n *    file invalidates the cache key. The local-file branch is gated behind\n *    `isValidPath` so a traversal / out-of-tree `src` is rejected BEFORE\n *    `fs.stat` ever runs — without this gate, a traversal `src` that happens\n *    to reference an existing file outside `baseDir` would still `fs.stat`\n *    successfully, turning the ETag into an oracle for that file's\n *    mtime/size and decoupling it from the fallback bytes `readLocalImage`\n *    actually serves for the same rejected path.\n *  - An `http(s)` src whose host matches the configured `options.websiteURL`\n *    (the \"internal host\" case) is resolved to its on-disk path via\n *    `resolveInternalLocalPath` — the same helper `fetchImage` uses to\n *    decide whether to read locally instead of over the network — and falls\n *    through to the SAME local-file branch below, so its ETag tracks the\n *    underlying file's `mtime:size` rather than the immutable URL string.\n *    Without this, overwriting the on-disk file behind an internal-host URL\n *    never changes its ETag, so a client can be served a stale `304`\n *    forever.\n *  - Any other `http(s)` URL contributes the resolved URL string. The\n *    framework cannot cheaply re-fetch HEAD per request, so the URL is the\n *    strongest identifier available without paying for the body.\n *  - A local file (direct path OR resolved from an internal-host URL) whose\n *    size exceeds the optional `options.maxBytes` returns `null` instead of\n *    a `file:` identifier: `readLocalImage` refuses to serve a file that\n *    large and returns the bundled fallback buffer instead, so keying the\n *    ETag on the oversized file's stat would decouple the ETag from the\n *    bytes actually served. Mirrors the size guard in `readLocalImage`.\n *  - Anything else (missing file, out-of-tree/traversal path, fallback\n *    paths) returns `null` so the caller falls back to the post-Sharp\n *    buffer hash — which always matches the bytes actually sent.\n */\nexport const buildSourceIdentifier = async (\n  src: string | undefined,\n  baseDir: string,\n  options?: {\n    websiteURL?: string;\n    apiRegex?: RegExp;\n    apiPrefix?: string;\n    maxBytes?: number;\n  },\n): Promise<string | null> => {\n  if (!src) return null;\n\n  const statLocalFile = async (localPath: string): Promise<string | null> => {\n    if (!(await isValidPath(baseDir, localPath))) return null;\n    try {\n      const resolved = path.resolve(baseDir, localPath);\n      const stats = await fs.stat(resolved);\n      if (options?.maxBytes !== undefined && stats.size > options.maxBytes) {\n        return null;\n      }\n      return `file:${stats.mtimeMs}:${stats.size}`;\n    } catch {\n      return null;\n    }\n  };\n\n  if (src.startsWith(\"http://\") || src.startsWith(\"https://\")) {\n    const internalLocalPath = resolveInternalLocalPath(\n      src,\n      options?.websiteURL,\n      options?.apiRegex ?? API_REGEX,\n      options?.apiPrefix,\n    );\n    if (internalLocalPath !== null) {\n      return statLocalFile(internalLocalPath);\n    }\n    return `url:${src}`;\n  }\n\n  return statLocalFile(src);\n};\n\n/**\n * Builds the deterministic SHA-256 ETag from the resolved user data + source\n * identifier. The result is wrapped in double-quotes per RFC 7232.\n *\n * SHA-256 is used over SHA-1 because the input contains user-controlled fields\n * (post-`idHandler` userId, src). SHA-1's collision weakness flagged by CodeQL\n * `js/weak-cryptographic-algorithm` does not affect ETag correctness in\n * practice, but a modern hash keeps static analysis green and removes any\n * theoretical concern about a third party forging a matching ETag.\n */\nexport const buildDeterministicEtag = (\n  fields: {\n    src: string | undefined;\n    width: number | undefined;\n    height: number | undefined;\n    format: string;\n    quality: number;\n    type: ImageType;\n    folder: \"public\" | \"private\";\n    parsedUserId: string | undefined;\n  },\n  sourceIdentifier: string,\n): string => {\n  const key = JSON.stringify({\n    src: fields.src ?? \"\",\n    w: fields.width ?? \"\",\n    h: fields.height ?? \"\",\n    f: fields.format,\n    q: fields.quality,\n    t: fields.type,\n    fo: fields.folder,\n    u: fields.parsedUserId ?? \"\",\n    sid: sourceIdentifier,\n  });\n  return `\"${createHash(\"sha256\").update(key).digest(\"hex\")}\"`;\n};\n\n/**\n * @typedef {Object} Options\n * @property {string} baseDir - The base directory for public image files.\n * @property {function(string): string} idHandler - A function to handle user IDs.\n * @property {function(string, Request): Promise<string>} getUserFolder - Asynchronous function to retrieve user-specific folders.\n * @property {string} websiteURL - The base URL of the website for internal link resolution.\n * @property {RegExp} apiRegex - Regex to parse API endpoints from URLs.\n * @property {string[]} allowedNetworkList - List of allowed network domains for external image fetching.\n */\n\n/**\n * Races a promise against a timeout and clears the timer on settle so it\n * cannot pin the event loop after the race resolves.\n */\nconst raceWithTimeout = async <T>(\n  promise: Promise<T>,\n  ms: number,\n  label: string,\n): Promise<T> => {\n  let timer: NodeJS.Timeout | undefined;\n  try {\n    return await Promise.race([\n      promise,\n      new Promise<never>((_, reject) => {\n        timer = setTimeout(\n          () => reject(new Error(`${label} timed out after ${ms}ms`)),\n          ms,\n        );\n      }),\n    ]);\n  } finally {\n    if (timer !== undefined) clearTimeout(timer);\n  }\n};\n\n/**\n * Verifies that `candidate` is contained within `rootDir`. Used to enforce\n * `getUserFolderRootDir` containment so a buggy or malicious `getUserFolder`\n * implementation cannot expand the framework's filesystem surface area\n * beyond an opt-in root.\n *\n * Both the **root** and the **candidate** are normalized via `fs.realpath`\n * before the containment check. This catches symlink escapes (a path that\n * lexically lives inside the root but whose final segment is a symlink\n * pointing outward) at the containment layer rather than waiting for the\n * downstream `isValidPath` read. When `fs.realpath` fails — typically\n * because the candidate is a lazy per-user directory that doesn't exist\n * yet — the function falls back to the lexical `path.resolve` value so\n * containment can still be evaluated; the descendant `isValidPath()`\n * check then realpaths the actual file before reading.\n *\n * The optional `preResolvedRoot` parameter lets the middleware factory\n * cache the resolved root path once at startup and skip the per-request\n * `fs.realpath` syscall on the root side. When supplied, the function\n * treats it as the already-resolved value.\n *\n * Returns `true` when the candidate is inside the root (or equal to it).\n * Returns `false` for empty inputs and any escape detected lexically or\n * via realpath.\n */\nexport const isInsideRoot = async (\n  rootDir: string,\n  candidate: string,\n  preResolvedRoot?: string,\n): Promise<boolean> => {\n  if (!rootDir || !candidate) return false;\n  let realRoot: string;\n  if (preResolvedRoot !== undefined) {\n    // The factory already paid the realpath cost — use the cached value.\n    realRoot = preResolvedRoot;\n  } else {\n    try {\n      realRoot = await fs.realpath(path.resolve(rootDir));\n    } catch {\n      // Root does not exist or is unreadable; fall back to lexical resolve\n      // so a not-yet-created root tree can still be evaluated. Symlink\n      // escapes from this branch are out of scope — the caller opted into a\n      // root that does not exist yet, and any descendant `isValidPath`\n      // check will still realpath the final candidate before reading.\n      realRoot = path.resolve(rootDir);\n    }\n  }\n\n  const lexicalCandidate = path.resolve(candidate);\n  // Resolve the candidate through realpath so a `getUserFolder` result that\n  // is a symlink pointing outside the root is caught here rather than\n  // silently passing the lexical-prefix check. When the candidate does not\n  // exist on disk yet, fall back to the lexical resolve — the descendant\n  // isValidPath() check will realpath the final file before reading.\n  let realCandidate: string;\n  try {\n    realCandidate = await fs.realpath(lexicalCandidate);\n  } catch {\n    realCandidate = lexicalCandidate;\n  }\n  if (realRoot === realCandidate) return true;\n  const relative = path.relative(realRoot, realCandidate);\n  if (relative === \"\" || relative === \".\") return true;\n  return !relative.startsWith(\"..\") && !path.isAbsolute(relative);\n};\n\n/**\n * Resolves a configured `getUserFolderRootDir` to its canonical realpath\n * once at middleware-factory time. Returns the lexically-resolved path\n * when the directory does not yet exist or `fs.realpath` fails so a lazy\n * containment root can still be evaluated against future requests.\n *\n * Exported so consumers can pre-resolve their own roots for unit tests.\n */\nexport const resolveRootDir = async (rootDir: string): Promise<string> => {\n  try {\n    return await fs.realpath(path.resolve(rootDir));\n  } catch {\n    return path.resolve(rootDir);\n  }\n};\n\n/**\n * Matches a `<!DOCTYPE …>` declaration that names `svg` as the document's\n * root element (e.g. `<!DOCTYPE svg PUBLIC …>` or `<!DOCTYPE svg [ … ]>`).\n * ANCHORED to the start of the string it is tested against: callers first\n * strip any leading `<?xml …?>` declaration and `<!-- … -->` comment prolog\n * (via `skipXmlProlog`), so this matches a genuine top-level DOCTYPE token\n * and NOT the literal characters `<!doctype svg` appearing inside a comment's\n * prose (which would wrongly flag a non-SVG document that merely mentions the\n * phrase). The trailing `(?:[\\s[>]|$)` boundary keeps it from false-matching\n * a longer root name (`<!DOCTYPE svgish>`).\n *\n * A DOCTYPE naming `svg` as its root is an unambiguous SVG signal on its own,\n * independent of where — or whether — the literal `<svg` root tag falls\n * inside the scanned window: an oversized internal-subset DTD (a\n * billion-laughs entity-bomb) can pad the `<svg` tag past the 4 KiB window,\n * so the DOCTYPE's own root name is the only reliable in-window signal.\n */\nconst DOCTYPE_SVG_ROOT = /^<!doctype\\s+svg(?:[\\s[>]|$)/;\n\n/**\n * Skips a leading XML prolog — an optional `<?xml …?>` declaration and any\n * number of `<!-- … -->` comments, in any order, plus surrounding ASCII\n * whitespace — and returns the remainder of `s` starting at the first real\n * markup token. Uses `indexOf` only (no regex backtracking), so it stays\n * linear-time and ReDoS-free on hostile input.\n *\n * If a construct is not closed within `s` (e.g. an oversized comment padded\n * past the 4 KiB scan window so its `-->` never appears in the head),\n * skipping stops and the still-open remainder is returned as-is; that\n * remainder then fails the `<!doctype svg` check and the buffer is left to\n * Sharp's own `meta.format === \"svg\"` guard (the documented defense-in-depth\n * layer — see the \"metadata-based Sharp guards\" tests).\n */\nconst skipXmlProlog = (s: string): string => {\n  let i = 0;\n  const skipWs = (): void => {\n    while (\n      i < s.length &&\n      (s[i] === \" \" || s[i] === \"\\t\" || s[i] === \"\\n\" || s[i] === \"\\r\")\n    ) {\n      i++;\n    }\n  };\n  for (;;) {\n    skipWs();\n    if (s.startsWith(\"<?xml\", i)) {\n      const end = s.indexOf(\"?>\", i);\n      if (end === -1) return s.slice(i);\n      i = end + 2;\n      continue;\n    }\n    if (s.startsWith(\"<!--\", i)) {\n      const end = s.indexOf(\"-->\", i);\n      if (end === -1) return s.slice(i);\n      i = end + 3;\n      continue;\n    }\n    return s.slice(i);\n  }\n};\n\n/**\n * Classifies an already-`trimStart()`ed, lowercased head string as SVG or\n * not. Shared by the latin1/UTF-8 and UTF-16 BOM decode paths so the two\n * cannot drift. Only ever runs its scans on a head that begins with a\n * recognized XML prolog, so it never inspects arbitrary raster bytes.\n *\n *  - A leading `<svg` root tag is conclusive.\n *  - Behind a recognized prolog (`<?xml`/`<!--`/`<!doctype`): an in-window\n *    `<svg[\\s>]` root tag is conclusive (the intentionally-conservative\n *    scan), AND — after skipping the leading `<?xml …?>`/comment prolog — a\n *    genuine top-level `<!DOCTYPE svg …>` declaration is conclusive even when\n *    an oversized entity-bomb DTD pushes the `<svg` root past the window.\n *    Anchoring the DOCTYPE check to the post-prolog position (rather than an\n *    unanchored substring search) avoids false-positiving a non-SVG document\n *    whose comment prose merely mentions the characters `<!doctype svg`.\n *  - Anything else is not SVG.\n */\nconst headLooksLikeSvg = (head: string): boolean => {\n  if (head.startsWith(\"<svg\")) return true;\n  if (\n    head.startsWith(\"<?xml\") ||\n    head.startsWith(\"<!--\") ||\n    head.startsWith(\"<!doctype\")\n  ) {\n    if (/<svg[\\s>]/.test(head)) return true;\n    return DOCTYPE_SVG_ROOT.test(skipXmlProlog(head));\n  }\n  return false;\n};\n\n/**\n * Detects whether a buffer is an SVG by inspecting its leading bytes for\n * common SVG / XML markers. Tolerates UTF-8 BOM, UTF-16 BE/LE BOMs, leading\n * ASCII whitespace (incl. whitespace BEFORE a BOM), `<?xml` prologs, and\n * `<!--` comments preceding the `<svg` root element. Reads up to 4 KiB so\n * pathologically large XML prologs cannot push `<svg` out of the inspection\n * window. A `<!DOCTYPE …>` prolog is recognized the same way — and when the\n * prolog names `svg` as the DOCTYPE's root element (`<!doctype svg`), that\n * alone is treated as conclusive, even if an oversized internal subset (a\n * billion-laughs entity-bomb DTD) pushes the `<svg` root past the 4 KiB\n * window. This DOCTYPE-root signal fires whether the buffer opens directly\n * with `<!doctype` OR the DOCTYPE sits behind an `<?xml …?>` declaration or\n * an XML comment (see `DOCTYPE_SVG_ROOT`), so the entity-bomb defense is not\n * limited to the bare-`<!doctype`-first shape.\n *\n * The detector is intentionally conservative — any buffer that looks even\n * vaguely SVG-shaped is rejected when `allowSvgInput` is false. This guards\n * against billion-laughs / nested-use SVG bombs that libvips/librsvg parses.\n */\nexport const looksLikeSvg = (buf: Buffer): boolean => {\n  if (!buf || buf.length === 0) return false;\n  let start = 0;\n  // Skip leading ASCII whitespace (tab, LF, CR, space) before checking BOMs.\n  // An attacker who prefixes a single 0x20 byte before the BOM previously\n  // bypassed the detector.\n  while (\n    start < buf.length &&\n    (buf[start] === 0x09 ||\n      buf[start] === 0x0a ||\n      buf[start] === 0x0d ||\n      buf[start] === 0x20)\n  ) {\n    start++;\n  }\n\n  // UTF-16 BE / LE BOMs — re-decode the head as UTF-16 then run the heuristic.\n  // UTF-16 SVGs are exotic but cheap to defend against.\n  if (\n    buf.length >= start + 2 &&\n    ((buf[start] === 0xfe && buf[start + 1] === 0xff) ||\n      (buf[start] === 0xff && buf[start + 1] === 0xfe))\n  ) {\n    const isLe = buf[start] === 0xff;\n    const sliceEnd = Math.min(buf.length, start + 2 + 4096);\n    // Node's `toString(\"utf16le\")` decodes LE bytes verbatim. For BE we swap\n    // bytes pair-wise into a temp buffer so the same decoder produces the\n    // intended characters.\n    let head16: string;\n    if (isLe) {\n      head16 = buf.subarray(start + 2, sliceEnd).toString(\"utf16le\");\n    } else {\n      const beSrc = buf.subarray(start + 2, sliceEnd);\n      const swapped = Buffer.alloc(beSrc.length - (beSrc.length % 2));\n      for (let i = 0; i + 1 < beSrc.length; i += 2) {\n        swapped[i] = beSrc[i + 1]!;\n        swapped[i + 1] = beSrc[i]!;\n      }\n      head16 = swapped.toString(\"utf16le\");\n    }\n    const trimmed = head16.trimStart().toLowerCase();\n    // Shared classifier — identical logic to the latin1/UTF-8 path below, so\n    // the two decode paths cannot drift. Gated on a recognized XML prolog,\n    // so a UTF-16 BOM-prefixed plain-text buffer that merely contains `<svg`\n    // (or the phrase `<!doctype svg`) without a real prolog/DOCTYPE is not\n    // over-blocked.\n    return headLooksLikeSvg(trimmed);\n  }\n\n  // UTF-8 BOM.\n  if (\n    buf.length >= start + 3 &&\n    buf[start] === 0xef &&\n    buf[start + 1] === 0xbb &&\n    buf[start + 2] === 0xbf\n  ) {\n    start += 3;\n  }\n\n  // Read up to 4 KiB (was 1 KiB) as latin1 to avoid utf8 decode cost; SVG is\n  // ASCII so latin1 round-trips every meaningful byte. The wider window\n  // covers pathological XML prologs that pad with comments / DOCTYPE before\n  // `<svg`.\n  const head = buf\n    .subarray(start, Math.min(buf.length, start + 4096))\n    .toString(\"latin1\")\n    .trimStart()\n    .toLowerCase();\n  return headLooksLikeSvg(head);\n};\n\n/**\n * @function serveImage\n * @description Processes and serves an image based on user data and options.\n * @param {Request} req - The Express request object.\n * @param {Response} res - The Express response object.\n * @param {NextFunction} next - The Express next function.\n * @param {ParsedOptions} parsedOptions - Already-validated options produced\n *   once by `registerServe`. The Zod schema parse is paid at factory time\n *   so the request hot path is purely arithmetic — see Task 4.\n * @param {string | undefined} cachedRealRoot - Optional pre-resolved\n *   realpath of `options.getUserFolderRootDir`, populated once by the\n *   middleware factory so per-request containment checks do not pay a\n *   fresh `fs.realpath` syscall on the root side.\n * @returns {Promise<void>}\n */\nconst serveImage = async (\n  req: Request,\n  res: Response,\n  next: NextFunction,\n  parsedOptions: ParsedOptions,\n  cachedRealRoot?: string,\n): Promise<void> => {\n  // Monotonic timestamp captured at the top of every request so the onComplete\n  // hook can report end-to-end pipeline latency regardless of which branch\n  // (200 happy path, 304 cached short-circuit, or fallback path) was taken.\n  const startedAt = process.hrtime.bigint();\n  let requestedType: ImageType = \"normal\";\n  // The schema parse already ran once at factory time, so `onError` and\n  // `onComplete` are already the validated function references. Aliased into\n  // locals so the outer catch (and the same-named helpers below) can read\n  // them without re-deriving the values on every request.\n  const onError: PixelServeOnError | undefined = parsedOptions.onError;\n  const onComplete: PixelServeOnComplete | undefined = parsedOptions.onComplete;\n  let observedSrc: string | undefined;\n  let observedUserId: string | undefined;\n  try {\n    let userData: ReturnType<typeof renderUserData>;\n    try {\n      // `req.query` is typed by Express as `ParsedQs` (recursive string /\n      // string[] / nested object). Pass through as `unknown` and let the\n      // Zod schema reject any shape that isn't a flat record of primitive\n      // strings/numbers — the schema preprocesses `src` to reject arrays\n      // (e.g., `?src[]=a&src[]=b`) with a clear error.\n      userData = renderUserData(req.query, {\n        minWidth: parsedOptions.minWidth,\n        maxWidth: parsedOptions.maxWidth,\n        minHeight: parsedOptions.minHeight,\n        maxHeight: parsedOptions.maxHeight,\n        defaultQuality: parsedOptions.defaultQuality,\n      });\n    } catch (err) {\n      reportError(onError, err, { phase: \"validation\" });\n      throw err;\n    }\n\n    observedSrc = userData.src;\n    observedUserId = userData.userId;\n    // userData.type is narrowed to `ImageType` by the schema enum default.\n    requestedType = userData.type ?? \"normal\";\n\n    let baseDir = parsedOptions.baseDir;\n    let parsedUserId: string | undefined;\n\n    if (userData.userId) {\n      parsedUserId = userData.userId;\n      if (parsedOptions.idHandler) {\n        const rawUserId = userData.userId;\n        const idTimeoutMs =\n          parsedOptions.idHandlerTimeoutMs ?? parsedOptions.requestTimeoutMs;\n        try {\n          const handlerResult = Promise.resolve().then(() =>\n            parsedOptions.idHandler!(rawUserId),\n          );\n          const candidate = await raceWithTimeout(\n            handlerResult,\n            idTimeoutMs,\n            \"idHandler\",\n          );\n          parsedUserId = typeof candidate === \"string\" ? candidate : rawUserId;\n          if (typeof candidate !== \"string\") {\n            reportError(\n              onError,\n              new Error(\n                `idHandler returned a non-string value (${typeof candidate})`,\n              ),\n              {\n                phase: \"idHandler\",\n                src: observedSrc,\n                userId: rawUserId,\n              },\n            );\n          }\n        } catch (err) {\n          // idHandler threw, rejected, or timed out — fall back to raw userId.\n          parsedUserId = rawUserId;\n          reportError(onError, err, {\n            phase: \"idHandler\",\n            src: observedSrc,\n            userId: rawUserId,\n          });\n        }\n        observedUserId = parsedUserId;\n      }\n    }\n\n    if (userData.folder === \"private\" && parsedOptions.getUserFolder) {\n      try {\n        // Wrap the invocation in `Promise.resolve().then(...)` so a\n        // synchronous throw from `getUserFolder` is captured as a rejection\n        // and routed through the timeout race + onError hook.\n        const folderPromise = Promise.resolve().then(() =>\n          parsedOptions.getUserFolder!(req, parsedUserId),\n        );\n        const dir = await raceWithTimeout(\n          folderPromise,\n          parsedOptions.requestTimeoutMs,\n          \"getUserFolder\",\n        );\n        if (dir) {\n          // When the user opts into `getUserFolderRootDir`, the framework\n          // validates that the returned path resolves to a descendant of the\n          // configured root via realpath + path.relative. If the path\n          // escapes (e.g., the user-supplied callback joined a malicious\n          // `../etc` userId, or a symlink redirects outside the tree), the\n          // resolver is treated as a failure: `onError` is invoked with\n          // `phase: \"getUserFolder\"` and the request falls back to the\n          // public `baseDir` configured on `PixelServeOptions`.\n          if (parsedOptions.getUserFolderRootDir) {\n            const inside = await isInsideRoot(\n              parsedOptions.getUserFolderRootDir,\n              dir,\n              cachedRealRoot,\n            );\n            if (!inside) {\n              reportError(\n                onError,\n                new Error(\n                  `getUserFolder returned path \"${dir}\" outside getUserFolderRootDir \"${parsedOptions.getUserFolderRootDir}\"`,\n                ),\n                {\n                  phase: \"getUserFolder\",\n                  src: observedSrc,\n                  userId: observedUserId,\n                },\n              );\n            } else {\n              baseDir = dir;\n            }\n          } else {\n            baseDir = dir;\n          }\n        }\n      } catch (err) {\n        // getUserFolder timed out or failed — use default baseDir\n        reportError(onError, err, {\n          phase: \"getUserFolder\",\n          src: observedSrc,\n          userId: observedUserId,\n        });\n      }\n    }\n\n    // `userData.format` is narrowed to `ImageFormat` by the schema — invalid\n    // formats coerce to `undefined`, the renderer fills in `\"jpeg\"`. The\n    // `allowedFormats.includes` check is defensive in case `allowedFormats`\n    // drifts away from the schema in the future.\n    const outputFormat: ImageFormat = allowedFormats.includes(userData.format)\n      ? userData.format\n      : \"jpeg\";\n\n    // ------------------------------------------------------------------\n    // Deterministic ETag: built BEFORE any Sharp work so `If-None-Match`\n    // can short-circuit decode + resize + re-encode entirely. The key\n    // combines every input that materially affects the response bytes\n    // (src, width, height, format, quality, type, folder, parsedUserId)\n    // plus a source identifier (mtime+size for local files, URL string\n    // for remote sources). Source-identifier failures degrade to \"no\n    // deterministic key available\" and the pipeline falls back to the\n    // legacy buffer hash (defense in depth).\n    // ------------------------------------------------------------------\n    // `buildSourceIdentifier` swallows its own filesystem errors and returns\n    // `null` when no stable key can be derived (missing file, etc.), so this\n    // call cannot throw and does not need its own try/catch. The options\n    // mirror what `resolveBuffer` below passes to `fetchImage`/\n    // `readLocalImage`, so the identifier this computes always matches the\n    // branch that will actually serve the bytes.\n    const sourceIdentifier = await buildSourceIdentifier(\n      userData.src,\n      baseDir,\n      {\n        websiteURL: parsedOptions.websiteURL,\n        apiRegex: parsedOptions.apiRegex,\n        apiPrefix: parsedOptions.apiPrefix,\n        maxBytes: parsedOptions.maxDownloadBytes,\n      },\n    );\n\n    let etag: string | undefined;\n    if (parsedOptions.etag && sourceIdentifier) {\n      etag = buildDeterministicEtag(\n        {\n          src: userData.src,\n          width: userData.width,\n          height: userData.height,\n          format: outputFormat,\n          quality: userData.quality,\n          type: userData.type,\n          folder: userData.folder,\n          parsedUserId,\n        },\n        sourceIdentifier,\n      );\n      if (req.headers[\"if-none-match\"] === etag) {\n        // Short-circuit BEFORE Sharp is touched at all. RFC 9110 §15.4.5: a\n        // 304 SHOULD echo the validators its 200 counterpart would have\n        // sent. This branch only ever matches a genuine deterministic ETag\n        // (a soft fallback always clears `etag`, so a client can never hold\n        // a deterministic ETag for a placeholder), so Cache-Control here is\n        // unconditionally the configured/default value.\n        res.setHeader(\"Vary\", \"Accept-Encoding\");\n        res.setHeader(\n          \"Cache-Control\",\n          parsedOptions.cacheControl ?? DEFAULT_CACHE_CONTROL,\n        );\n        res.setHeader(\"ETag\", etag);\n        res.status(304).end();\n        safeOnComplete(onComplete, {\n          src: observedSrc,\n          userId: observedUserId,\n          format: outputFormat,\n          outputBytes: 0,\n          cached: true,\n          durationMs: elapsedMs(startedAt),\n          // No bytes are sent on a 304 — there is nothing to characterize as\n          // fallback-or-not for this response.\n          fallback: false,\n        });\n        return;\n      }\n    }\n\n    // Set by `markSoftFallback` (threaded into `resolveBuffer` below) when\n    // the resolved buffer turned out to be a bundled placeholder rather than\n    // genuinely-resolved bytes (missing/invalid local file, blocked host,\n    // SSRF-reject, oversized file, transport failure, etc.) — a \"soft\"\n    // fallback that still flows through Sharp and gets re-encoded like any\n    // other image. Declared fresh on every `serveImage` invocation (never\n    // module- or factory-scoped) so concurrent requests cannot leak the mark\n    // between each other.\n    let servedSoftFallback = false;\n    const markSoftFallback = (): void => {\n      servedSoftFallback = true;\n    };\n\n    const resolveBuffer = async (): Promise<Buffer> => {\n      if (!userData.src) {\n        // userData.type is always present (schema defaults to \"normal\").\n        markSoftFallback();\n        return FALLBACKIMAGES[userData.type]();\n      }\n      if (\n        userData.src.startsWith(\"http://\") ||\n        userData.src.startsWith(\"https://\")\n      ) {\n        return fetchImage(\n          userData.src,\n          baseDir,\n          parsedOptions.websiteURL,\n          userData.type,\n          parsedOptions.apiRegex,\n          parsedOptions.allowedNetworkList,\n          {\n            timeoutMs: parsedOptions.requestTimeoutMs,\n            maxBytes: parsedOptions.maxDownloadBytes,\n            maxRedirects: parsedOptions.maxRedirects,\n            onError,\n            apiPrefix: parsedOptions.apiPrefix,\n            onFallback: markSoftFallback,\n          },\n        );\n      }\n      return readLocalImage(\n        userData.src,\n        baseDir,\n        userData.type,\n        parsedOptions.maxDownloadBytes,\n        onError,\n        markSoftFallback,\n      );\n    };\n\n    const imageBuffer = await resolveBuffer();\n\n    // A soft fallback served a bundled placeholder, not the requested bytes.\n    // Do not let it inherit the real-image cache profile: discard any\n    // source-derived deterministic ETag so the response is keyed on the\n    // actual placeholder bytes instead — the buffer-hash block below then\n    // runs unconditionally. Without this, a source that already had a\n    // pre-fetch deterministic identifier (e.g. any external URL, whose\n    // identifier is computed before the fetch is even attempted) would ship\n    // a stale ETag that names the *source*, not the placeholder that was\n    // actually sent, letting a future recovered fetch get permanently\n    // 304-locked onto the placeholder.\n    if (servedSoftFallback) {\n      etag = undefined;\n    }\n\n    if (!parsedOptions.allowSvgInput && looksLikeSvg(imageBuffer)) {\n      const err = new Error(\"svg input rejected\");\n      reportError(onError, err, {\n        phase: \"sharp\",\n        src: observedSrc,\n        userId: observedUserId,\n      });\n      throw err;\n    }\n\n    let processedImage: Buffer;\n    try {\n      let image = sharp(imageBuffer, {\n        failOn: \"warning\",\n        limitInputPixels: parsedOptions.maxInputPixels,\n        sequentialRead: true,\n        unlimited: false,\n      });\n\n      // Peek metadata first to avoid the expensive decode for hostile inputs.\n      const meta = await image.metadata();\n      if (meta.width && meta.height) {\n        if (meta.width * meta.height > parsedOptions.maxInputPixels) {\n          throw new Error(\"input exceeds maxInputPixels\");\n        }\n      }\n      if (!parsedOptions.allowSvgInput && meta.format === \"svg\") {\n        throw new Error(\"svg input rejected\");\n      }\n\n      // Re-instantiate Sharp because metadata() consumed the stream state.\n      image = sharp(imageBuffer, {\n        failOn: \"warning\",\n        limitInputPixels: parsedOptions.maxInputPixels,\n        sequentialRead: true,\n        unlimited: false,\n      }).rotate();\n\n      if (userData.width || userData.height) {\n        const resizeOptions: ResizeOptions = {\n          width: userData.width ?? undefined,\n          height: userData.height ?? undefined,\n          fit: sharp.fit.cover,\n          withoutEnlargement: true,\n        };\n        image = image.resize(resizeOptions);\n      }\n\n      processedImage = await image\n        .toFormat(outputFormat as keyof FormatEnum, {\n          quality: userData.quality,\n        })\n        .toBuffer();\n    } catch (err) {\n      reportError(onError, err, {\n        phase: \"sharp\",\n        src: observedSrc,\n        userId: observedUserId,\n      });\n      throw err;\n    }\n\n    // Fallback ETag: if no deterministic source identifier was available, OR\n    // the deterministic ETag was just discarded above because this response\n    // is a soft fallback, hash the processed buffer instead. This preserves\n    // the historical behavior for sources that cannot produce a stable key\n    // (e.g., missing file paths) and additionally keys every soft-fallback\n    // response on the placeholder bytes actually sent.\n    if (parsedOptions.etag && !etag) {\n      etag = `\"${createHash(\"sha256\").update(processedImage).digest(\"hex\")}\"`;\n      if (req.headers[\"if-none-match\"] === etag) {\n        // RFC 9110 §15.4.5: echo the same validators the 200 would have\n        // sent. Unlike the pre-Sharp 304 above, `servedSoftFallback` is\n        // already known here, so Cache-Control must track it too — otherwise\n        // a recurring placeholder (e.g. a still-missing local file) would get\n        // re-validated under the long-lived real-image policy instead of the\n        // short fallback one it was originally served with.\n        res.setHeader(\"Vary\", \"Accept-Encoding\");\n        res.setHeader(\n          \"Cache-Control\",\n          servedSoftFallback\n            ? FALLBACK_CACHE_CONTROL\n            : (parsedOptions.cacheControl ?? DEFAULT_CACHE_CONTROL),\n        );\n        res.setHeader(\"ETag\", etag);\n        res.status(304).end();\n        safeOnComplete(onComplete, {\n          src: observedSrc,\n          userId: observedUserId,\n          format: outputFormat,\n          outputBytes: 0,\n          cached: true,\n          durationMs: elapsedMs(startedAt),\n          // No bytes are sent on a 304 — there is nothing to characterize as\n          // fallback-or-not for this response, even if the resolved buffer\n          // (hashed above) happened to be a soft-fallback placeholder.\n          fallback: false,\n        });\n        return;\n      }\n    }\n\n    const { asciiFilename, encodedFilename } = buildFilename(\n      userData.src,\n      outputFormat,\n    );\n\n    res.type(mimeTypes[outputFormat]);\n    res.setHeader(\n      \"Content-Disposition\",\n      `inline; filename=\"${asciiFilename}\"; filename*=UTF-8''${encodedFilename}`,\n    );\n    res.setHeader(\"Vary\", \"Accept-Encoding\");\n    res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n    res.setHeader(\n      \"Cache-Control\",\n      servedSoftFallback\n        ? FALLBACK_CACHE_CONTROL\n        : (parsedOptions.cacheControl ?? DEFAULT_CACHE_CONTROL),\n    );\n    if (etag) {\n      res.setHeader(\"ETag\", etag);\n    }\n    res.setHeader(\"Content-Length\", processedImage.length.toString());\n    res.send(processedImage);\n    safeOnComplete(onComplete, {\n      src: observedSrc,\n      userId: observedUserId,\n      format: outputFormat,\n      outputBytes: processedImage.length,\n      cached: false,\n      durationMs: elapsedMs(startedAt),\n      fallback: servedSoftFallback,\n    });\n  } catch {\n    // If the success path already started flushing the response (e.g., a\n    // future streaming refactor calls `res.write` before `res.send`), we\n    // cannot recover into a fresh fallback without tripping\n    // ERR_HTTP_HEADERS_SENT. Surface to the Express error handler instead so\n    // the connection is torn down cleanly. Today the happy path only flushes\n    // via `res.send` at the very end, so this guard is defence-in-depth.\n    if (res.headersSent) {\n      const flushedError = new Error(\"response already flushed\");\n      reportError(onError, flushedError, {\n        phase: \"fs\",\n        src: observedSrc,\n        userId: observedUserId,\n      });\n      next(flushedError);\n      return;\n    }\n    try {\n      const fallbackType = requestedType === \"avatar\" ? \"avatar\" : \"normal\";\n      const fallback = await FALLBACKIMAGES[fallbackType]();\n      // The bundled fallback assets are pre-encoded and sent here VERBATIM\n      // (this error path deliberately skips Sharp re-encoding), so the\n      // response Content-Type and filename extension must match the asset\n      // actually served: the avatar fallback (`noavatar.png`) is a PNG while\n      // the normal fallback (`noimage.jpg`) is a JPEG. Hardcoding JPEG here\n      // mislabels the PNG avatar bytes as `image/jpeg`.\n      const fallbackFormat = fallbackType === \"avatar\" ? \"png\" : \"jpeg\";\n      res.type(mimeTypes[fallbackFormat]);\n      res.setHeader(\n        \"Content-Disposition\",\n        `inline; filename=\"fallback.${fallbackFormat}\"`,\n      );\n      res.setHeader(\"Vary\", \"Accept-Encoding\");\n      res.setHeader(\"X-Content-Type-Options\", \"nosniff\");\n      res.setHeader(\"Cache-Control\", FALLBACK_CACHE_CONTROL);\n      res.send(fallback);\n      // The hard-fallback path now fires onComplete too (fallback:true) so\n      // every response that resolves to a 200 fires the hook exactly once —\n      // previously this catch branch left onComplete silent entirely,\n      // leaving a consumer unable to distinguish \"no completion signal\n      // arrived\" from \"the pipeline is quietly serving 200s full of\n      // placeholder bytes.\"\n      safeOnComplete(onComplete, {\n        src: observedSrc,\n        userId: observedUserId,\n        format: fallbackFormat,\n        outputBytes: fallback.length,\n        cached: false,\n        durationMs: elapsedMs(startedAt),\n        fallback: true,\n      });\n    } catch (fallbackError) {\n      reportError(onError, fallbackError, {\n        phase: \"fs\",\n        src: observedSrc,\n        userId: observedUserId,\n      });\n      next(fallbackError);\n    }\n  }\n};\n\n/**\n * @function registerServe\n * @description A function to register the serveImage function as middleware for Express.\n * @param {PixelServeOptions} options - The options object for image processing.\n * @returns {function(Request, Response, NextFunction): Promise<void>} The middleware function.\n *\n * The factory eagerly validates `options` via `optionsSchema.parse` exactly\n * **once** at registration time (Task 4) so the request hot path does not\n * re-run the Zod schema, refine() callbacks, regex matches, or the\n * `allowedNetworkList` trim/lowercase transform on every request. Operator\n * misconfiguration is surfaced synchronously: the eagerly-captured\n * `options.onError` hook (if any) receives `{ phase: \"schema\" }` and the\n * factory re-throws so the failure is loud at startup rather than silent\n * fallback noise per-request.\n *\n * The factory also eagerly resolves `options.getUserFolderRootDir` via\n * `fs.realpath` once and caches the result. Every subsequent request reuses\n * the cached realpath inside `isInsideRoot`, so the per-request containment\n * check costs zero extra filesystem syscalls on the root side. When the\n * configured root does not yet exist on disk, the factory falls back to a\n * lexical `path.resolve` so the containment check still works for lazily-\n * created trees.\n */\nconst registerServe = (\n  options: PixelServeOptions,\n): ((req: Request, res: Response, next: NextFunction) => Promise<void>) => {\n  // Validate options exactly once at factory time. On failure, fire the\n  // eagerly-captured onError hook with `phase: \"schema\"` so operators that\n  // wired up observability still see the misconfiguration, then re-throw so\n  // the deployment fails loudly at startup rather than serving fallback\n  // images forever.\n  let parsedOptions: ParsedOptions;\n  try {\n    parsedOptions = renderOptions(options);\n  } catch (err) {\n    reportError(options.onError, err, { phase: \"schema\" });\n    throw err;\n  }\n\n  // Cached real path of the configured containment root. Populated lazily on\n  // the first request that needs it so the factory itself stays synchronous\n  // (no awaits at module-load time) and the cost is paid exactly once.\n  let cachedRealRoot: string | undefined;\n  let cacheResolved = false;\n  let pendingResolution: Promise<string> | undefined;\n\n  const ensureCachedRealRoot = async (rootDir: string): Promise<string> => {\n    if (cacheResolved && cachedRealRoot !== undefined) return cachedRealRoot;\n    // Coalesce concurrent first-request resolutions so a burst of N requests\n    // produces exactly one realpath syscall rather than N.\n    if (!pendingResolution) {\n      pendingResolution = resolveRootDir(rootDir).then((resolved) => {\n        cachedRealRoot = resolved;\n        cacheResolved = true;\n        return resolved;\n      });\n    }\n    return pendingResolution;\n  };\n\n  return async (\n    req: Request,\n    res: Response,\n    next: NextFunction,\n  ): Promise<void> => {\n    let rootForRequest: string | undefined;\n    if (parsedOptions.getUserFolderRootDir) {\n      rootForRequest = await ensureCachedRealRoot(\n        parsedOptions.getUserFolderRootDir,\n      );\n    }\n    return serveImage(req, res, next, parsedOptions, rootForRequest);\n  };\n};\n\nexport default registerServe;\n"]}