@osqd/bothandlerjs 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +227 -1
  2. package/README.md +18 -10
  3. package/dist/adapters/fastify.d.ts +10 -0
  4. package/dist/adapters/index.cjs +38 -10
  5. package/dist/adapters/index.cjs.map +1 -1
  6. package/dist/adapters/index.js +38 -10
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/challenge/index.d.ts +40 -0
  9. package/dist/cli.cjs +2256 -103
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +2256 -103
  12. package/dist/cli.js.map +1 -1
  13. package/dist/config.d.ts +55 -0
  14. package/dist/core.d.ts +48 -21
  15. package/dist/corpus/index.cjs +365 -7
  16. package/dist/corpus/index.cjs.map +1 -1
  17. package/dist/corpus/index.js +365 -7
  18. package/dist/corpus/index.js.map +1 -1
  19. package/dist/corpus/schema.d.ts +33 -0
  20. package/dist/crawler-ranges.d.ts +31 -0
  21. package/dist/dashboard/client/actions.d.ts +1 -1
  22. package/dist/dashboard/client/app.d.ts +9 -2
  23. package/dist/dashboard/client/boot.d.ts +32 -3
  24. package/dist/dashboard/client/query.d.ts +72 -12
  25. package/dist/dashboard/client/registry.d.ts +25 -0
  26. package/dist/dashboard/client/saved.d.ts +29 -0
  27. package/dist/dashboard/client/store.d.ts +16 -2
  28. package/dist/dashboard/client/types.d.ts +2 -0
  29. package/dist/dashboard/client.generated.d.ts +1 -1
  30. package/dist/dashboard/types.d.ts +15 -0
  31. package/dist/detectors/blended-identity.d.ts +34 -0
  32. package/dist/detectors/challenge-integrity.d.ts +26 -0
  33. package/dist/detectors/challenge-reaction.d.ts +39 -0
  34. package/dist/detectors/clearance.d.ts +1 -23
  35. package/dist/detectors/id-enumeration.d.ts +31 -0
  36. package/dist/detectors/index.d.ts +24 -1
  37. package/dist/detectors/known-bots.d.ts +11 -0
  38. package/dist/detectors/marker.d.ts +106 -0
  39. package/dist/detectors/parameter-sweep.d.ts +39 -0
  40. package/dist/detectors/probe-signature.d.ts +27 -0
  41. package/dist/detectors/probe-volume.d.ts +26 -0
  42. package/dist/detectors/site-baseline.d.ts +135 -0
  43. package/dist/detectors/target-integrity.d.ts +16 -0
  44. package/dist/detectors/transport-coherence.d.ts +31 -0
  45. package/dist/detectors/trap.d.ts +10 -3
  46. package/dist/detectors/types.d.ts +17 -0
  47. package/dist/element/index.cjs +730 -80
  48. package/dist/element/index.cjs.map +1 -1
  49. package/dist/element/index.js +730 -80
  50. package/dist/element/index.js.map +1 -1
  51. package/dist/index.cjs +2044 -123
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.d.ts +6 -2
  54. package/dist/index.js +2025 -123
  55. package/dist/index.js.map +1 -1
  56. package/dist/internal/async.d.ts +0 -3
  57. package/dist/internal/ip.d.ts +18 -0
  58. package/dist/internal/text.d.ts +28 -0
  59. package/dist/metrics.d.ts +18 -0
  60. package/dist/probe/index.d.ts +153 -0
  61. package/dist/probe/marker.d.ts +119 -0
  62. package/dist/site/index.d.ts +122 -0
  63. package/dist/state.d.ts +205 -0
  64. package/dist/stores/redis.d.ts +24 -1
  65. package/dist/types.d.ts +106 -0
  66. package/docs/course/05-detectors.md +9 -4
  67. package/docs/course/06-identity.md +1 -1
  68. package/docs/course/16-proving-it.md +15 -9
  69. package/docs/course/index.md +1 -1
  70. package/docs/design/decisions.md +1 -1
  71. package/docs/detection/correlation.md +284 -0
  72. package/docs/detection/detectors.md +259 -1
  73. package/docs/detection/index.md +2 -1
  74. package/docs/detection/shadow-mode.md +147 -0
  75. package/docs/detection/signatures.md +10 -2
  76. package/docs/index.md +3 -2
  77. package/docs/integration/client-ip.md +16 -0
  78. package/docs/operations/dashboard.md +40 -1
  79. package/docs/operations/filters.md +143 -0
  80. package/docs/operations/index.md +1 -0
  81. package/docs/operations/metrics.md +18 -0
  82. package/docs/policy/presets.md +1 -1
  83. package/docs/start/choosing-a-policy.md +1 -1
  84. package/docs/start/first-integration.md +1 -1
  85. package/docs/start/installation.md +2 -2
  86. package/docs/testing/cli.md +7 -1
  87. package/docs/testing/corpus.md +12 -8
  88. package/docs/testing/index.md +1 -1
  89. package/docs/testing/try-it.md +1 -1
  90. package/package.json +4 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/index.ts","../../src/internal/http.ts","../../src/internal/ip.ts","../../src/facts.ts","../../src/adapters/shared.ts","../../src/adapters/node.ts","../../src/adapters/fetch.ts","../../src/adapters/fastify.ts","../../src/adapters/koa.ts"],"sourcesContent":["export { botHandler } from \"./node.js\";\nexport type { NodeMiddleware, NodeMiddlewareOptions, NextFunction } from \"./node.js\";\nexport { createFetchAdapter, withBotHandler } from \"./fetch.js\";\nexport type { FetchAdapterOptions, FetchDecision } from \"./fetch.js\";\nexport { fastifyBotHandler } from \"./fastify.js\";\nexport type { FastifyAdapterOptions, FastifyLikeRequest, FastifyLikeReply } from \"./fastify.js\";\nexport { koaBotHandler } from \"./koa.js\";\nexport type { KoaLikeContext, KoaAdapterOptions } from \"./koa.js\";\n","/** Cookie parsing and serialisation, plus small header helpers. No dependencies. */\n\n/** Longest Cookie header we will parse. Beyond this it is a payload, not a session. */\nconst MAX_COOKIE_HEADER = 8192;\nconst MAX_COOKIES = 64;\n\n/**\n * Parses a `Cookie` header into a null-prototype bag.\n *\n * Null-prototype matters: a request carrying `__proto__=x` would otherwise hit the\n * prototype setter on a plain object and vanish, and any later `cookies.constructor`\n * lookup would find `Object`'s rather than `undefined`. Neither is a vulnerability by\n * itself; both are the kind of surprise that becomes one.\n */\nexport function parseCookies(header: string | undefined): Record<string, string> {\n const cookies: Record<string, string> = Object.create(null) as Record<string, string>;\n if (header === undefined || header.length === 0) return cookies;\n const source = header.length > MAX_COOKIE_HEADER ? header.slice(0, MAX_COOKIE_HEADER) : header;\n\n let count = 0;\n for (const pair of source.split(\";\")) {\n if (count >= MAX_COOKIES) break;\n const equals = pair.indexOf(\"=\");\n if (equals <= 0) continue;\n const name = pair.slice(0, equals).trim();\n if (name.length === 0) continue;\n const rawValue = pair.slice(equals + 1).trim();\n const value = rawValue.startsWith('\"') && rawValue.endsWith('\"') ? rawValue.slice(1, -1) : rawValue;\n try {\n cookies[name] = decodeURIComponent(value);\n } catch {\n // A malformed percent-escape is not worth rejecting the whole header over.\n cookies[name] = value;\n }\n count++;\n }\n return cookies;\n}\n\nexport interface CookieOptions {\n maxAgeMs?: number;\n path?: string;\n domain?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"Lax\" | \"Strict\" | \"None\";\n}\n\n/** Builds a `Set-Cookie` value. Rejects names and values that would let a caller inject attributes. */\nexport function serializeCookie(name: string, value: string, options: CookieOptions = {}): string {\n if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(name)) throw new TypeError(`Invalid cookie name: ${name}`);\n const parts = [`${name}=${encodeURIComponent(value)}`];\n if (options.maxAgeMs !== undefined) parts.push(`Max-Age=${Math.floor(options.maxAgeMs / 1000)}`);\n parts.push(`Path=${options.path ?? \"/\"}`);\n if (options.domain !== undefined) {\n if (!/^[A-Za-z0-9.-]+$/.test(options.domain)) throw new TypeError(`Invalid cookie domain: ${options.domain}`);\n parts.push(`Domain=${options.domain}`);\n }\n if (options.secure !== false) parts.push(\"Secure\");\n if (options.httpOnly !== false) parts.push(\"HttpOnly\");\n // `SameSite=None` without `Secure` is rejected by browsers, so it is never a valid\n // combination to emit — fail loudly here rather than shipping a cookie nothing keeps.\n const sameSite = options.sameSite ?? \"Lax\";\n if (sameSite === \"None\" && options.secure === false) {\n throw new TypeError(\"SameSite=None requires Secure\");\n }\n parts.push(`SameSite=${sameSite}`);\n return parts.join(\"; \");\n}\n\n/** Joins multi-value headers the way the rest of the library expects to see them. */\nexport function joinHeaderValue(value: string | string[] | undefined): string | undefined {\n if (value === undefined) return undefined;\n return Array.isArray(value) ? value.join(\", \") : value;\n}\n","/**\n * IP parsing, normalisation and CIDR matching, with no dependencies.\n *\n * Everything works on raw byte arrays rather than strings, because string\n * comparison of IPs is a classic source of bypasses: `::ffff:127.0.0.1`,\n * `0177.0.0.1` and `127.0.0.001` are all the same host to the network stack but\n * three different strings to a naive allowlist.\n */\n\n/** An IP as bytes: 4 for IPv4, 16 for IPv6. IPv4-mapped IPv6 is folded to 4. */\nexport type IpBytes = Uint8Array;\n\n/**\n * Parses an IPv4 or IPv6 literal into bytes, or `null` if it is not a valid\n * address. Deliberately strict: no octal, no hex, no shorthand octets, no zone\n * ids. Anything ambiguous is rejected rather than guessed at.\n */\nexport function parseIp(value: string): IpBytes | null {\n const input = value.trim();\n if (input.length === 0 || input.length > 45) return null;\n // Bracketed form from a Host/Forwarded header: [2001:db8::1]\n const bare = input.startsWith(\"[\") && input.endsWith(\"]\") ? input.slice(1, -1) : input;\n if (bare.includes(\":\")) return parseIpv6(bare);\n return parseIpv4(bare);\n}\n\nfunction parseIpv4(value: string): IpBytes | null {\n const parts = value.split(\".\");\n if (parts.length !== 4) return null;\n const bytes = new Uint8Array(4);\n for (let i = 0; i < 4; i++) {\n const part = parts[i]!;\n // Reject empty, over-long, non-digit and leading-zero forms outright.\n if (part.length === 0 || part.length > 3) return null;\n if (!/^\\d+$/.test(part)) return null;\n if (part.length > 1 && part[0] === \"0\") return null;\n const n = Number(part);\n if (n > 255) return null;\n bytes[i] = n;\n }\n return bytes;\n}\n\nfunction parseIpv6(value: string): IpBytes | null {\n // Strip a zone id (`%eth0`) — it is a local routing detail, not part of identity.\n const withoutZone = value.split(\"%\")[0]!;\n const doubleColon = withoutZone.indexOf(\"::\");\n if (doubleColon !== withoutZone.lastIndexOf(\"::\")) return null;\n\n const [headText, tailText] =\n doubleColon === -1\n ? [withoutZone, \"\"]\n : [withoutZone.slice(0, doubleColon), withoutZone.slice(doubleColon + 2)];\n\n const head = headText.length > 0 ? headText.split(\":\") : [];\n const tail = tailText.length > 0 ? tailText.split(\":\") : [];\n\n // A trailing dotted-quad (`::ffff:192.0.2.1`) occupies the last two groups.\n let embedded: IpBytes | null = null;\n const groups = [...head, ...tail];\n const last = groups[groups.length - 1];\n if (last !== undefined && last.includes(\".\")) {\n embedded = parseIpv4(last);\n if (!embedded) return null;\n if (tail.length > 0) tail.pop();\n else head.pop();\n }\n\n const groupCount = head.length + tail.length + (embedded ? 2 : 0);\n if (doubleColon === -1 ? groupCount !== 8 : groupCount > 7) return null;\n\n const bytes = new Uint8Array(16);\n let offset = 0;\n for (const group of head) {\n if (!writeGroup(bytes, offset, group)) return null;\n offset += 2;\n }\n // The gap the `::` stands for, left as the zeroes the array already holds.\n offset = 16 - tail.length * 2 - (embedded ? 4 : 0);\n for (const group of tail) {\n if (!writeGroup(bytes, offset, group)) return null;\n offset += 2;\n }\n if (embedded) bytes.set(embedded, 12);\n\n // Fold IPv4-mapped (::ffff:0:0/96) down to a plain v4 address so that one host\n // has exactly one representation regardless of which stack accepted it.\n if (isIpv4Mapped(bytes)) return bytes.slice(12, 16);\n return bytes;\n}\n\nfunction writeGroup(bytes: Uint8Array, offset: number, group: string): boolean {\n if (group.length === 0 || group.length > 4 || !/^[0-9a-fA-F]+$/.test(group)) return false;\n const n = Number.parseInt(group, 16);\n bytes[offset] = n >> 8;\n bytes[offset + 1] = n & 0xff;\n return true;\n}\n\nfunction isIpv4Mapped(bytes: Uint8Array): boolean {\n for (let i = 0; i < 10; i++) if (bytes[i] !== 0) return false;\n return bytes[10] === 0xff && bytes[11] === 0xff;\n}\n\n/** Canonical string form, so the same host always produces the same actor key. */\nexport function formatIp(bytes: IpBytes): string {\n if (bytes.length === 4) return `${bytes[0]}.${bytes[1]}.${bytes[2]}.${bytes[3]}`;\n const groups: string[] = [];\n for (let i = 0; i < 16; i += 2) groups.push(((bytes[i]! << 8) | bytes[i + 1]!).toString(16));\n // RFC 5952: compress the longest run of zero groups, leftmost on a tie.\n let bestStart = -1;\n let bestLen = 0;\n for (let i = 0; i < 8; i++) {\n if (groups[i] !== \"0\") continue;\n let j = i;\n while (j < 8 && groups[j] === \"0\") j++;\n if (j - i > bestLen) {\n bestLen = j - i;\n bestStart = i;\n }\n i = j;\n }\n if (bestLen < 2) return groups.join(\":\");\n return `${groups.slice(0, bestStart).join(\":\")}::${groups.slice(bestStart + bestLen).join(\":\")}`;\n}\n\n/** Normalises any accepted spelling of an address to its canonical form. Returns `null` if unparseable. */\nexport function normalizeIp(value: string): string | null {\n const bytes = parseIp(value);\n return bytes ? formatIp(bytes) : null;\n}\n\nexport interface Cidr {\n readonly bytes: IpBytes;\n readonly prefix: number;\n readonly source: string;\n}\n\n/** Parses `\"10.0.0.0/8\"`, `\"2001:db8::/32\"`, or a bare address (treated as a /32 or /128). */\nexport function parseCidr(value: string): Cidr | null {\n const slash = value.lastIndexOf(\"/\");\n const addressText = slash === -1 ? value : value.slice(0, slash);\n const bytes = parseIp(addressText);\n if (!bytes) return null;\n const maxPrefix = bytes.length * 8;\n if (slash === -1) return { bytes, prefix: maxPrefix, source: value };\n const prefixText = value.slice(slash + 1);\n if (!/^\\d{1,3}$/.test(prefixText)) return null;\n const prefix = Number(prefixText);\n if (prefix > maxPrefix) return null;\n return { bytes, prefix, source: value };\n}\n\n/** True when `ip` falls inside `cidr`. Address families never match across each other. */\nexport function cidrContains(cidr: Cidr, ip: IpBytes): boolean {\n if (cidr.bytes.length !== ip.length) return false;\n const fullBytes = cidr.prefix >> 3;\n for (let i = 0; i < fullBytes; i++) if (cidr.bytes[i] !== ip[i]) return false;\n const remainder = cidr.prefix & 7;\n if (remainder === 0) return true;\n const mask = 0xff << (8 - remainder);\n return (cidr.bytes[fullBytes]! & mask) === (ip[fullBytes]! & mask);\n}\n\n/**\n * One range plus the position it was added at, so that when several ranges match the\n * same address the answer does not depend on how the index happens to be laid out.\n */\ninterface IndexedCidr {\n readonly cidr: Cidr;\n readonly order: number;\n}\n\n/**\n * Ranges for one address family, indexed on the first two bytes.\n *\n * A linear scan is fine for the handful of CIDRs in an allowlist and catastrophic for\n * the list this library actually asks operators to supply: AWS publishes around seven\n * thousand IPv4 prefixes, GCP and Azure comparable numbers, and `datacenterRanges` is\n * documented as the place to put them. Scanning that on every request measured at\n * ~87µs — several times the cost of the entire rest of an assessment — so the size of\n * a range list silently became the dominant term in request latency.\n *\n * Bucketing by the leading bytes fixes that without changing any answer. A prefix of\n * /16 or longer pins both leading bytes, so it is reachable from exactly one bucket;\n * a /8 to /15 pins only the first; anything shorter than /8 spans buckets and stays in\n * a list checked on every lookup. Real range lists are overwhelmingly /16 and longer,\n * which is why this collapses to a handful of comparisons.\n */\nclass FamilyIndex {\n /** Prefix < 8. Spans first bytes, so it is always scanned. Realistically empty. */\n private readonly wide: IndexedCidr[] = [];\n private readonly buckets = new Map<number, { mid: IndexedCidr[]; deep: Map<number, IndexedCidr[]> }>();\n\n add(entry: IndexedCidr): void {\n const { cidr } = entry;\n if (cidr.prefix < 8) {\n this.wide.push(entry);\n return;\n }\n const first = cidr.bytes[0]!;\n let bucket = this.buckets.get(first);\n if (bucket === undefined) {\n bucket = { mid: [], deep: new Map() };\n this.buckets.set(first, bucket);\n }\n if (cidr.prefix < 16) {\n bucket.mid.push(entry);\n return;\n }\n const second = cidr.bytes[1]!;\n let deep = bucket.deep.get(second);\n if (deep === undefined) {\n deep = [];\n bucket.deep.set(second, deep);\n }\n deep.push(entry);\n }\n\n /** The earliest-added range containing `ip`, or `undefined`. */\n find(ip: IpBytes): IndexedCidr | undefined {\n let best = scan(this.wide, ip, undefined);\n const bucket = this.buckets.get(ip[0]!);\n if (bucket !== undefined) {\n best = scan(bucket.mid, ip, best);\n const deep = bucket.deep.get(ip[1]!);\n if (deep !== undefined) best = scan(deep, ip, best);\n }\n return best;\n }\n}\n\nfunction scan(entries: readonly IndexedCidr[], ip: IpBytes, best: IndexedCidr | undefined): IndexedCidr | undefined {\n let winner = best;\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i]!;\n if (winner !== undefined && entry.order > winner.order) continue;\n if (cidrContains(entry.cidr, ip)) winner = entry;\n }\n return winner;\n}\n\n/**\n * A compiled set of CIDR ranges. Building it once and reusing it keeps the hot\n * path free of string parsing — matching is a handful of byte comparisons, and stays\n * that way as the list grows into the thousands.\n */\nexport class IpRangeSet {\n private readonly v4 = new FamilyIndex();\n private readonly v6 = new FamilyIndex();\n private count = 0;\n /**\n * Every range that parsed, in the order it was added.\n *\n * Kept as plain text beside the index rather than reconstructed from it. The index\n * is shaped for lookups — bucketed by leading byte, prefix-split — and walking it\n * back into a list would be both slower and, for anything that wants to *show* the\n * set, wrong: what an operator recognises is the string they wrote, not a normalised\n * form of it. These sets are configuration-sized, so the array costs nothing.\n */\n private readonly sources: string[] = [];\n /** Ranges that failed to parse, surfaced so a typo in config is loud rather than silent. */\n readonly invalid: string[] = [];\n\n constructor(ranges: Iterable<string> = []) {\n for (const range of ranges) this.add(range);\n }\n\n add(range: string): void {\n const cidr = parseCidr(range);\n if (!cidr) {\n this.invalid.push(range);\n return;\n }\n this.sources.push(cidr.source);\n (cidr.bytes.length === 4 ? this.v4 : this.v6).add({ cidr, order: this.count++ });\n }\n\n get size(): number {\n return this.count;\n }\n\n /** The ranges in this set, as written, oldest first. What a reader can act on. */\n entries(): readonly string[] {\n return this.sources;\n }\n\n /**\n * Returns the matching range's original text, or `undefined`. Useful for explaining\n * a decision. When several ranges match, the one added first wins, so the\n * explanation does not depend on the index's internal layout.\n */\n match(ip: string | IpBytes): string | undefined {\n const bytes = typeof ip === \"string\" ? parseIp(ip) : ip;\n if (!bytes) return undefined;\n return (bytes.length === 4 ? this.v4 : this.v6).find(bytes)?.cidr.source;\n }\n\n contains(ip: string | IpBytes): boolean {\n const bytes = typeof ip === \"string\" ? parseIp(ip) : ip;\n if (!bytes) return false;\n return (bytes.length === 4 ? this.v4 : this.v6).find(bytes) !== undefined;\n }\n}\n\n/** IANA special-purpose ranges: loopback, private, link-local, CGNAT, documentation. */\nexport const SPECIAL_USE_RANGES = [\n \"0.0.0.0/8\",\n \"10.0.0.0/8\",\n \"100.64.0.0/10\",\n \"127.0.0.0/8\",\n \"169.254.0.0/16\",\n \"172.16.0.0/12\",\n \"192.0.0.0/24\",\n \"192.0.2.0/24\",\n \"192.168.0.0/16\",\n \"198.18.0.0/15\",\n \"198.51.100.0/24\",\n \"203.0.113.0/24\",\n \"224.0.0.0/4\",\n \"240.0.0.0/4\",\n \"::1/128\",\n \"fc00::/7\",\n \"fe80::/10\",\n \"2001:db8::/32\",\n] as const;\n\nconst SPECIAL_USE = new IpRangeSet(SPECIAL_USE_RANGES);\n\n/** True for loopback/private/link-local/documentation addresses — never public clients. */\nexport function isSpecialUse(ip: string): boolean {\n return SPECIAL_USE.contains(ip);\n}\n\n/**\n * Masks an address to a coarse network for rate accounting: /24 for IPv4, /64 for\n * IPv6. IPv6 clients routinely get a whole /64 to themselves and rotate the host\n * bits freely, so per-address counting is trivially defeated there.\n */\nexport function networkKey(ip: string): string {\n const bytes = parseIp(ip);\n if (!bytes) return ip;\n // Build the masked address at full width. Slicing to three bytes and formatting\n // that would produce a 3-byte array, which `formatIp` reads as IPv6.\n if (bytes.length === 4) {\n const masked = new Uint8Array([bytes[0]!, bytes[1]!, bytes[2]!, 0]);\n return `${formatIp(masked)}/24`;\n }\n const masked = new Uint8Array(16);\n masked.set(bytes.subarray(0, 8), 0);\n return `${formatIp(masked)}/64`;\n}\n","import { joinHeaderValue, parseCookies } from \"./internal/http.js\";\nimport { normalizeIp } from \"./internal/ip.js\";\nimport type { RequestFacts } from \"./types.js\";\n\n/** Longest URL we will parse. Anything beyond this is a payload, not a path. */\nconst MAX_URL_LENGTH = 8192;\n/** Cap on query parameters kept. */\nconst MAX_QUERY_PARAMS = 64;\n\nexport interface FactsInput {\n method?: string | undefined;\n /** Request target, path plus optional query — what `req.url` gives you. */\n url?: string | undefined;\n /** Raw header map. Values may be arrays; names may be any case. */\n headers: Record<string, string | string[] | undefined>;\n /**\n * Header names in wire order. Node exposes them via `req.rawHeaders` (alternating\n * name/value) — pass that array directly, or a name-only list.\n */\n rawHeaders?: readonly string[] | undefined;\n /** Socket address. Pass the *socket's* address; forwarding is resolved separately. */\n ip: string;\n timestamp?: number | undefined;\n protocol?: \"http\" | \"https\" | undefined;\n /**\n * The HTTP version of the connection **this process accepted** — Node's\n * `request.httpVersion`, not the version the client negotiated with your edge.\n *\n * The distinction has teeth. HTTP/2 forbids connection-specific headers, and\n * `header-integrity` treats one as a deterministic protocol violation. If you set\n * this from a forwarded header while the request itself arrived over HTTP/1.1 from\n * a proxy — which adds `Connection: keep-alive` — you will manufacture that\n * violation for every real browser behind that proxy, and it is a `certain` verdict,\n * so it can block. Report the connection you actually have, or leave it unset.\n */\n httpVersion?: string | undefined;\n tlsFingerprint?: string | undefined;\n /** See {@link RequestFacts.partialHeaders}. Set it when the source cannot supply every header. */\n partialHeaders?: boolean | undefined;\n extra?: Record<string, unknown> | undefined;\n}\n\n/**\n * Reduces a request to the facts detectors are allowed to see.\n *\n * Normalisation happens exactly once, here, and every detector reads the result. That\n * is partly performance — lowercasing a header map per detector would be absurd — but\n * mostly correctness: if one detector reads `req.headers['User-Agent']` and another\n * reads `req.headers['user-agent']`, they will eventually disagree about the same\n * request, and the bug will be invisible.\n *\n * Every field is bounded. Each one is attacker-controlled, and this runs on every\n * request to your site.\n */\nexport function createFacts(input: FactsInput): RequestFacts {\n const rawUrl = input.url ?? \"/\";\n const url = rawUrl.length > MAX_URL_LENGTH ? rawUrl.slice(0, MAX_URL_LENGTH) : rawUrl;\n const queryStart = url.indexOf(\"?\");\n const rawPath = queryStart === -1 ? url : url.slice(0, queryStart);\n\n const headers: Record<string, string | undefined> = Object.create(null) as Record<string, string | undefined>;\n for (const [name, value] of Object.entries(input.headers)) {\n const joined = joinHeaderValue(value);\n if (joined !== undefined) headers[name.toLowerCase()] = joined;\n }\n\n const facts: RequestFacts = {\n method: (input.method ?? \"GET\").toUpperCase(),\n path: normalizePath(rawPath),\n query: parseQuery(queryStart === -1 ? \"\" : url.slice(queryStart + 1)),\n headers,\n headerOrder: extractOrder(input.rawHeaders, headers),\n ip: normalizeIp(input.ip) ?? input.ip,\n timestamp: input.timestamp ?? Date.now(),\n };\n\n // Only parse cookies that exist. Most bot traffic carries none, and building an\n // empty bag for every one of those requests is pure garbage.\n const cookieHeader = headers[\"cookie\"];\n if (cookieHeader !== undefined) facts.cookies = parseCookies(cookieHeader);\n\n if (input.protocol !== undefined) facts.protocol = input.protocol;\n if (input.httpVersion !== undefined) facts.httpVersion = input.httpVersion;\n if (input.tlsFingerprint !== undefined) facts.tlsFingerprint = input.tlsFingerprint;\n if (input.partialHeaders === true) facts.partialHeaders = true;\n if (input.extra !== undefined) facts.extra = input.extra;\n\n return facts;\n}\n\n/**\n * Decodes and normalises a path.\n *\n * Rule matching is done on this value, so `/admin`, `/%61dmin` and `/./admin` must\n * not be three different paths as far as a policy is concerned — otherwise a rule\n * scoped to a path is trivially side-stepped by spelling it differently. Decoding is\n * single-pass: repeatedly decoding until it stops changing is how `%2525` becomes `%`\n * and how path-traversal filters get bypassed.\n */\nfunction normalizePath(rawPath: string): string {\n let path = rawPath;\n try {\n path = decodeURIComponent(rawPath);\n } catch {\n // Malformed percent-encoding. Keep the raw form: it is still a fact about the\n // request, and guessing at an intended decoding would be inventing one.\n }\n path = path.replace(/\\\\/g, \"/\").replace(/\\/{2,}/g, \"/\");\n if (!path.startsWith(\"/\")) path = `/${path}`;\n\n // Resolve `.` and `..` segments so a rule on a path prefix cannot be walked around.\n if (path.includes(\"./\")) {\n const resolved: string[] = [];\n for (const segment of path.split(\"/\")) {\n if (segment === \"\" || segment === \".\") continue;\n if (segment === \"..\") resolved.pop();\n else resolved.push(segment);\n }\n path = `/${resolved.join(\"/\")}`;\n }\n return path.length > 1 && path.endsWith(\"/\") ? path.slice(0, -1) : path;\n}\n\nfunction parseQuery(search: string): Record<string, string> {\n // Null-prototype: a literal `?__proto__=x` becomes an ordinary own key instead of\n // hitting the prototype setter and vanishing, so a detector can actually see it.\n const query: Record<string, string> = Object.create(null) as Record<string, string>;\n if (search.length === 0) return query;\n let count = 0;\n for (const [key, value] of new URLSearchParams(search)) {\n if (count++ >= MAX_QUERY_PARAMS) break;\n query[key] = value.length > 1024 ? value.slice(0, 1024) : value;\n }\n return query;\n}\n\n/**\n * Extracts wire-order header names.\n *\n * Both documented input shapes have to be told apart: Node's `rawHeaders`, which\n * alternates name and value, and a plain list of names.\n *\n * Asking whether the even-indexed entries *look like* header names cannot do it, and\n * used to get the common case backwards. Every entry of a name-only list looks like a\n * header name, so any such list of even length was read as Node-style and every second\n * name was discarded — silently halving the header order, which is a fingerprint, for\n * a shape the documentation invites callers to pass.\n *\n * The header map decides it instead. In a name-only list every entry is a header that\n * was actually received; in an alternating list the odd entries are values, which are\n * almost never also header names. That distinguishes the two even for a single-header\n * request, where any shape-based guess is ambiguous.\n */\nconst MAX_ORDERED_HEADERS = 64;\n\nfunction extractOrder(rawHeaders: readonly string[] | undefined, headers: Record<string, string | undefined>): readonly string[] {\n if (!rawHeaders || rawHeaders.length === 0) return EMPTY_ORDER;\n\n let isNodeStyle = rawHeaders.length % 2 === 0;\n if (isNodeStyle) {\n let everyEntryIsAHeader = true;\n for (let i = 0; i < rawHeaders.length; i++) {\n const entry = rawHeaders[i]!;\n const known = headers[entry.toLowerCase()] !== undefined;\n // An even entry that is neither a received header nor even a valid token cannot\n // be a name in either shape; fall back to reading the list as names rather than\n // dropping half of something unrecognised.\n if (i % 2 === 0 && !known && !isHeaderName(entry)) {\n isNodeStyle = false;\n break;\n }\n if (!known) everyEntryIsAHeader = false;\n }\n if (everyEntryIsAHeader) isNodeStyle = false;\n }\n\n // One pass, one array. The previous version walked the input three times —\n // `every`, then `filter`, then `slice().map()` — allocating at each step.\n const step = isNodeStyle ? 2 : 1;\n const order: string[] = [];\n for (let i = 0; i < rawHeaders.length && order.length < MAX_ORDERED_HEADERS; i += step) {\n order.push(rawHeaders[i]!.toLowerCase());\n }\n return order;\n}\n\nconst EMPTY_ORDER: readonly string[] = Object.freeze([]);\n\n/** RFC 9110 token characters. A scan rather than a regex: this runs once per header. */\nfunction isHeaderName(value: string): boolean {\n if (value.length === 0) return false;\n for (let i = 0; i < value.length; i++) {\n const code = value.charCodeAt(i);\n const ok =\n (code >= 0x30 && code <= 0x39) || // 0-9\n (code >= 0x41 && code <= 0x5a) || // A-Z\n (code >= 0x61 && code <= 0x7a) || // a-z\n code === 0x21 || (code >= 0x23 && code <= 0x27) || code === 0x2a || code === 0x2b ||\n code === 0x2d || code === 0x2e || code === 0x5e || code === 0x5f || code === 0x60 ||\n code === 0x7c || code === 0x7e;\n if (!ok) return false;\n }\n return true;\n}\n","import type { IncomingMessage } from \"node:http\";\nimport type { ActionOutcome } from \"../actions/types.js\";\n\n/** Largest challenge-solution body we will read. A solution is a few hundred bytes. */\nexport const MAX_VERIFY_BODY = 4096;\n\n/**\n * Waits, with the timer unreferenced.\n *\n * The `unref` is what stops a pending `delay` action from holding a process open at\n * shutdown — which in a serverless runtime is the difference between a request that\n * finishes and an invocation that is billed until it is killed.\n */\nexport function pause(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n (timer as { unref?: () => void }).unref?.();\n });\n}\n\n/** True when the outcome means the request should reach the application. */\nexport function isContinue(outcome: ActionOutcome): outcome is Extract<ActionOutcome, { kind: \"continue\" }> {\n return outcome.kind === \"continue\";\n}\n\n/**\n * Parses a JSON body defensively.\n *\n * Returns `undefined` rather than throwing on anything malformed. This parses input\n * from a client that is, by construction, already under suspicion.\n */\nexport function parseJson(text: string): unknown {\n if (text.length === 0 || text.length > MAX_VERIFY_BODY) return undefined;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Reads at most {@link MAX_VERIFY_BODY} bytes from a Node request stream.\n *\n * The cap is enforced by destroying the socket rather than by buffering and then\n * discarding: a client that keeps sending after the limit is spending its own\n * bandwidth against a buffer that stopped growing. Errors resolve to an empty string,\n * because a malformed submission is a rejected challenge, not an exception.\n */\nexport function readBoundedBody(request: IncomingMessage): Promise<string> {\n return new Promise((resolve) => {\n let data = \"\";\n let bytes = 0;\n request.on(\"data\", (chunk: Buffer) => {\n bytes += chunk.length;\n if (bytes > MAX_VERIFY_BODY) {\n request.destroy();\n return;\n }\n data += chunk.toString(\"utf8\");\n });\n request.on(\"end\", () => resolve(data));\n request.on(\"error\", () => resolve(\"\"));\n });\n}\n\n/** The JSON body of a challenge verification response, shared by every adapter. */\nexport function verificationBody(outcome: { ok: true } | { ok: false; reason: string }): string {\n return JSON.stringify(outcome.ok ? { ok: true } : { ok: false, error: outcome.reason });\n}\n","import { createFacts } from \"../facts.js\";\nimport { pause, parseJson, readBoundedBody, verificationBody } from \"./shared.js\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { BotHandler } from \"../core.js\";\nimport type { RequestFacts } from \"../types.js\";\n\nexport type NextFunction = (error?: unknown) => void;\nexport type NodeMiddleware = (request: IncomingMessage, response: ServerResponse, next: NextFunction) => void;\n\nexport interface NodeMiddlewareOptions {\n /**\n * Extracts a TLS fingerprint your edge computed, e.g. `\"x-ja3-hash\"` or\n * Cloudflare's `\"cf-ja3-hash\"`. Only set it if the header cannot be forged by a\n * client — that is, your edge always overwrites it.\n */\n tlsFingerprintHeader?: string;\n /** Also handle the challenge verification endpoint. Default true. */\n mountChallengeEndpoint?: boolean;\n /** Extra facts, e.g. a session id for a better actor key. */\n enrich?: (request: IncomingMessage, facts: RequestFacts) => RequestFacts;\n}\n\n/**\n * Connect/Express middleware. Also works with a bare `node:http` server.\n *\n * Mount it early — ahead of your routes, behind anything that terminates TLS and\n * sets forwarded headers — and after your body parser only if you need one, since\n * this reads no body except on its own verification endpoint.\n *\n * `next()` is always reached unless a response was actually produced, and any\n * unexpected error inside the engine results in the request being served rather than\n * refused. A bot filter that fails closed is an outage with extra steps.\n */\nexport function botHandler(handler: BotHandler, options: NodeMiddlewareOptions = {}): NodeMiddleware {\n const mountChallenge = options.mountChallengeEndpoint ?? true;\n\n return function botHandlerMiddleware(request, response, next): void {\n let handedOff = false;\n void (async (): Promise<void> => {\n try {\n const headers: Record<string, string | undefined> = {};\n for (const [name, value] of Object.entries(request.headers)) {\n headers[name] = Array.isArray(value) ? value.join(\", \") : value;\n }\n\n const ip = handler.resolveIp(request.socket.remoteAddress, headers);\n const fingerprint = options.tlsFingerprintHeader !== undefined ? headers[options.tlsFingerprintHeader.toLowerCase()] : undefined;\n\n let facts = createFacts({\n method: request.method,\n url: request.url,\n headers: request.headers,\n rawHeaders: request.rawHeaders,\n ip,\n httpVersion: request.httpVersion,\n // `encrypted` is present only on a TLS socket. Behind a terminating proxy\n // the forwarded protocol header is the only truth available.\n protocol: (request.socket as { encrypted?: boolean }).encrypted === true || headers[\"x-forwarded-proto\"] === \"https\" ? \"https\" : \"http\",\n ...(fingerprint !== undefined ? { tlsFingerprint: fingerprint } : {}),\n });\n if (options.enrich) facts = options.enrich(request, facts);\n\n if (mountChallenge && handler.isChallengeEndpoint(facts)) {\n await serveVerification(handler, request, response, facts);\n return;\n }\n\n const { outcome } = await handler.handle(facts);\n\n if (outcome.kind === \"drop\") {\n request.socket.destroy();\n return;\n }\n\n if (outcome.kind === \"respond\") {\n response.statusCode = outcome.status;\n for (const [name, value] of Object.entries(outcome.headers)) response.setHeader(name, value);\n response.end(outcome.body);\n return;\n }\n\n for (const [name, value] of Object.entries(outcome.requestHeaders ?? {})) {\n request.headers[name] = value;\n }\n if (outcome.responseHeaders) {\n for (const [name, value] of Object.entries(outcome.responseHeaders)) response.setHeader(name, value);\n }\n if (outcome.delayMs !== undefined) await pause(outcome.delayMs);\n handedOff = true;\n next();\n } catch (error) {\n // Fail open, loudly: reported to the operator, invisible to the visitor.\n // `next(error)` — which this used to call — routes into Express's error\n // handler and answers 500, which is failing *closed* and contradicts the\n // guarantee stated above.\n //\n // `handedOff` guards the case where the throw came from downstream rather\n // than from us: once `next()` has run, the rest of the application owns the\n // request, and calling `next` a second time would run it twice.\n failOpen(handler, error, \"adapter:node\");\n if (!handedOff && !response.headersSent) next();\n }\n })();\n };\n}\n\n/**\n * Reports an error the engine raised and says whether the request can still be served.\n *\n * Every adapter promises the same thing in its own doc comment: an unexpected failure\n * inside detection serves the request rather than refusing it, because a bot filter\n * that fails closed is an outage with extra steps. Handing the error to the\n * framework's error path does the opposite — Express, Koa and Fastify all turn it into\n * a 500 — so a bug in this library became a broken page for a real visitor. The error\n * is reported through the operator's own `onError`, which is where it is useful, and\n * the visitor gets their page.\n */\nfunction failOpen(handler: BotHandler, error: unknown, source: string): void {\n handler.config.onError(error, { source });\n}\n\nasync function serveVerification(handler: BotHandler, request: IncomingMessage, response: ServerResponse, facts: RequestFacts): Promise<void> {\n const outcome = await handler.verifyChallenge(facts, parseJson(await readBoundedBody(request)));\n response.setHeader(\"content-type\", \"application/json; charset=utf-8\");\n response.setHeader(\"cache-control\", \"no-store\");\n if (outcome.ok) response.setHeader(\"set-cookie\", outcome.setCookie);\n response.statusCode = outcome.ok ? 200 : outcome.status;\n response.end(verificationBody(outcome));\n}\n","import { createFacts } from \"../facts.js\";\nimport { MAX_VERIFY_BODY, parseJson, pause } from \"./shared.js\";\nimport type { BotHandler } from \"../core.js\";\nimport type { HandleResult } from \"../core.js\";\nimport type { RequestFacts } from \"../types.js\";\n\nexport interface FetchAdapterOptions {\n /**\n * Headers to read the client address from, in order of preference.\n *\n * Only headers your edge *overwrites* belong here. One a client can set is one it\n * can use to choose its own identity — and with it the address you rate-limit,\n * allowlist, denylist and block on. `x-forwarded-for` is deliberately **not** in the\n * default list: it is appended to rather than replaced, so its leftmost entry is\n * whatever the client wrote. List it explicitly only if you know your edge replaces\n * the whole header.\n */\n ipHeaders?: readonly string[];\n /** Reads the address from the platform's own context object, e.g. a Workers `ConnInfo`. */\n clientIp?: (request: Request, context: unknown) => string | undefined;\n /** Header carrying an edge-computed JA3/JA4 fingerprint. */\n tlsFingerprintHeader?: string;\n mountChallengeEndpoint?: boolean;\n enrich?: (request: Request, facts: RequestFacts) => RequestFacts;\n}\n\n/**\n * Address headers trusted without being asked for.\n *\n * Both are single-valued and written by the edge that terminates the connection, so a\n * client cannot choose what they say. `x-forwarded-for` used to be here and is not any\n * more: it is a chain a proxy *appends* to, so its leftmost entry — the one this\n * adapter reads — is supplied by the client. Trusting it by default let anyone pick\n * their own address and walk straight past a denylist, a rate limit and every\n * behavioural signal keyed on the actor. The other three adapters never did: they go\n * through `resolveIp`, where reading a forwarded header at all requires\n * `proxy.trustProxy` and the configuration warns at length about exactly this. This\n * one now agrees with them.\n */\nconst DEFAULT_IP_HEADERS = [\"cf-connecting-ip\", \"x-real-ip\"] as const;\n\nexport interface FetchDecision {\n /** Serve this instead of calling your handler. `undefined` means carry on. */\n response?: Response | undefined;\n /** The request to pass on, carrying the verdict headers. */\n request: Request;\n /**\n * What the engine concluded — absent for a challenge verification request, which is\n * answered without being assessed.\n *\n * Reported as missing rather than filled in with a placeholder assessment: \"we did\n * not judge this request\" and \"we judged it and found nothing\" must not look the\n * same to whatever is reading this.\n */\n result?: HandleResult | undefined;\n}\n\n/**\n * Web-standard adapter, for Cloudflare Workers, Deno, Bun, Vercel Edge and anything\n * else built on `Request`/`Response`.\n *\n * Returns a decision rather than wrapping your handler, so the same primitive fits a\n * router, a middleware chain or a plain `fetch` export. {@link withBotHandler} wraps it\n * for the common case.\n *\n * Note that the engine uses `node:crypto` for HMAC and hashing. On Workers that means\n * enabling `nodejs_compat`; Deno and Bun provide it natively.\n */\nexport function createFetchAdapter(handler: BotHandler, options: FetchAdapterOptions = {}) {\n const ipHeaders = options.ipHeaders ?? DEFAULT_IP_HEADERS;\n const mountChallenge = options.mountChallengeEndpoint ?? true;\n let warnedAboutAddress = false;\n\n return async function evaluate(request: Request, context: unknown = undefined): Promise<FetchDecision> {\n try {\n return await decide(request, context);\n } catch (error) {\n // Fail open, loudly — the same promise the node, Koa and Fastify adapters make,\n // and the one this adapter was not keeping. An error escaping here reaches the\n // runtime's own handler, and Workers, Deno and Bun all answer 500: a detection\n // bug charged to the visitor. Returning the request unchanged, with no verdict,\n // serves the page instead; `result` is absent, which already means \"we did not\n // judge this request\" to everything downstream.\n handler.config.onError(error, { source: \"adapter:fetch\" });\n return { request };\n }\n };\n\n async function decide(request: Request, context: unknown): Promise<FetchDecision> {\n const headers: Record<string, string | undefined> = {};\n const order: string[] = [];\n for (const [name, value] of request.headers) {\n headers[name] = value;\n order.push(name);\n }\n\n const url = new URL(request.url);\n const ip = options.clientIp?.(request, context) ?? firstHeaderAddress(headers, ipHeaders) ?? \"\";\n // An empty address is not a harmless blank: it becomes the actor key, so every\n // visitor collapses into one actor and every rate limit and behavioural signal\n // becomes site-wide. Said once, because it is a deployment mistake rather than a\n // per-request event, and a warning on every request is a warning nobody reads.\n if (ip === \"\" && !warnedAboutAddress) {\n warnedAboutAddress = true;\n // Through the handler rather than straight to the callback, so a subscriber —\n // and the dashboard's notices panel — hears about it too.\n handler.warn(\n `The fetch adapter could not determine a client address: none of ${ipHeaders.join(\", \")} is present and no \\`clientIp\\` was supplied. ` +\n `Every request will be tracked under one empty actor key, so rate limits and behavioural detection apply to your whole site at once. ` +\n `Pass \\`clientIp\\` to read the address from your platform's connection info, or list the header your edge sets.`,\n );\n }\n const fingerprint = options.tlsFingerprintHeader !== undefined ? headers[options.tlsFingerprintHeader.toLowerCase()] : undefined;\n\n let facts = createFacts({\n method: request.method,\n url: `${url.pathname}${url.search}`,\n headers,\n // The Fetch API normalises and sorts headers, so wire order is genuinely\n // unavailable here. Passing the sorted order would be worse than passing none:\n // the header-order detector would compare against an ordering no client chose.\n rawHeaders: [],\n ip,\n protocol: url.protocol === \"https:\" ? \"https\" : \"http\",\n ...(fingerprint !== undefined ? { tlsFingerprint: fingerprint } : {}),\n });\n if (options.enrich) facts = options.enrich(request, facts);\n\n if (mountChallenge && handler.isChallengeEndpoint(facts)) {\n const body = await readBounded(request);\n const outcome = await handler.verifyChallenge(facts, parseJson(body));\n const responseHeaders = new Headers({ \"content-type\": \"application/json; charset=utf-8\", \"cache-control\": \"no-store\" });\n if (outcome.ok) responseHeaders.set(\"set-cookie\", outcome.setCookie);\n // Deliberately not assessed. `assess` records the request against the actor, so\n // running it here inflated the request count and arrival cadence of the one\n // client that is trying to answer a challenge — feeding the rate and cadence\n // detectors with the evidence of its own compliance. The comment here already\n // said the request was not assessed; the code called `assess` anyway.\n return {\n response: new Response(JSON.stringify(outcome.ok ? { ok: true } : { ok: false, error: outcome.reason }), {\n status: outcome.ok ? 200 : outcome.status,\n headers: responseHeaders,\n }),\n request,\n };\n }\n\n const result = await handler.handle(facts);\n const { outcome } = result;\n\n if (outcome.kind === \"drop\") {\n // A Fetch runtime cannot close a connection without answering, so the nearest\n // equivalent is an empty 444-style refusal. Say so rather than pretend.\n return { response: new Response(null, { status: 444 }), request, result };\n }\n\n if (outcome.kind === \"respond\") {\n return { response: new Response(outcome.body, { status: outcome.status, headers: outcome.headers }), request, result };\n }\n\n if (outcome.delayMs !== undefined) await pause(outcome.delayMs);\n\n // Requests are immutable, so tagging means constructing a new one.\n const tagged = outcome.requestHeaders\n ? new Request(request, { headers: mergeHeaders(request.headers, outcome.requestHeaders) })\n : request;\n\n return { request: tagged, result };\n }\n}\n\n/** Wraps a handler. The common case: one call, one line. */\nexport function withBotHandler(\n handler: BotHandler,\n next: (request: Request, context: unknown) => Response | Promise<Response>,\n options: FetchAdapterOptions = {},\n): (request: Request, context?: unknown) => Promise<Response> {\n const evaluate = createFetchAdapter(handler, options);\n return async (request, context = undefined) => {\n const decision = await evaluate(request, context);\n if (decision.response) return decision.response;\n const response = await next(decision.request, context);\n const extra = decision.result?.outcome.kind === \"continue\" ? decision.result.outcome.responseHeaders : undefined;\n if (!extra) return response;\n const merged = new Response(response.body, response);\n for (const [name, value] of Object.entries(extra)) merged.headers.set(name, value);\n return merged;\n };\n}\n\nfunction firstHeaderAddress(headers: Record<string, string | undefined>, names: readonly string[]): string | undefined {\n for (const name of names) {\n const value = headers[name];\n if (value === undefined) continue;\n // A forwarded chain is left-to-right, oldest first. The leftmost entry is the\n // one the client itself can write, so it is taken only from headers the caller\n // has declared trustworthy by listing them.\n const first = value.split(\",\")[0]?.trim();\n if (first !== undefined && first.length > 0) return first;\n }\n return undefined;\n}\n\nfunction mergeHeaders(original: Headers, additions: Record<string, string>): Headers {\n const merged = new Headers(original);\n for (const [name, value] of Object.entries(additions)) merged.set(name, value);\n return merged;\n}\n\nasync function readBounded(request: Request): Promise<string> {\n const text = await request.clone().text();\n return text.length > MAX_VERIFY_BODY ? \"\" : text;\n}\n","import { createFacts } from \"../facts.js\";\nimport { pause, parseJson, readBoundedBody, verificationBody } from \"./shared.js\";\nimport type { BotHandler } from \"../core.js\";\nimport type { RequestFacts } from \"../types.js\";\n\n/**\n * The parts of Fastify's request and reply this adapter touches, described\n * structurally so the library takes no dependency on Fastify or its types.\n */\nexport interface FastifyLikeRequest {\n method: string;\n url: string;\n headers: Record<string, string | string[] | undefined>;\n raw: import(\"node:http\").IncomingMessage;\n}\n\nexport interface FastifyLikeReply {\n code(status: number): FastifyLikeReply;\n header(name: string, value: string): FastifyLikeReply;\n send(body: unknown): unknown;\n hijack?(): void;\n}\n\nexport interface FastifyAdapterOptions {\n tlsFingerprintHeader?: string;\n /**\n * Also serve the challenge verification endpoint. Default true.\n *\n * Fastify parses bodies after `onRequest`, so this reads the raw stream itself.\n * That is why it must run here rather than as a route: by the time a route handler\n * sees the request, the body has been consumed by a parser that does not know about\n * this endpoint.\n */\n mountChallengeEndpoint?: boolean;\n enrich?: (request: FastifyLikeRequest, facts: RequestFacts) => RequestFacts;\n}\n\n/**\n * An `onRequest` hook for Fastify.\n *\n * ```ts\n * fastify.addHook(\"onRequest\", fastifyBotHandler(handler));\n * ```\n *\n * `onRequest` rather than `preHandler` on purpose: it is the earliest hook, so a\n * refused request never reaches routing, validation or your body parser — none of\n * which should be doing work for traffic that has already been decided about.\n */\nexport function fastifyBotHandler(handler: BotHandler, options: FastifyAdapterOptions = {}) {\n const mountChallenge = options.mountChallengeEndpoint ?? true;\n\n return async function onRequest(request: FastifyLikeRequest, reply: FastifyLikeReply): Promise<void> {\n try {\n await evaluate(handler, options, mountChallenge, request, reply);\n } catch (error) {\n // Returning normally lets routing continue. Letting the throw reach Fastify\n // would answer 500 instead — a detection bug charged to the visitor, which is\n // the one thing every adapter here promises not to do.\n handler.config.onError(error, { source: \"adapter:fastify\" });\n }\n };\n}\n\nasync function evaluate(\n handler: BotHandler,\n options: FastifyAdapterOptions,\n mountChallenge: boolean,\n request: FastifyLikeRequest,\n reply: FastifyLikeReply,\n): Promise<void> {\n const headers: Record<string, string | undefined> = {};\n for (const [name, value] of Object.entries(request.headers)) {\n headers[name] = Array.isArray(value) ? value.join(\", \") : value;\n }\n\n const ip = handler.resolveIp(request.raw.socket.remoteAddress, headers);\n const fingerprint = options.tlsFingerprintHeader !== undefined ? headers[options.tlsFingerprintHeader.toLowerCase()] : undefined;\n\n let facts = createFacts({\n method: request.method,\n url: request.url,\n headers: request.headers,\n rawHeaders: request.raw.rawHeaders,\n ip,\n httpVersion: request.raw.httpVersion,\n protocol: (request.raw.socket as { encrypted?: boolean }).encrypted === true || headers[\"x-forwarded-proto\"] === \"https\" ? \"https\" : \"http\",\n ...(fingerprint !== undefined ? { tlsFingerprint: fingerprint } : {}),\n });\n if (options.enrich) facts = options.enrich(request, facts);\n\n if (mountChallenge && handler.isChallengeEndpoint(facts)) {\n const verification = await handler.verifyChallenge(facts, parseJson(await readBoundedBody(request.raw)));\n reply.code(verification.ok ? 200 : verification.status);\n reply.header(\"content-type\", \"application/json; charset=utf-8\");\n reply.header(\"cache-control\", \"no-store\");\n if (verification.ok) reply.header(\"set-cookie\", verification.setCookie);\n reply.send(verificationBody(verification));\n return;\n }\n\n const { outcome } = await handler.handle(facts);\n\n if (outcome.kind === \"drop\") {\n // `hijack` tells Fastify this route will not produce a reply, so it does not\n // try to serialise one for a socket that is about to disappear.\n reply.hijack?.();\n request.raw.socket.destroy();\n return;\n }\n\n if (outcome.kind === \"respond\") {\n reply.code(outcome.status);\n for (const [name, value] of Object.entries(outcome.headers)) reply.header(name, value);\n reply.send(outcome.body);\n return;\n }\n\n for (const [name, value] of Object.entries(outcome.requestHeaders ?? {})) {\n request.headers[name] = value;\n }\n if (outcome.responseHeaders) {\n for (const [name, value] of Object.entries(outcome.responseHeaders)) reply.header(name, value);\n }\n if (outcome.delayMs !== undefined) await pause(outcome.delayMs);\n}\n","import { createFacts } from \"../facts.js\";\nimport { pause, parseJson, readBoundedBody, verificationBody } from \"./shared.js\";\nimport type { BotHandler } from \"../core.js\";\n\n/** The parts of a Koa context this adapter touches, described structurally. */\nexport interface KoaLikeContext {\n method: string;\n url: string;\n headers: Record<string, string | string[] | undefined>;\n req: import(\"node:http\").IncomingMessage;\n status: number;\n body: unknown;\n set(name: string, value: string): void;\n secure?: boolean;\n}\n\nexport interface KoaAdapterOptions {\n /** Also serve the challenge verification endpoint. Default true. */\n mountChallengeEndpoint?: boolean;\n}\n\n/**\n * Koa middleware.\n *\n * ```ts\n * app.use(koaBotHandler(handler));\n * ```\n *\n * Place it above everything else. Koa's downstream-then-upstream flow means anything\n * mounted before this still runs for a request that is about to be refused.\n */\nexport function koaBotHandler(handler: BotHandler, options: KoaAdapterOptions = {}) {\n const mountChallenge = options.mountChallengeEndpoint ?? true;\n\n return async function botHandlerMiddleware(context: KoaLikeContext, next: () => Promise<void>): Promise<void> {\n let proceed: boolean;\n try {\n proceed = await evaluate(handler, mountChallenge, context);\n } catch (error) {\n // Serve the request rather than let a detection bug become a 500. The try\n // covers the engine only: `next()` runs the rest of your application, and\n // catching *its* failures here would both swallow them and call `next` twice.\n handler.config.onError(error, { source: \"adapter:koa\" });\n proceed = true;\n }\n if (proceed) await next();\n };\n}\n\n/** Runs the engine and applies its outcome. Returns whether the request continues downstream. */\nasync function evaluate(handler: BotHandler, mountChallenge: boolean, context: KoaLikeContext): Promise<boolean> {\n const headers: Record<string, string | undefined> = {};\n for (const [name, value] of Object.entries(context.headers)) {\n headers[name] = Array.isArray(value) ? value.join(\", \") : value;\n }\n\n const facts = createFacts({\n method: context.method,\n url: context.url,\n headers: context.headers,\n rawHeaders: context.req.rawHeaders,\n ip: handler.resolveIp(context.req.socket.remoteAddress, headers),\n httpVersion: context.req.httpVersion,\n protocol:\n context.secure === true || (context.req.socket as { encrypted?: boolean }).encrypted === true || headers[\"x-forwarded-proto\"] === \"https\" ? \"https\" : \"http\",\n });\n\n if (mountChallenge && handler.isChallengeEndpoint(facts)) {\n const verification = await handler.verifyChallenge(facts, parseJson(await readBoundedBody(context.req)));\n context.status = verification.ok ? 200 : verification.status;\n context.set(\"content-type\", \"application/json; charset=utf-8\");\n context.set(\"cache-control\", \"no-store\");\n if (verification.ok) context.set(\"set-cookie\", verification.setCookie);\n context.body = verificationBody(verification);\n return false;\n }\n\n const { outcome } = await handler.handle(facts);\n\n if (outcome.kind === \"drop\") {\n context.req.socket.destroy();\n return false;\n }\n\n if (outcome.kind === \"respond\") {\n context.status = outcome.status;\n for (const [name, value] of Object.entries(outcome.headers)) context.set(name, value);\n context.body = outcome.body;\n return false;\n }\n\n for (const [name, value] of Object.entries(outcome.requestHeaders ?? {})) {\n context.req.headers[name] = value;\n }\n if (outcome.responseHeaders) {\n for (const [name, value] of Object.entries(outcome.responseHeaders)) context.set(name, value);\n }\n if (outcome.delayMs !== undefined) await pause(outcome.delayMs);\n return true;\n}\n"],"mappings":"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","names":["evaluate","outcome","evaluate"]}
1
+ {"version":3,"sources":["../../src/adapters/index.ts","../../src/internal/http.ts","../../src/internal/ip.ts","../../src/facts.ts","../../src/adapters/shared.ts","../../src/adapters/node.ts","../../src/adapters/fetch.ts","../../src/adapters/fastify.ts","../../src/adapters/koa.ts"],"sourcesContent":["export { botHandler } from \"./node.js\";\nexport type { NodeMiddleware, NodeMiddlewareOptions, NextFunction } from \"./node.js\";\nexport { createFetchAdapter, withBotHandler } from \"./fetch.js\";\nexport type { FetchAdapterOptions, FetchDecision } from \"./fetch.js\";\nexport { fastifyBotHandler } from \"./fastify.js\";\nexport type { FastifyAdapterOptions, FastifyLikeRequest, FastifyLikeReply } from \"./fastify.js\";\nexport { koaBotHandler } from \"./koa.js\";\nexport type { KoaLikeContext, KoaAdapterOptions } from \"./koa.js\";\n","/** Cookie parsing and serialisation, plus small header helpers. No dependencies. */\n\n/** Longest Cookie header we will parse. Beyond this it is a payload, not a session. */\nconst MAX_COOKIE_HEADER = 8192;\nconst MAX_COOKIES = 64;\n\n/**\n * Parses a `Cookie` header into a null-prototype bag.\n *\n * Null-prototype matters: a request carrying `__proto__=x` would otherwise hit the\n * prototype setter on a plain object and vanish, and any later `cookies.constructor`\n * lookup would find `Object`'s rather than `undefined`. Neither is a vulnerability by\n * itself; both are the kind of surprise that becomes one.\n */\nexport function parseCookies(header: string | undefined): Record<string, string> {\n const cookies: Record<string, string> = Object.create(null) as Record<string, string>;\n if (header === undefined || header.length === 0) return cookies;\n const source = header.length > MAX_COOKIE_HEADER ? header.slice(0, MAX_COOKIE_HEADER) : header;\n\n let count = 0;\n for (const pair of source.split(\";\")) {\n if (count >= MAX_COOKIES) break;\n const equals = pair.indexOf(\"=\");\n if (equals <= 0) continue;\n const name = pair.slice(0, equals).trim();\n if (name.length === 0) continue;\n const rawValue = pair.slice(equals + 1).trim();\n const value = rawValue.startsWith('\"') && rawValue.endsWith('\"') ? rawValue.slice(1, -1) : rawValue;\n try {\n cookies[name] = decodeURIComponent(value);\n } catch {\n // A malformed percent-escape is not worth rejecting the whole header over.\n cookies[name] = value;\n }\n count++;\n }\n return cookies;\n}\n\nexport interface CookieOptions {\n maxAgeMs?: number;\n path?: string;\n domain?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: \"Lax\" | \"Strict\" | \"None\";\n}\n\n/** Builds a `Set-Cookie` value. Rejects names and values that would let a caller inject attributes. */\nexport function serializeCookie(name: string, value: string, options: CookieOptions = {}): string {\n if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(name)) throw new TypeError(`Invalid cookie name: ${name}`);\n const parts = [`${name}=${encodeURIComponent(value)}`];\n if (options.maxAgeMs !== undefined) parts.push(`Max-Age=${Math.floor(options.maxAgeMs / 1000)}`);\n parts.push(`Path=${options.path ?? \"/\"}`);\n if (options.domain !== undefined) {\n if (!/^[A-Za-z0-9.-]+$/.test(options.domain)) throw new TypeError(`Invalid cookie domain: ${options.domain}`);\n parts.push(`Domain=${options.domain}`);\n }\n if (options.secure !== false) parts.push(\"Secure\");\n if (options.httpOnly !== false) parts.push(\"HttpOnly\");\n // `SameSite=None` without `Secure` is rejected by browsers, so it is never a valid\n // combination to emit — fail loudly here rather than shipping a cookie nothing keeps.\n const sameSite = options.sameSite ?? \"Lax\";\n if (sameSite === \"None\" && options.secure === false) {\n throw new TypeError(\"SameSite=None requires Secure\");\n }\n parts.push(`SameSite=${sameSite}`);\n return parts.join(\"; \");\n}\n\n/** Joins multi-value headers the way the rest of the library expects to see them. */\nexport function joinHeaderValue(value: string | string[] | undefined): string | undefined {\n if (value === undefined) return undefined;\n return Array.isArray(value) ? value.join(\", \") : value;\n}\n","/**\n * IP parsing, normalisation and CIDR matching, with no dependencies.\n *\n * Everything works on raw byte arrays rather than strings, because string\n * comparison of IPs is a classic source of bypasses: `::ffff:127.0.0.1`,\n * `0177.0.0.1` and `127.0.0.001` are all the same host to the network stack but\n * three different strings to a naive allowlist.\n */\n\n/** An IP as bytes: 4 for IPv4, 16 for IPv6. IPv4-mapped IPv6 is folded to 4. */\nexport type IpBytes = Uint8Array;\n\n/**\n * Parses an IPv4 or IPv6 literal into bytes, or `null` if it is not a valid\n * address. Deliberately strict: no octal, no hex, no shorthand octets, no zone\n * ids. Anything ambiguous is rejected rather than guessed at.\n */\n/**\n * An address with its source port removed, when it carried one.\n *\n * Forwarded headers are not consistent about this. Most proxies write a bare address,\n * but Azure's Application Gateway and Front Door write `1.2.3.4:5678`, and the\n * bracketed `[2001:db8::1]:5678` is the form RFC 7239 defines for IPv6. An entry that\n * carries a port parses as nothing at all, and the consequence is not that one entry is\n * skipped: every entry in the chain looks the same way, the chain empties, and the whole\n * internet collapses onto the proxy's own address as a single actor. Rate limits, actor\n * history and reputation then apply to everyone at once, so one bot locks out every real\n * visitor — and none of it announces itself.\n *\n * The rule has to be narrow, because a bare IPv6 address is *made of* colons and must\n * never be mistaken for a host and port. Only two shapes are a port: a bracketed host,\n * which is unambiguous, and a single colon whose left side is an IPv4 address. Anything\n * with more colons and no brackets is IPv6 and is returned untouched.\n */\nexport function stripPort(value: string): string {\n const input = value.trim();\n if (input.startsWith(\"[\")) {\n const close = input.indexOf(\"]\");\n if (close > 0) return input.slice(1, close);\n return input;\n }\n const colon = input.indexOf(\":\");\n // Exactly one colon: IPv6 always has at least two, so this can only be host:port.\n if (colon === -1 || input.indexOf(\":\", colon + 1) !== -1) return input;\n const host = input.slice(0, colon);\n return parseIpv4(host) !== null ? host : input;\n}\n\nexport function parseIp(value: string): IpBytes | null {\n const input = value.trim();\n if (input.length === 0 || input.length > 45) return null;\n // Bracketed form from a Host/Forwarded header: [2001:db8::1]\n const bare = input.startsWith(\"[\") && input.endsWith(\"]\") ? input.slice(1, -1) : input;\n if (bare.includes(\":\")) return parseIpv6(bare);\n return parseIpv4(bare);\n}\n\nfunction parseIpv4(value: string): IpBytes | null {\n const parts = value.split(\".\");\n if (parts.length !== 4) return null;\n const bytes = new Uint8Array(4);\n for (let i = 0; i < 4; i++) {\n const part = parts[i]!;\n // Reject empty, over-long, non-digit and leading-zero forms outright.\n if (part.length === 0 || part.length > 3) return null;\n if (!/^\\d+$/.test(part)) return null;\n if (part.length > 1 && part[0] === \"0\") return null;\n const n = Number(part);\n if (n > 255) return null;\n bytes[i] = n;\n }\n return bytes;\n}\n\nfunction parseIpv6(value: string): IpBytes | null {\n // Strip a zone id (`%eth0`) — it is a local routing detail, not part of identity.\n const withoutZone = value.split(\"%\")[0]!;\n const doubleColon = withoutZone.indexOf(\"::\");\n if (doubleColon !== withoutZone.lastIndexOf(\"::\")) return null;\n\n const [headText, tailText] =\n doubleColon === -1\n ? [withoutZone, \"\"]\n : [withoutZone.slice(0, doubleColon), withoutZone.slice(doubleColon + 2)];\n\n const head = headText.length > 0 ? headText.split(\":\") : [];\n const tail = tailText.length > 0 ? tailText.split(\":\") : [];\n\n // A trailing dotted-quad (`::ffff:192.0.2.1`) occupies the last two groups.\n let embedded: IpBytes | null = null;\n const groups = [...head, ...tail];\n const last = groups[groups.length - 1];\n if (last !== undefined && last.includes(\".\")) {\n embedded = parseIpv4(last);\n if (!embedded) return null;\n if (tail.length > 0) tail.pop();\n else head.pop();\n }\n\n const groupCount = head.length + tail.length + (embedded ? 2 : 0);\n if (doubleColon === -1 ? groupCount !== 8 : groupCount > 7) return null;\n\n const bytes = new Uint8Array(16);\n let offset = 0;\n for (const group of head) {\n if (!writeGroup(bytes, offset, group)) return null;\n offset += 2;\n }\n // The gap the `::` stands for, left as the zeroes the array already holds.\n offset = 16 - tail.length * 2 - (embedded ? 4 : 0);\n for (const group of tail) {\n if (!writeGroup(bytes, offset, group)) return null;\n offset += 2;\n }\n if (embedded) bytes.set(embedded, 12);\n\n // Fold IPv4-mapped (::ffff:0:0/96) down to a plain v4 address so that one host\n // has exactly one representation regardless of which stack accepted it.\n if (isIpv4Mapped(bytes)) return bytes.slice(12, 16);\n return bytes;\n}\n\nfunction writeGroup(bytes: Uint8Array, offset: number, group: string): boolean {\n if (group.length === 0 || group.length > 4 || !/^[0-9a-fA-F]+$/.test(group)) return false;\n const n = Number.parseInt(group, 16);\n bytes[offset] = n >> 8;\n bytes[offset + 1] = n & 0xff;\n return true;\n}\n\nfunction isIpv4Mapped(bytes: Uint8Array): boolean {\n for (let i = 0; i < 10; i++) if (bytes[i] !== 0) return false;\n return bytes[10] === 0xff && bytes[11] === 0xff;\n}\n\n/** Canonical string form, so the same host always produces the same actor key. */\nexport function formatIp(bytes: IpBytes): string {\n if (bytes.length === 4) return `${bytes[0]}.${bytes[1]}.${bytes[2]}.${bytes[3]}`;\n const groups: string[] = [];\n for (let i = 0; i < 16; i += 2) groups.push(((bytes[i]! << 8) | bytes[i + 1]!).toString(16));\n // RFC 5952: compress the longest run of zero groups, leftmost on a tie.\n let bestStart = -1;\n let bestLen = 0;\n for (let i = 0; i < 8; i++) {\n if (groups[i] !== \"0\") continue;\n let j = i;\n while (j < 8 && groups[j] === \"0\") j++;\n if (j - i > bestLen) {\n bestLen = j - i;\n bestStart = i;\n }\n i = j;\n }\n if (bestLen < 2) return groups.join(\":\");\n return `${groups.slice(0, bestStart).join(\":\")}::${groups.slice(bestStart + bestLen).join(\":\")}`;\n}\n\n/** Normalises any accepted spelling of an address to its canonical form. Returns `null` if unparseable. */\nexport function normalizeIp(value: string): string | null {\n const bytes = parseIp(value);\n return bytes ? formatIp(bytes) : null;\n}\n\nexport interface Cidr {\n readonly bytes: IpBytes;\n readonly prefix: number;\n readonly source: string;\n}\n\n/** Parses `\"10.0.0.0/8\"`, `\"2001:db8::/32\"`, or a bare address (treated as a /32 or /128). */\nexport function parseCidr(value: string): Cidr | null {\n const slash = value.lastIndexOf(\"/\");\n const addressText = slash === -1 ? value : value.slice(0, slash);\n const bytes = parseIp(addressText);\n if (!bytes) return null;\n const maxPrefix = bytes.length * 8;\n if (slash === -1) return { bytes, prefix: maxPrefix, source: value };\n const prefixText = value.slice(slash + 1);\n if (!/^\\d{1,3}$/.test(prefixText)) return null;\n const prefix = Number(prefixText);\n if (prefix > maxPrefix) return null;\n return { bytes, prefix, source: value };\n}\n\n/** True when `ip` falls inside `cidr`. Address families never match across each other. */\nexport function cidrContains(cidr: Cidr, ip: IpBytes): boolean {\n if (cidr.bytes.length !== ip.length) return false;\n const fullBytes = cidr.prefix >> 3;\n for (let i = 0; i < fullBytes; i++) if (cidr.bytes[i] !== ip[i]) return false;\n const remainder = cidr.prefix & 7;\n if (remainder === 0) return true;\n const mask = 0xff << (8 - remainder);\n return (cidr.bytes[fullBytes]! & mask) === (ip[fullBytes]! & mask);\n}\n\n/**\n * One range plus the position it was added at, so that when several ranges match the\n * same address the answer does not depend on how the index happens to be laid out.\n */\ninterface IndexedCidr {\n readonly cidr: Cidr;\n readonly order: number;\n}\n\n/**\n * Ranges for one address family, indexed on the first two bytes.\n *\n * A linear scan is fine for the handful of CIDRs in an allowlist and catastrophic for\n * the list this library actually asks operators to supply: AWS publishes around seven\n * thousand IPv4 prefixes, GCP and Azure comparable numbers, and `datacenterRanges` is\n * documented as the place to put them. Scanning that on every request measured at\n * ~87µs — several times the cost of the entire rest of an assessment — so the size of\n * a range list silently became the dominant term in request latency.\n *\n * Bucketing by the leading bytes fixes that without changing any answer. A prefix of\n * /16 or longer pins both leading bytes, so it is reachable from exactly one bucket;\n * a /8 to /15 pins only the first; anything shorter than /8 spans buckets and stays in\n * a list checked on every lookup. Real range lists are overwhelmingly /16 and longer,\n * which is why this collapses to a handful of comparisons.\n */\nclass FamilyIndex {\n /** Prefix < 8. Spans first bytes, so it is always scanned. Realistically empty. */\n private readonly wide: IndexedCidr[] = [];\n private readonly buckets = new Map<number, { mid: IndexedCidr[]; deep: Map<number, IndexedCidr[]> }>();\n\n add(entry: IndexedCidr): void {\n const { cidr } = entry;\n if (cidr.prefix < 8) {\n this.wide.push(entry);\n return;\n }\n const first = cidr.bytes[0]!;\n let bucket = this.buckets.get(first);\n if (bucket === undefined) {\n bucket = { mid: [], deep: new Map() };\n this.buckets.set(first, bucket);\n }\n if (cidr.prefix < 16) {\n bucket.mid.push(entry);\n return;\n }\n const second = cidr.bytes[1]!;\n let deep = bucket.deep.get(second);\n if (deep === undefined) {\n deep = [];\n bucket.deep.set(second, deep);\n }\n deep.push(entry);\n }\n\n /** The earliest-added range containing `ip`, or `undefined`. */\n find(ip: IpBytes): IndexedCidr | undefined {\n let best = scan(this.wide, ip, undefined);\n const bucket = this.buckets.get(ip[0]!);\n if (bucket !== undefined) {\n best = scan(bucket.mid, ip, best);\n const deep = bucket.deep.get(ip[1]!);\n if (deep !== undefined) best = scan(deep, ip, best);\n }\n return best;\n }\n}\n\nfunction scan(entries: readonly IndexedCidr[], ip: IpBytes, best: IndexedCidr | undefined): IndexedCidr | undefined {\n let winner = best;\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i]!;\n if (winner !== undefined && entry.order > winner.order) continue;\n if (cidrContains(entry.cidr, ip)) winner = entry;\n }\n return winner;\n}\n\n/**\n * A compiled set of CIDR ranges. Building it once and reusing it keeps the hot\n * path free of string parsing — matching is a handful of byte comparisons, and stays\n * that way as the list grows into the thousands.\n */\nexport class IpRangeSet {\n private readonly v4 = new FamilyIndex();\n private readonly v6 = new FamilyIndex();\n private count = 0;\n /**\n * Every range that parsed, in the order it was added.\n *\n * Kept as plain text beside the index rather than reconstructed from it. The index\n * is shaped for lookups — bucketed by leading byte, prefix-split — and walking it\n * back into a list would be both slower and, for anything that wants to *show* the\n * set, wrong: what an operator recognises is the string they wrote, not a normalised\n * form of it. These sets are configuration-sized, so the array costs nothing.\n */\n private readonly sources: string[] = [];\n /** Ranges that failed to parse, surfaced so a typo in config is loud rather than silent. */\n readonly invalid: string[] = [];\n\n constructor(ranges: Iterable<string> = []) {\n for (const range of ranges) this.add(range);\n }\n\n add(range: string): void {\n const cidr = parseCidr(range);\n if (!cidr) {\n this.invalid.push(range);\n return;\n }\n this.sources.push(cidr.source);\n (cidr.bytes.length === 4 ? this.v4 : this.v6).add({ cidr, order: this.count++ });\n }\n\n get size(): number {\n return this.count;\n }\n\n /** The ranges in this set, as written, oldest first. What a reader can act on. */\n entries(): readonly string[] {\n return this.sources;\n }\n\n /**\n * Returns the matching range's original text, or `undefined`. Useful for explaining\n * a decision. When several ranges match, the one added first wins, so the\n * explanation does not depend on the index's internal layout.\n */\n match(ip: string | IpBytes): string | undefined {\n const bytes = typeof ip === \"string\" ? parseIp(ip) : ip;\n if (!bytes) return undefined;\n return (bytes.length === 4 ? this.v4 : this.v6).find(bytes)?.cidr.source;\n }\n\n contains(ip: string | IpBytes): boolean {\n const bytes = typeof ip === \"string\" ? parseIp(ip) : ip;\n if (!bytes) return false;\n return (bytes.length === 4 ? this.v4 : this.v6).find(bytes) !== undefined;\n }\n}\n\n/** IANA special-purpose ranges: loopback, private, link-local, CGNAT, documentation. */\nexport const SPECIAL_USE_RANGES = [\n \"0.0.0.0/8\",\n \"10.0.0.0/8\",\n \"100.64.0.0/10\",\n \"127.0.0.0/8\",\n \"169.254.0.0/16\",\n \"172.16.0.0/12\",\n \"192.0.0.0/24\",\n \"192.0.2.0/24\",\n \"192.168.0.0/16\",\n \"198.18.0.0/15\",\n \"198.51.100.0/24\",\n \"203.0.113.0/24\",\n \"224.0.0.0/4\",\n \"240.0.0.0/4\",\n \"::1/128\",\n \"fc00::/7\",\n \"fe80::/10\",\n \"2001:db8::/32\",\n] as const;\n\nconst SPECIAL_USE = new IpRangeSet(SPECIAL_USE_RANGES);\n\n/** True for loopback/private/link-local/documentation addresses — never public clients. */\nexport function isSpecialUse(ip: string): boolean {\n return SPECIAL_USE.contains(ip);\n}\n\n/**\n * Masks an address to a coarse network for rate accounting: /24 for IPv4, /64 for\n * IPv6. IPv6 clients routinely get a whole /64 to themselves and rotate the host\n * bits freely, so per-address counting is trivially defeated there.\n */\nexport function networkKey(ip: string): string {\n const bytes = parseIp(ip);\n if (!bytes) return ip;\n // Build the masked address at full width. Slicing to three bytes and formatting\n // that would produce a 3-byte array, which `formatIp` reads as IPv6.\n if (bytes.length === 4) {\n const masked = new Uint8Array([bytes[0]!, bytes[1]!, bytes[2]!, 0]);\n return `${formatIp(masked)}/24`;\n }\n const masked = new Uint8Array(16);\n masked.set(bytes.subarray(0, 8), 0);\n return `${formatIp(masked)}/64`;\n}\n","import { joinHeaderValue, parseCookies } from \"./internal/http.js\";\nimport { normalizeIp } from \"./internal/ip.js\";\nimport type { RequestFacts } from \"./types.js\";\n\n/** Longest URL we will parse. Anything beyond this is a payload, not a path. */\nconst MAX_URL_LENGTH = 8192;\n/**\n * Cap on the retained raw target.\n *\n * Shorter than the URL bound because this one only exists to be *read*: everything that\n * makes a target evasive is at its front, and the rest is a payload somebody would like\n * this process to spend memory on.\n */\nconst MAX_RAW_PATH = 512;\n/** Cap on query parameters kept. */\nconst MAX_QUERY_PARAMS = 64;\n\nexport interface FactsInput {\n method?: string | undefined;\n /** Request target, path plus optional query — what `req.url` gives you. */\n url?: string | undefined;\n /** Raw header map. Values may be arrays; names may be any case. */\n headers: Record<string, string | string[] | undefined>;\n /**\n * Header names in wire order. Node exposes them via `req.rawHeaders` (alternating\n * name/value) — pass that array directly, or a name-only list.\n */\n rawHeaders?: readonly string[] | undefined;\n /** Socket address. Pass the *socket's* address; forwarding is resolved separately. */\n ip: string;\n timestamp?: number | undefined;\n protocol?: \"http\" | \"https\" | undefined;\n /**\n * The HTTP version of the connection **this process accepted** — Node's\n * `request.httpVersion`, not the version the client negotiated with your edge.\n *\n * The distinction has teeth. HTTP/2 forbids connection-specific headers, and\n * `header-integrity` treats one as a deterministic protocol violation. If you set\n * this from a forwarded header while the request itself arrived over HTTP/1.1 from\n * a proxy — which adds `Connection: keep-alive` — you will manufacture that\n * violation for every real browser behind that proxy, and it is a `certain` verdict,\n * so it can block. Report the connection you actually have, or leave it unset.\n */\n httpVersion?: string | undefined;\n tlsFingerprint?: string | undefined;\n /** See {@link RequestFacts.partialHeaders}. Set it when the source cannot supply every header. */\n partialHeaders?: boolean | undefined;\n extra?: Record<string, unknown> | undefined;\n}\n\n/**\n * Reduces a request to the facts detectors are allowed to see.\n *\n * Normalisation happens exactly once, here, and every detector reads the result. That\n * is partly performance — lowercasing a header map per detector would be absurd — but\n * mostly correctness: if one detector reads `req.headers['User-Agent']` and another\n * reads `req.headers['user-agent']`, they will eventually disagree about the same\n * request, and the bug will be invisible.\n *\n * Every field is bounded. Each one is attacker-controlled, and this runs on every\n * request to your site.\n */\nexport function createFacts(input: FactsInput): RequestFacts {\n const rawUrl = input.url ?? \"/\";\n const url = rawUrl.length > MAX_URL_LENGTH ? rawUrl.slice(0, MAX_URL_LENGTH) : rawUrl;\n const queryStart = url.indexOf(\"?\");\n const rawPath = queryStart === -1 ? url : url.slice(0, queryStart);\n\n const headers: Record<string, string | undefined> = Object.create(null) as Record<string, string | undefined>;\n for (const [name, value] of Object.entries(input.headers)) {\n const lower = name.toLowerCase();\n // `cookie` is the one header that is not comma-joined, and getting it wrong is not\n // cosmetic. HTTP/2 explicitly permits a client to split its cookies across several\n // header fields — Node's `http2` hands them over as an array — and RFC 9113 §8.2.3\n // says a receiver concatenates them with \"; \". Joining with \", \" instead produced\n // `sid=abc, __bh_clearance=token`, which parses as *one* cookie named `sid` whose\n // value is the rest of the line. Every cookie after the first was invisible, so an\n // HTTP/2 visitor who had solved a challenge was re-challenged forever.\n const joined = lower === \"cookie\" && Array.isArray(value) ? value.join(\"; \") : joinHeaderValue(value);\n if (joined !== undefined) headers[lower] = joined;\n }\n\n const normalized = normalizePath(rawPath);\n const facts: RequestFacts = {\n method: (input.method ?? \"GET\").toUpperCase(),\n path: normalized,\n query: parseQuery(queryStart === -1 ? \"\" : url.slice(queryStart + 1)),\n headers,\n headerOrder: extractOrder(input.rawHeaders, headers),\n ip: normalizeIp(input.ip) ?? input.ip,\n timestamp: input.timestamp ?? Date.now(),\n };\n\n // How the target was spelled, when that is not how it reads.\n //\n // Normalisation is what makes a policy hold — `/admin` and `/%61dmin` must be one path\n // to a rule — but it also destroys the evidence that somebody spelled it the second\n // way. `/%2e%2e%2f%2e%2e%2fapp/config.yml` arrives here and leaves as\n // `/app/config.yml`, which looks like an ordinary page nobody has, and the one fact\n // that made it interesting is gone. So the original is kept for `target-integrity` to\n // read — and only when it differs, because on ordinary traffic it does not, and a\n // second copy of every path is a string per request to say nothing.\n if (rawPath !== normalized) facts.rawPath = rawPath.length > MAX_RAW_PATH ? rawPath.slice(0, MAX_RAW_PATH) : rawPath;\n\n // Only parse cookies that exist. Most bot traffic carries none, and building an\n // empty bag for every one of those requests is pure garbage.\n const cookieHeader = headers[\"cookie\"];\n if (cookieHeader !== undefined) facts.cookies = parseCookies(cookieHeader);\n\n if (input.protocol !== undefined) facts.protocol = input.protocol;\n if (input.httpVersion !== undefined) facts.httpVersion = input.httpVersion;\n if (input.tlsFingerprint !== undefined) facts.tlsFingerprint = input.tlsFingerprint;\n if (input.partialHeaders === true) facts.partialHeaders = true;\n if (input.extra !== undefined) facts.extra = input.extra;\n\n return facts;\n}\n\n/**\n * Decodes and normalises a path.\n *\n * Rule matching is done on this value, so `/admin`, `/%61dmin` and `/./admin` must\n * not be three different paths as far as a policy is concerned — otherwise a rule\n * scoped to a path is trivially side-stepped by spelling it differently. Decoding is\n * single-pass: repeatedly decoding until it stops changing is how `%2525` becomes `%`\n * and how path-traversal filters get bypassed.\n */\nfunction normalizePath(rawPath: string): string {\n let path = rawPath;\n try {\n path = decodeURIComponent(rawPath);\n } catch {\n // Malformed percent-encoding. Keep the raw form: it is still a fact about the\n // request, and guessing at an intended decoding would be inventing one.\n }\n path = path.replace(/\\\\/g, \"/\").replace(/\\/{2,}/g, \"/\");\n if (!path.startsWith(\"/\")) path = `/${path}`;\n\n // Resolve `.` and `..` segments so a rule on a path prefix cannot be walked around.\n if (path.includes(\"./\")) {\n const resolved: string[] = [];\n for (const segment of path.split(\"/\")) {\n if (segment === \"\" || segment === \".\") continue;\n if (segment === \"..\") resolved.pop();\n else resolved.push(segment);\n }\n path = `/${resolved.join(\"/\")}`;\n }\n return path.length > 1 && path.endsWith(\"/\") ? path.slice(0, -1) : path;\n}\n\nfunction parseQuery(search: string): Record<string, string> {\n // Null-prototype: a literal `?__proto__=x` becomes an ordinary own key instead of\n // hitting the prototype setter and vanishing, so a detector can actually see it.\n const query: Record<string, string> = Object.create(null) as Record<string, string>;\n if (search.length === 0) return query;\n let count = 0;\n for (const [key, value] of new URLSearchParams(boundedSearch(search))) {\n if (count++ >= MAX_QUERY_PARAMS) break;\n query[key] = value.length > 1024 ? value.slice(0, 1024) : value;\n }\n return query;\n}\n\n/**\n * The leading part of a query string that can hold at most `MAX_QUERY_PARAMS` of them.\n *\n * `URLSearchParams` parses and percent-decodes its whole input up front, so the cap in\n * `parseQuery` bounded how many parameters were *kept* and not how many were *parsed*.\n * A URL carrying a thousand of them paid for a thousand decodes to keep sixty-four, and\n * a request cost five times an ordinary one for as long as the client cared to make it.\n * `MAX_URL_LENGTH` capped that at eight kilobytes of parsing rather than at sixty-four\n * parameters, which is a ceiling but not a bound worth having.\n *\n * Cutting the string first keeps the parser — and with it every rule about `+`, about a\n * half-written `%E4`, about a repeated key — while giving it only what it can use. The\n * cut lands on a separator, so no escape and no multi-byte character is split, and empty\n * pairs are skipped here for the same reason `URLSearchParams` skips them: they are not\n * parameters and must not spend the budget.\n */\nfunction boundedSearch(search: string): string {\n let seen = 0;\n // `URLSearchParams` strips one leading \"?\" before it splits, so counting it as a\n // segment would spend a parameter of the budget on something that never becomes one.\n // Reachable from a URL of \"/p??a=1\", and found by differential fuzzing against the\n // parser this stands in front of — every disagreement was a query beginning \"?&\".\n let at = search.charCodeAt(0) === 63 ? 1 : 0;\n while (at < search.length) {\n let end = search.indexOf(\"&\", at);\n if (end === -1) end = search.length;\n if (end !== at && ++seen > MAX_QUERY_PARAMS) return search.slice(0, at - 1);\n at = end + 1;\n }\n return search;\n}\n\n/**\n * Extracts wire-order header names.\n *\n * Both documented input shapes have to be told apart: Node's `rawHeaders`, which\n * alternates name and value, and a plain list of names.\n *\n * Asking whether the even-indexed entries *look like* header names cannot do it, and\n * used to get the common case backwards. Every entry of a name-only list looks like a\n * header name, so any such list of even length was read as Node-style and every second\n * name was discarded — silently halving the header order, which is a fingerprint, for\n * a shape the documentation invites callers to pass.\n *\n * The header map decides it instead. In a name-only list every entry is a header that\n * was actually received; in an alternating list the odd entries are values, which are\n * almost never also header names. That distinguishes the two even for a single-header\n * request, where any shape-based guess is ambiguous.\n */\nconst MAX_ORDERED_HEADERS = 64;\n\nfunction extractOrder(rawHeaders: readonly string[] | undefined, headers: Record<string, string | undefined>): readonly string[] {\n if (!rawHeaders || rawHeaders.length === 0) return EMPTY_ORDER;\n\n let isNodeStyle = rawHeaders.length % 2 === 0;\n if (isNodeStyle) {\n let everyEntryIsAHeader = true;\n for (let i = 0; i < rawHeaders.length; i++) {\n const entry = rawHeaders[i]!;\n const known = headers[entry.toLowerCase()] !== undefined;\n // An even entry that is neither a received header nor even a valid token cannot\n // be a name in either shape; fall back to reading the list as names rather than\n // dropping half of something unrecognised.\n if (i % 2 === 0 && !known && !isHeaderName(entry)) {\n isNodeStyle = false;\n break;\n }\n if (!known) everyEntryIsAHeader = false;\n }\n if (everyEntryIsAHeader) isNodeStyle = false;\n }\n\n // One pass, one array. The previous version walked the input three times —\n // `every`, then `filter`, then `slice().map()` — allocating at each step.\n const step = isNodeStyle ? 2 : 1;\n const order: string[] = [];\n for (let i = 0; i < rawHeaders.length && order.length < MAX_ORDERED_HEADERS; i += step) {\n order.push(rawHeaders[i]!.toLowerCase());\n }\n return order;\n}\n\nconst EMPTY_ORDER: readonly string[] = Object.freeze([]);\n\n/** RFC 9110 token characters. A scan rather than a regex: this runs once per header. */\nfunction isHeaderName(value: string): boolean {\n if (value.length === 0) return false;\n for (let i = 0; i < value.length; i++) {\n const code = value.charCodeAt(i);\n const ok =\n (code >= 0x30 && code <= 0x39) || // 0-9\n (code >= 0x41 && code <= 0x5a) || // A-Z\n (code >= 0x61 && code <= 0x7a) || // a-z\n code === 0x21 || (code >= 0x23 && code <= 0x27) || code === 0x2a || code === 0x2b ||\n code === 0x2d || code === 0x2e || code === 0x5e || code === 0x5f || code === 0x60 ||\n code === 0x7c || code === 0x7e;\n if (!ok) return false;\n }\n return true;\n}\n","import type { IncomingMessage } from \"node:http\";\nimport type { ActionOutcome } from \"../actions/types.js\";\n\n/** Largest challenge-solution body we will read. A solution is a few hundred bytes. */\nexport const MAX_VERIFY_BODY = 4096;\n\n/**\n * Waits, with the timer unreferenced.\n *\n * The `unref` is what stops a pending `delay` action from holding a process open at\n * shutdown — which in a serverless runtime is the difference between a request that\n * finishes and an invocation that is billed until it is killed.\n */\nexport function pause(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n (timer as { unref?: () => void }).unref?.();\n });\n}\n\n/** True when the outcome means the request should reach the application. */\nexport function isContinue(outcome: ActionOutcome): outcome is Extract<ActionOutcome, { kind: \"continue\" }> {\n return outcome.kind === \"continue\";\n}\n\n/**\n * Parses a JSON body defensively.\n *\n * Returns `undefined` rather than throwing on anything malformed. This parses input\n * from a client that is, by construction, already under suspicion.\n */\nexport function parseJson(text: string): unknown {\n if (text.length === 0 || text.length > MAX_VERIFY_BODY) return undefined;\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Reads at most {@link MAX_VERIFY_BODY} bytes from a Node request stream.\n *\n * The cap is enforced by destroying the socket rather than by buffering and then\n * discarding: a client that keeps sending after the limit is spending its own\n * bandwidth against a buffer that stopped growing. Errors resolve to an empty string,\n * because a malformed submission is a rejected challenge, not an exception.\n */\nexport function readBoundedBody(request: IncomingMessage): Promise<string> {\n return new Promise((resolve) => {\n let data = \"\";\n let bytes = 0;\n request.on(\"data\", (chunk: Buffer) => {\n bytes += chunk.length;\n if (bytes > MAX_VERIFY_BODY) {\n request.destroy();\n return;\n }\n data += chunk.toString(\"utf8\");\n });\n request.on(\"end\", () => resolve(data));\n request.on(\"error\", () => resolve(\"\"));\n });\n}\n\n/** The JSON body of a challenge verification response, shared by every adapter. */\nexport function verificationBody(outcome: { ok: true } | { ok: false; reason: string }): string {\n return JSON.stringify(outcome.ok ? { ok: true } : { ok: false, error: outcome.reason });\n}\n","import { createFacts } from \"../facts.js\";\nimport { pause, parseJson, readBoundedBody, verificationBody } from \"./shared.js\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { BotHandler } from \"../core.js\";\nimport type { RequestFacts } from \"../types.js\";\n\nexport type NextFunction = (error?: unknown) => void;\nexport type NodeMiddleware = (request: IncomingMessage, response: ServerResponse, next: NextFunction) => void;\n\nexport interface NodeMiddlewareOptions {\n /**\n * Extracts a TLS fingerprint your edge computed, e.g. `\"x-ja3-hash\"` or\n * Cloudflare's `\"cf-ja3-hash\"`. Only set it if the header cannot be forged by a\n * client — that is, your edge always overwrites it.\n */\n tlsFingerprintHeader?: string;\n /** Also handle the challenge verification endpoint. Default true. */\n mountChallengeEndpoint?: boolean;\n /** Extra facts, e.g. a session id for a better actor key. */\n enrich?: (request: IncomingMessage, facts: RequestFacts) => RequestFacts;\n}\n\n/**\n * Connect/Express middleware. Also works with a bare `node:http` server.\n *\n * Mount it early — ahead of your routes, behind anything that terminates TLS and\n * sets forwarded headers — and after your body parser only if you need one, since\n * this reads no body except on its own verification endpoint.\n *\n * `next()` is always reached unless a response was actually produced, and any\n * unexpected error inside the engine results in the request being served rather than\n * refused. A bot filter that fails closed is an outage with extra steps.\n */\nexport function botHandler(handler: BotHandler, options: NodeMiddlewareOptions = {}): NodeMiddleware {\n const mountChallenge = options.mountChallengeEndpoint ?? true;\n\n return function botHandlerMiddleware(request, response, next): void {\n let handedOff = false;\n void (async (): Promise<void> => {\n try {\n const headers: Record<string, string | undefined> = {};\n for (const [name, value] of Object.entries(request.headers)) {\n headers[name] = Array.isArray(value) ? value.join(\", \") : value;\n }\n\n const ip = handler.resolveIp(request.socket.remoteAddress, headers);\n const fingerprint = options.tlsFingerprintHeader !== undefined ? headers[options.tlsFingerprintHeader.toLowerCase()] : undefined;\n\n let facts = createFacts({\n method: request.method,\n url: request.url,\n headers: request.headers,\n rawHeaders: request.rawHeaders,\n ip,\n httpVersion: request.httpVersion,\n // `encrypted` is present only on a TLS socket. Behind a terminating proxy\n // the forwarded protocol header is the only truth available.\n protocol: (request.socket as { encrypted?: boolean }).encrypted === true || headers[\"x-forwarded-proto\"] === \"https\" ? \"https\" : \"http\",\n ...(fingerprint !== undefined ? { tlsFingerprint: fingerprint } : {}),\n });\n if (options.enrich) facts = options.enrich(request, facts);\n\n if (mountChallenge && handler.isChallengeEndpoint(facts)) {\n await serveVerification(handler, request, response, facts);\n return;\n }\n\n const { outcome } = await handler.handle(facts);\n\n if (outcome.kind === \"drop\") {\n request.socket.destroy();\n return;\n }\n\n if (outcome.kind === \"respond\") {\n response.statusCode = outcome.status;\n for (const [name, value] of Object.entries(outcome.headers)) response.setHeader(name, value);\n response.end(outcome.body);\n return;\n }\n\n for (const [name, value] of Object.entries(outcome.requestHeaders ?? {})) {\n request.headers[name] = value;\n }\n if (outcome.responseHeaders) {\n for (const [name, value] of Object.entries(outcome.responseHeaders)) response.setHeader(name, value);\n }\n if (outcome.delayMs !== undefined) await pause(outcome.delayMs);\n // What the application answers is the one thing detection cannot see for itself:\n // the verdict above was reached before this response existed, which is what lets\n // it shape the response. Reported back on the way out, it feeds `probe-volume` —\n // an actor whose requests are almost all misses is looking for something rather\n // than reading anything. One listener, and nothing depends on it arriving.\n // Guarded rather than called outright: this runs inside the try that fails the\n // request open, so a response object without `once` — an exotic framework\n // wrapper, a test double — would turn every ordinary request into a reported\n // adapter error and a trip through the error path. The outcome is optional\n // information; not being able to collect it is not a failure worth charging\n // anybody for.\n if (typeof response.once === \"function\") {\n response.once(\"finish\", () => {\n handler.recordOutcome(facts, response.statusCode);\n });\n }\n handedOff = true;\n next();\n } catch (error) {\n // Fail open, loudly: reported to the operator, invisible to the visitor.\n // `next(error)` — which this used to call — routes into Express's error\n // handler and answers 500, which is failing *closed* and contradicts the\n // guarantee stated above.\n //\n // `handedOff` guards the case where the throw came from downstream rather\n // than from us: once `next()` has run, the rest of the application owns the\n // request, and calling `next` a second time would run it twice.\n failOpen(handler, error, \"adapter:node\");\n if (!handedOff && !response.headersSent) next();\n }\n })();\n };\n}\n\n/**\n * Reports an error the engine raised and says whether the request can still be served.\n *\n * Every adapter promises the same thing in its own doc comment: an unexpected failure\n * inside detection serves the request rather than refusing it, because a bot filter\n * that fails closed is an outage with extra steps. Handing the error to the\n * framework's error path does the opposite — Express, Koa and Fastify all turn it into\n * a 500 — so a bug in this library became a broken page for a real visitor. The error\n * is reported through the operator's own `onError`, which is where it is useful, and\n * the visitor gets their page.\n */\nfunction failOpen(handler: BotHandler, error: unknown, source: string): void {\n handler.config.onError(error, { source });\n}\n\nasync function serveVerification(handler: BotHandler, request: IncomingMessage, response: ServerResponse, facts: RequestFacts): Promise<void> {\n const outcome = await handler.verifyChallenge(facts, parseJson(await readBoundedBody(request)));\n response.setHeader(\"content-type\", \"application/json; charset=utf-8\");\n response.setHeader(\"cache-control\", \"no-store\");\n if (outcome.ok) response.setHeader(\"set-cookie\", outcome.setCookie);\n response.statusCode = outcome.ok ? 200 : outcome.status;\n response.end(verificationBody(outcome));\n}\n","import { createFacts } from \"../facts.js\";\nimport { MAX_VERIFY_BODY, parseJson, pause } from \"./shared.js\";\nimport type { BotHandler } from \"../core.js\";\nimport type { HandleResult } from \"../core.js\";\nimport type { RequestFacts } from \"../types.js\";\n\nexport interface FetchAdapterOptions {\n /**\n * Headers to read the client address from, in order of preference.\n *\n * Only headers your edge *overwrites* belong here. One a client can set is one it\n * can use to choose its own identity — and with it the address you rate-limit,\n * allowlist, denylist and block on. `x-forwarded-for` is deliberately **not** in the\n * default list: it is appended to rather than replaced, so its leftmost entry is\n * whatever the client wrote. List it explicitly only if you know your edge replaces\n * the whole header.\n */\n ipHeaders?: readonly string[];\n /** Reads the address from the platform's own context object, e.g. a Workers `ConnInfo`. */\n clientIp?: (request: Request, context: unknown) => string | undefined;\n /** Header carrying an edge-computed JA3/JA4 fingerprint. */\n tlsFingerprintHeader?: string;\n mountChallengeEndpoint?: boolean;\n enrich?: (request: Request, facts: RequestFacts) => RequestFacts;\n}\n\n/**\n * Address headers trusted without being asked for.\n *\n * Both are single-valued and written by the edge that terminates the connection, so a\n * client cannot choose what they say. `x-forwarded-for` used to be here and is not any\n * more: it is a chain a proxy *appends* to, so its leftmost entry — the one this\n * adapter reads — is supplied by the client. Trusting it by default let anyone pick\n * their own address and walk straight past a denylist, a rate limit and every\n * behavioural signal keyed on the actor. The other three adapters never did: they go\n * through `resolveIp`, where reading a forwarded header at all requires\n * `proxy.trustProxy` and the configuration warns at length about exactly this. This\n * one now agrees with them.\n */\nconst DEFAULT_IP_HEADERS = [\"cf-connecting-ip\", \"x-real-ip\"] as const;\n\nexport interface FetchDecision {\n /** Serve this instead of calling your handler. `undefined` means carry on. */\n response?: Response | undefined;\n /** The request to pass on, carrying the verdict headers. */\n request: Request;\n /**\n * What the engine concluded — absent for a challenge verification request, which is\n * answered without being assessed.\n *\n * Reported as missing rather than filled in with a placeholder assessment: \"we did\n * not judge this request\" and \"we judged it and found nothing\" must not look the\n * same to whatever is reading this.\n */\n result?: HandleResult | undefined;\n}\n\n/**\n * Web-standard adapter, for Cloudflare Workers, Deno, Bun, Vercel Edge and anything\n * else built on `Request`/`Response`.\n *\n * Returns a decision rather than wrapping your handler, so the same primitive fits a\n * router, a middleware chain or a plain `fetch` export. {@link withBotHandler} wraps it\n * for the common case.\n *\n * Note that the engine uses `node:crypto` for HMAC and hashing. On Workers that means\n * enabling `nodejs_compat`; Deno and Bun provide it natively.\n */\nexport function createFetchAdapter(handler: BotHandler, options: FetchAdapterOptions = {}) {\n const ipHeaders = options.ipHeaders ?? DEFAULT_IP_HEADERS;\n const mountChallenge = options.mountChallengeEndpoint ?? true;\n let warnedAboutAddress = false;\n\n return async function evaluate(request: Request, context: unknown = undefined): Promise<FetchDecision> {\n try {\n return await decide(request, context);\n } catch (error) {\n // Fail open, loudly — the same promise the node, Koa and Fastify adapters make,\n // and the one this adapter was not keeping. An error escaping here reaches the\n // runtime's own handler, and Workers, Deno and Bun all answer 500: a detection\n // bug charged to the visitor. Returning the request unchanged, with no verdict,\n // serves the page instead; `result` is absent, which already means \"we did not\n // judge this request\" to everything downstream.\n handler.config.onError(error, { source: \"adapter:fetch\" });\n return { request };\n }\n };\n\n async function decide(request: Request, context: unknown): Promise<FetchDecision> {\n const headers: Record<string, string | undefined> = {};\n const order: string[] = [];\n for (const [name, value] of request.headers) {\n headers[name] = value;\n order.push(name);\n }\n\n const url = new URL(request.url);\n const ip = options.clientIp?.(request, context) ?? firstHeaderAddress(headers, ipHeaders) ?? \"\";\n // An empty address is not a harmless blank: it becomes the actor key, so every\n // visitor collapses into one actor and every rate limit and behavioural signal\n // becomes site-wide. Said once, because it is a deployment mistake rather than a\n // per-request event, and a warning on every request is a warning nobody reads.\n if (ip === \"\" && !warnedAboutAddress) {\n warnedAboutAddress = true;\n // Through the handler rather than straight to the callback, so a subscriber —\n // and the dashboard's notices panel — hears about it too.\n handler.warn(\n `The fetch adapter could not determine a client address: none of ${ipHeaders.join(\", \")} is present and no \\`clientIp\\` was supplied. ` +\n `Every request will be tracked under one empty actor key, so rate limits and behavioural detection apply to your whole site at once. ` +\n `Pass \\`clientIp\\` to read the address from your platform's connection info, or list the header your edge sets.`,\n );\n }\n const fingerprint = options.tlsFingerprintHeader !== undefined ? headers[options.tlsFingerprintHeader.toLowerCase()] : undefined;\n\n let facts = createFacts({\n method: request.method,\n url: `${url.pathname}${url.search}`,\n headers,\n // The Fetch API normalises and sorts headers, so wire order is genuinely\n // unavailable here. Passing the sorted order would be worse than passing none:\n // the header-order detector would compare against an ordering no client chose.\n rawHeaders: [],\n ip,\n protocol: url.protocol === \"https:\" ? \"https\" : \"http\",\n ...(fingerprint !== undefined ? { tlsFingerprint: fingerprint } : {}),\n });\n if (options.enrich) facts = options.enrich(request, facts);\n\n if (mountChallenge && handler.isChallengeEndpoint(facts)) {\n const body = await readBounded(request);\n const outcome = await handler.verifyChallenge(facts, parseJson(body));\n const responseHeaders = new Headers({ \"content-type\": \"application/json; charset=utf-8\", \"cache-control\": \"no-store\" });\n if (outcome.ok) responseHeaders.set(\"set-cookie\", outcome.setCookie);\n // Deliberately not assessed. `assess` records the request against the actor, so\n // running it here inflated the request count and arrival cadence of the one\n // client that is trying to answer a challenge — feeding the rate and cadence\n // detectors with the evidence of its own compliance. The comment here already\n // said the request was not assessed; the code called `assess` anyway.\n return {\n response: new Response(JSON.stringify(outcome.ok ? { ok: true } : { ok: false, error: outcome.reason }), {\n status: outcome.ok ? 200 : outcome.status,\n headers: responseHeaders,\n }),\n request,\n };\n }\n\n const result = await handler.handle(facts);\n const { outcome } = result;\n\n if (outcome.kind === \"drop\") {\n // A Fetch runtime cannot close a connection without answering, so the nearest\n // equivalent is an empty 444-style refusal. Say so rather than pretend.\n return { response: new Response(null, { status: 444 }), request, result };\n }\n\n if (outcome.kind === \"respond\") {\n return { response: new Response(outcome.body, { status: outcome.status, headers: outcome.headers }), request, result };\n }\n\n if (outcome.delayMs !== undefined) await pause(outcome.delayMs);\n\n // Requests are immutable, so tagging means constructing a new one.\n const tagged = outcome.requestHeaders\n ? new Request(request, { headers: mergeHeaders(request.headers, outcome.requestHeaders) })\n : request;\n\n return { request: tagged, result };\n }\n}\n\n/** Wraps a handler. The common case: one call, one line. */\nexport function withBotHandler(\n handler: BotHandler,\n next: (request: Request, context: unknown) => Response | Promise<Response>,\n options: FetchAdapterOptions = {},\n): (request: Request, context?: unknown) => Promise<Response> {\n const evaluate = createFetchAdapter(handler, options);\n return async (request, context = undefined) => {\n const decision = await evaluate(request, context);\n if (decision.response) return decision.response;\n const response = await next(decision.request, context);\n // What the application answered is the one thing detection cannot see for itself,\n // and it feeds `probe-volume`: an actor whose requests are almost all misses is\n // looking for something rather than reading anything. Recorded here for the same\n // reason the Node adapter records it on `finish` — without it that detector is\n // installed and silently inert on every Fetch runtime.\n if (decision.result !== undefined) handler.recordOutcome(decision.result.assessment.facts, response.status);\n const extra = decision.result?.outcome.kind === \"continue\" ? decision.result.outcome.responseHeaders : undefined;\n if (!extra) return response;\n const merged = new Response(response.body, response);\n for (const [name, value] of Object.entries(extra)) merged.headers.set(name, value);\n return merged;\n };\n}\n\nfunction firstHeaderAddress(headers: Record<string, string | undefined>, names: readonly string[]): string | undefined {\n for (const name of names) {\n const value = headers[name];\n if (value === undefined) continue;\n // A forwarded chain is left-to-right, oldest first. The leftmost entry is the\n // one the client itself can write, so it is taken only from headers the caller\n // has declared trustworthy by listing them.\n const first = value.split(\",\")[0]?.trim();\n if (first !== undefined && first.length > 0) return first;\n }\n return undefined;\n}\n\nfunction mergeHeaders(original: Headers, additions: Record<string, string>): Headers {\n const merged = new Headers(original);\n for (const [name, value] of Object.entries(additions)) merged.set(name, value);\n return merged;\n}\n\nasync function readBounded(request: Request): Promise<string> {\n const text = await request.clone().text();\n return text.length > MAX_VERIFY_BODY ? \"\" : text;\n}\n","import { createFacts } from \"../facts.js\";\nimport { pause, parseJson, readBoundedBody, verificationBody } from \"./shared.js\";\nimport type { BotHandler } from \"../core.js\";\nimport type { RequestFacts } from \"../types.js\";\n\n/**\n * The parts of Fastify's request and reply this adapter touches, described\n * structurally so the library takes no dependency on Fastify or its types.\n */\nexport interface FastifyLikeRequest {\n method: string;\n url: string;\n headers: Record<string, string | string[] | undefined>;\n raw: import(\"node:http\").IncomingMessage;\n}\n\nexport interface FastifyLikeReply {\n code(status: number): FastifyLikeReply;\n header(name: string, value: string): FastifyLikeReply;\n send(body: unknown): unknown;\n hijack?(): void;\n /**\n * The underlying response. Optional because this type describes the shape the adapter\n * needs rather than Fastify's own, and a test double should not have to build one —\n * but Fastify always provides it, and without it the status this request ends up\n * answering is never reported back.\n */\n raw?: { once(event: \"finish\", listener: () => void): unknown; statusCode: number };\n}\n\nexport interface FastifyAdapterOptions {\n tlsFingerprintHeader?: string;\n /**\n * Also serve the challenge verification endpoint. Default true.\n *\n * Fastify parses bodies after `onRequest`, so this reads the raw stream itself.\n * That is why it must run here rather than as a route: by the time a route handler\n * sees the request, the body has been consumed by a parser that does not know about\n * this endpoint.\n */\n mountChallengeEndpoint?: boolean;\n enrich?: (request: FastifyLikeRequest, facts: RequestFacts) => RequestFacts;\n}\n\n/**\n * An `onRequest` hook for Fastify.\n *\n * ```ts\n * fastify.addHook(\"onRequest\", fastifyBotHandler(handler));\n * ```\n *\n * `onRequest` rather than `preHandler` on purpose: it is the earliest hook, so a\n * refused request never reaches routing, validation or your body parser — none of\n * which should be doing work for traffic that has already been decided about.\n */\nexport function fastifyBotHandler(handler: BotHandler, options: FastifyAdapterOptions = {}) {\n const mountChallenge = options.mountChallengeEndpoint ?? true;\n\n return async function onRequest(request: FastifyLikeRequest, reply: FastifyLikeReply): Promise<void> {\n try {\n await evaluate(handler, options, mountChallenge, request, reply);\n } catch (error) {\n // Returning normally lets routing continue. Letting the throw reach Fastify\n // would answer 500 instead — a detection bug charged to the visitor, which is\n // the one thing every adapter here promises not to do.\n handler.config.onError(error, { source: \"adapter:fastify\" });\n }\n };\n}\n\nasync function evaluate(\n handler: BotHandler,\n options: FastifyAdapterOptions,\n mountChallenge: boolean,\n request: FastifyLikeRequest,\n reply: FastifyLikeReply,\n): Promise<void> {\n const headers: Record<string, string | undefined> = {};\n for (const [name, value] of Object.entries(request.headers)) {\n headers[name] = Array.isArray(value) ? value.join(\", \") : value;\n }\n\n const ip = handler.resolveIp(request.raw.socket.remoteAddress, headers);\n const fingerprint = options.tlsFingerprintHeader !== undefined ? headers[options.tlsFingerprintHeader.toLowerCase()] : undefined;\n\n let facts = createFacts({\n method: request.method,\n url: request.url,\n headers: request.headers,\n rawHeaders: request.raw.rawHeaders,\n ip,\n httpVersion: request.raw.httpVersion,\n protocol: (request.raw.socket as { encrypted?: boolean }).encrypted === true || headers[\"x-forwarded-proto\"] === \"https\" ? \"https\" : \"http\",\n ...(fingerprint !== undefined ? { tlsFingerprint: fingerprint } : {}),\n });\n if (options.enrich) facts = options.enrich(request, facts);\n\n if (mountChallenge && handler.isChallengeEndpoint(facts)) {\n const verification = await handler.verifyChallenge(facts, parseJson(await readBoundedBody(request.raw)));\n reply.code(verification.ok ? 200 : verification.status);\n reply.header(\"content-type\", \"application/json; charset=utf-8\");\n reply.header(\"cache-control\", \"no-store\");\n if (verification.ok) reply.header(\"set-cookie\", verification.setCookie);\n reply.send(verificationBody(verification));\n return;\n }\n\n const { outcome } = await handler.handle(facts);\n\n if (outcome.kind === \"drop\") {\n // `hijack` tells Fastify this route will not produce a reply, so it does not\n // try to serialise one for a socket that is about to disappear.\n reply.hijack?.();\n request.raw.socket.destroy();\n return;\n }\n\n if (outcome.kind === \"respond\") {\n reply.code(outcome.status);\n for (const [name, value] of Object.entries(outcome.headers)) reply.header(name, value);\n reply.send(outcome.body);\n return;\n }\n\n for (const [name, value] of Object.entries(outcome.requestHeaders ?? {})) {\n request.headers[name] = value;\n }\n if (outcome.responseHeaders) {\n for (const [name, value] of Object.entries(outcome.responseHeaders)) reply.header(name, value);\n }\n if (outcome.delayMs !== undefined) await pause(outcome.delayMs);\n\n // What the route answers is the one thing detection cannot see for itself, and it\n // feeds `probe-volume`: an actor whose requests are almost all misses is looking for\n // something rather than reading anything. `onRequest` returns long before the route\n // runs, so the status is collected on the way out, exactly as the Node adapter does\n // it. Without this the detector is installed and silently inert under Fastify.\n const raw = reply.raw;\n if (raw !== undefined) raw.once(\"finish\", () => handler.recordOutcome(facts, raw.statusCode));\n}\n","import { createFacts } from \"../facts.js\";\nimport { pause, parseJson, readBoundedBody, verificationBody } from \"./shared.js\";\nimport type { BotHandler } from \"../core.js\";\nimport type { RequestFacts } from \"../types.js\";\n\n/** The parts of a Koa context this adapter touches, described structurally. */\nexport interface KoaLikeContext {\n method: string;\n url: string;\n headers: Record<string, string | string[] | undefined>;\n req: import(\"node:http\").IncomingMessage;\n status: number;\n body: unknown;\n set(name: string, value: string): void;\n secure?: boolean;\n}\n\nexport interface KoaAdapterOptions {\n /** Also serve the challenge verification endpoint. Default true. */\n mountChallengeEndpoint?: boolean;\n}\n\n/**\n * Koa middleware.\n *\n * ```ts\n * app.use(koaBotHandler(handler));\n * ```\n *\n * Place it above everything else. Koa's downstream-then-upstream flow means anything\n * mounted before this still runs for a request that is about to be refused.\n */\nexport function koaBotHandler(handler: BotHandler, options: KoaAdapterOptions = {}) {\n const mountChallenge = options.mountChallengeEndpoint ?? true;\n\n return async function botHandlerMiddleware(context: KoaLikeContext, next: () => Promise<void>): Promise<void> {\n let proceed: boolean;\n let facts: RequestFacts | undefined;\n try {\n const decision = await evaluate(handler, mountChallenge, context);\n proceed = decision.proceed;\n facts = decision.facts;\n } catch (error) {\n // Serve the request rather than let a detection bug become a 500. The try\n // covers the engine only: `next()` runs the rest of your application, and\n // catching *its* failures here would both swallow them and call `next` twice.\n handler.config.onError(error, { source: \"adapter:koa\" });\n proceed = true;\n }\n if (!proceed) return;\n await next();\n // The status the application settled on, reported back on the way out. It feeds\n // `probe-volume`, which without it is installed and silently inert under Koa.\n // Reading `ctx.status` rather than listening on the socket keeps this on Koa's own\n // terms, where the status is whatever the middleware chain last set.\n if (facts !== undefined) handler.recordOutcome(facts, context.status);\n };\n}\n\n/** Runs the engine and applies its outcome. Says whether the request continues downstream. */\nasync function evaluate(handler: BotHandler, mountChallenge: boolean, context: KoaLikeContext): Promise<{ proceed: boolean; facts?: RequestFacts }> {\n const headers: Record<string, string | undefined> = {};\n for (const [name, value] of Object.entries(context.headers)) {\n headers[name] = Array.isArray(value) ? value.join(\", \") : value;\n }\n\n const facts = createFacts({\n method: context.method,\n url: context.url,\n headers: context.headers,\n rawHeaders: context.req.rawHeaders,\n ip: handler.resolveIp(context.req.socket.remoteAddress, headers),\n httpVersion: context.req.httpVersion,\n protocol:\n context.secure === true || (context.req.socket as { encrypted?: boolean }).encrypted === true || headers[\"x-forwarded-proto\"] === \"https\" ? \"https\" : \"http\",\n });\n\n if (mountChallenge && handler.isChallengeEndpoint(facts)) {\n const verification = await handler.verifyChallenge(facts, parseJson(await readBoundedBody(context.req)));\n context.status = verification.ok ? 200 : verification.status;\n context.set(\"content-type\", \"application/json; charset=utf-8\");\n context.set(\"cache-control\", \"no-store\");\n if (verification.ok) context.set(\"set-cookie\", verification.setCookie);\n context.body = verificationBody(verification);\n return { proceed: false };\n }\n\n const { outcome } = await handler.handle(facts);\n\n if (outcome.kind === \"drop\") {\n context.req.socket.destroy();\n return { proceed: false };\n }\n\n if (outcome.kind === \"respond\") {\n context.status = outcome.status;\n for (const [name, value] of Object.entries(outcome.headers)) context.set(name, value);\n context.body = outcome.body;\n return { proceed: false };\n }\n\n for (const [name, value] of Object.entries(outcome.requestHeaders ?? {})) {\n context.req.headers[name] = value;\n }\n if (outcome.responseHeaders) {\n for (const [name, value] of Object.entries(outcome.responseHeaders)) context.set(name, value);\n }\n if (outcome.delayMs !== undefined) await pause(outcome.delayMs);\n return { proceed: true, facts };\n}\n"],"mappings":"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","names":["evaluate","outcome","evaluate"]}
@@ -264,6 +264,7 @@ var SPECIAL_USE = new IpRangeSet(SPECIAL_USE_RANGES);
264
264
 
265
265
  // src/facts.ts
266
266
  var MAX_URL_LENGTH = 8192;
267
+ var MAX_RAW_PATH = 512;
267
268
  var MAX_QUERY_PARAMS = 64;
268
269
  function createFacts(input) {
269
270
  const rawUrl = input.url ?? "/";
@@ -272,18 +273,21 @@ function createFacts(input) {
272
273
  const rawPath = queryStart === -1 ? url : url.slice(0, queryStart);
273
274
  const headers = /* @__PURE__ */ Object.create(null);
274
275
  for (const [name, value] of Object.entries(input.headers)) {
275
- const joined = joinHeaderValue(value);
276
- if (joined !== void 0) headers[name.toLowerCase()] = joined;
276
+ const lower = name.toLowerCase();
277
+ const joined = lower === "cookie" && Array.isArray(value) ? value.join("; ") : joinHeaderValue(value);
278
+ if (joined !== void 0) headers[lower] = joined;
277
279
  }
280
+ const normalized = normalizePath(rawPath);
278
281
  const facts = {
279
282
  method: (input.method ?? "GET").toUpperCase(),
280
- path: normalizePath(rawPath),
283
+ path: normalized,
281
284
  query: parseQuery(queryStart === -1 ? "" : url.slice(queryStart + 1)),
282
285
  headers,
283
286
  headerOrder: extractOrder(input.rawHeaders, headers),
284
287
  ip: normalizeIp(input.ip) ?? input.ip,
285
288
  timestamp: input.timestamp ?? Date.now()
286
289
  };
290
+ if (rawPath !== normalized) facts.rawPath = rawPath.length > MAX_RAW_PATH ? rawPath.slice(0, MAX_RAW_PATH) : rawPath;
287
291
  const cookieHeader = headers["cookie"];
288
292
  if (cookieHeader !== void 0) facts.cookies = parseCookies(cookieHeader);
289
293
  if (input.protocol !== void 0) facts.protocol = input.protocol;
@@ -316,12 +320,23 @@ function parseQuery(search) {
316
320
  const query = /* @__PURE__ */ Object.create(null);
317
321
  if (search.length === 0) return query;
318
322
  let count = 0;
319
- for (const [key, value] of new URLSearchParams(search)) {
323
+ for (const [key, value] of new URLSearchParams(boundedSearch(search))) {
320
324
  if (count++ >= MAX_QUERY_PARAMS) break;
321
325
  query[key] = value.length > 1024 ? value.slice(0, 1024) : value;
322
326
  }
323
327
  return query;
324
328
  }
329
+ function boundedSearch(search) {
330
+ let seen = 0;
331
+ let at = search.charCodeAt(0) === 63 ? 1 : 0;
332
+ while (at < search.length) {
333
+ let end = search.indexOf("&", at);
334
+ if (end === -1) end = search.length;
335
+ if (end !== at && ++seen > MAX_QUERY_PARAMS) return search.slice(0, at - 1);
336
+ at = end + 1;
337
+ }
338
+ return search;
339
+ }
325
340
  var MAX_ORDERED_HEADERS = 64;
326
341
  function extractOrder(rawHeaders, headers) {
327
342
  if (!rawHeaders || rawHeaders.length === 0) return EMPTY_ORDER;
@@ -444,6 +459,11 @@ function botHandler(handler, options = {}) {
444
459
  for (const [name, value] of Object.entries(outcome.responseHeaders)) response.setHeader(name, value);
445
460
  }
446
461
  if (outcome.delayMs !== void 0) await pause(outcome.delayMs);
462
+ if (typeof response.once === "function") {
463
+ response.once("finish", () => {
464
+ handler.recordOutcome(facts, response.statusCode);
465
+ });
466
+ }
447
467
  handedOff = true;
448
468
  next();
449
469
  } catch (error) {
@@ -540,6 +560,7 @@ function withBotHandler(handler, next, options = {}) {
540
560
  const decision = await evaluate3(request, context);
541
561
  if (decision.response) return decision.response;
542
562
  const response = await next(decision.request, context);
563
+ if (decision.result !== void 0) handler.recordOutcome(decision.result.assessment.facts, response.status);
543
564
  const extra = decision.result?.outcome.kind === "continue" ? decision.result.outcome.responseHeaders : void 0;
544
565
  if (!extra) return response;
545
566
  const merged = new Response(response.body, response);
@@ -623,6 +644,8 @@ async function evaluate(handler, options, mountChallenge, request, reply) {
623
644
  for (const [name, value] of Object.entries(outcome.responseHeaders)) reply.header(name, value);
624
645
  }
625
646
  if (outcome.delayMs !== void 0) await pause(outcome.delayMs);
647
+ const raw = reply.raw;
648
+ if (raw !== void 0) raw.once("finish", () => handler.recordOutcome(facts, raw.statusCode));
626
649
  }
627
650
 
628
651
  // src/adapters/koa.ts
@@ -630,13 +653,18 @@ function koaBotHandler(handler, options = {}) {
630
653
  const mountChallenge = options.mountChallengeEndpoint ?? true;
631
654
  return async function botHandlerMiddleware(context, next) {
632
655
  let proceed;
656
+ let facts;
633
657
  try {
634
- proceed = await evaluate2(handler, mountChallenge, context);
658
+ const decision = await evaluate2(handler, mountChallenge, context);
659
+ proceed = decision.proceed;
660
+ facts = decision.facts;
635
661
  } catch (error) {
636
662
  handler.config.onError(error, { source: "adapter:koa" });
637
663
  proceed = true;
638
664
  }
639
- if (proceed) await next();
665
+ if (!proceed) return;
666
+ await next();
667
+ if (facts !== void 0) handler.recordOutcome(facts, context.status);
640
668
  };
641
669
  }
642
670
  async function evaluate2(handler, mountChallenge, context) {
@@ -660,18 +688,18 @@ async function evaluate2(handler, mountChallenge, context) {
660
688
  context.set("cache-control", "no-store");
661
689
  if (verification.ok) context.set("set-cookie", verification.setCookie);
662
690
  context.body = verificationBody(verification);
663
- return false;
691
+ return { proceed: false };
664
692
  }
665
693
  const { outcome } = await handler.handle(facts);
666
694
  if (outcome.kind === "drop") {
667
695
  context.req.socket.destroy();
668
- return false;
696
+ return { proceed: false };
669
697
  }
670
698
  if (outcome.kind === "respond") {
671
699
  context.status = outcome.status;
672
700
  for (const [name, value] of Object.entries(outcome.headers)) context.set(name, value);
673
701
  context.body = outcome.body;
674
- return false;
702
+ return { proceed: false };
675
703
  }
676
704
  for (const [name, value] of Object.entries(outcome.requestHeaders ?? {})) {
677
705
  context.req.headers[name] = value;
@@ -680,7 +708,7 @@ async function evaluate2(handler, mountChallenge, context) {
680
708
  for (const [name, value] of Object.entries(outcome.responseHeaders)) context.set(name, value);
681
709
  }
682
710
  if (outcome.delayMs !== void 0) await pause(outcome.delayMs);
683
- return true;
711
+ return { proceed: true, facts };
684
712
  }
685
713
  export {
686
714
  botHandler,