@osqd/bothandlerjs 0.3.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 (186) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/LICENSE +102 -0
  3. package/README.md +308 -0
  4. package/bin/bothandlerjs.mjs +14 -0
  5. package/dist/actions/index.d.ts +22 -0
  6. package/dist/actions/types.d.ts +59 -0
  7. package/dist/adapters/fastify.d.ts +43 -0
  8. package/dist/adapters/fetch.d.ts +51 -0
  9. package/dist/adapters/index.cjs +723 -0
  10. package/dist/adapters/index.cjs.map +1 -0
  11. package/dist/adapters/index.d.ts +8 -0
  12. package/dist/adapters/index.js +692 -0
  13. package/dist/adapters/index.js.map +1 -0
  14. package/dist/adapters/koa.d.ts +27 -0
  15. package/dist/adapters/node.d.ts +29 -0
  16. package/dist/adapters/shared.d.ts +39 -0
  17. package/dist/audit.d.ts +172 -0
  18. package/dist/challenge/index.d.ts +176 -0
  19. package/dist/challenge/language.d.ts +57 -0
  20. package/dist/challenge/page.d.ts +42 -0
  21. package/dist/challenge/pow.d.ts +33 -0
  22. package/dist/challenge/token.d.ts +72 -0
  23. package/dist/cli.cjs +13388 -0
  24. package/dist/cli.cjs.map +1 -0
  25. package/dist/cli.d.ts +5 -0
  26. package/dist/cli.js +13358 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/client/index.cjs +139 -0
  29. package/dist/client/index.cjs.map +1 -0
  30. package/dist/client/index.d.ts +63 -0
  31. package/dist/client/index.js +112 -0
  32. package/dist/client/index.js.map +1 -0
  33. package/dist/config.d.ts +316 -0
  34. package/dist/core.d.ts +443 -0
  35. package/dist/corpus/adversarial.d.ts +2 -0
  36. package/dist/corpus/advertising-email.d.ts +2 -0
  37. package/dist/corpus/ai-crawlers.d.ts +2 -0
  38. package/dist/corpus/benign-bots.d.ts +2 -0
  39. package/dist/corpus/cdn-gateways.d.ts +2 -0
  40. package/dist/corpus/cookies.d.ts +49 -0
  41. package/dist/corpus/crawlers-regional.d.ts +2 -0
  42. package/dist/corpus/crawlers-vertical.d.ts +2 -0
  43. package/dist/corpus/headers.d.ts +403 -0
  44. package/dist/corpus/humans-apps.d.ts +2 -0
  45. package/dist/corpus/humans-browsers.d.ts +2 -0
  46. package/dist/corpus/humans.d.ts +2 -0
  47. package/dist/corpus/index.cjs +4196 -0
  48. package/dist/corpus/index.cjs.map +1 -0
  49. package/dist/corpus/index.d.ts +37 -0
  50. package/dist/corpus/index.js +4132 -0
  51. package/dist/corpus/index.js.map +1 -0
  52. package/dist/corpus/infrastructure.d.ts +17 -0
  53. package/dist/corpus/libraries-extended.d.ts +2 -0
  54. package/dist/corpus/ranges.d.ts +31 -0
  55. package/dist/corpus/reputation.d.ts +21 -0
  56. package/dist/corpus/runner.d.ts +130 -0
  57. package/dist/corpus/schema.d.ts +185 -0
  58. package/dist/corpus/tooling.d.ts +2 -0
  59. package/dist/corpus/unwanted.d.ts +2 -0
  60. package/dist/crawler-ranges.d.ts +113 -0
  61. package/dist/dashboard/client.generated.d.ts +7 -0
  62. package/dist/dashboard/feed.d.ts +186 -0
  63. package/dist/dashboard/index.d.ts +5 -0
  64. package/dist/dashboard/page.d.ts +51 -0
  65. package/dist/dashboard/parse-request.d.ts +28 -0
  66. package/dist/dashboard/preview.d.ts +45 -0
  67. package/dist/dashboard/server.d.ts +70 -0
  68. package/dist/dashboard/types.d.ts +714 -0
  69. package/dist/detectors/accept-signature.d.ts +6 -0
  70. package/dist/detectors/browsing-coherence.d.ts +44 -0
  71. package/dist/detectors/cadence.d.ts +28 -0
  72. package/dist/detectors/clearance.d.ts +25 -0
  73. package/dist/detectors/client-hints.d.ts +19 -0
  74. package/dist/detectors/client-signals.d.ts +27 -0
  75. package/dist/detectors/crawl-breadth.d.ts +28 -0
  76. package/dist/detectors/crawler-verification.d.ts +39 -0
  77. package/dist/detectors/fetch-metadata.d.ts +20 -0
  78. package/dist/detectors/header-integrity.d.ts +26 -0
  79. package/dist/detectors/header-order.d.ts +27 -0
  80. package/dist/detectors/identity-rotation.d.ts +34 -0
  81. package/dist/detectors/index.d.ts +56 -0
  82. package/dist/detectors/ip-intelligence.d.ts +30 -0
  83. package/dist/detectors/known-bots.d.ts +94 -0
  84. package/dist/detectors/probe-signature.d.ts +50 -0
  85. package/dist/detectors/rate-anomaly.d.ts +36 -0
  86. package/dist/detectors/self-identified.d.ts +33 -0
  87. package/dist/detectors/session-integrity.d.ts +24 -0
  88. package/dist/detectors/tls-fingerprint.d.ts +34 -0
  89. package/dist/detectors/trap.d.ts +98 -0
  90. package/dist/detectors/types.d.ts +70 -0
  91. package/dist/detectors/ua-coherence.d.ts +32 -0
  92. package/dist/evidence.d.ts +45 -0
  93. package/dist/facts.d.ts +46 -0
  94. package/dist/index.cjs +9600 -0
  95. package/dist/index.cjs.map +1 -0
  96. package/dist/index.d.ts +63 -0
  97. package/dist/index.js +9444 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/internal/async.d.ts +16 -0
  100. package/dist/internal/clock.d.ts +17 -0
  101. package/dist/internal/crypto.d.ts +26 -0
  102. package/dist/internal/dns.d.ts +100 -0
  103. package/dist/internal/emitter.d.ts +18 -0
  104. package/dist/internal/http.d.ts +22 -0
  105. package/dist/internal/ip.d.ts +73 -0
  106. package/dist/internal/lru.d.ts +34 -0
  107. package/dist/internal/matcher.d.ts +41 -0
  108. package/dist/internal/pattern.d.ts +14 -0
  109. package/dist/internal/ua.d.ts +92 -0
  110. package/dist/metrics.d.ts +120 -0
  111. package/dist/notify/hub.d.ts +70 -0
  112. package/dist/notify/index.d.ts +7 -0
  113. package/dist/notify/redact.d.ts +48 -0
  114. package/dist/notify/sinks.d.ts +62 -0
  115. package/dist/notify/types.d.ts +56 -0
  116. package/dist/policy/index.d.ts +7 -0
  117. package/dist/policy/match.d.ts +20 -0
  118. package/dist/policy/policy.d.ts +123 -0
  119. package/dist/policy/presets.d.ts +161 -0
  120. package/dist/policy/types.d.ts +155 -0
  121. package/dist/robots.d.ts +83 -0
  122. package/dist/state.d.ts +161 -0
  123. package/dist/stores/index.d.ts +5 -0
  124. package/dist/stores/memory.d.ts +33 -0
  125. package/dist/stores/redis.d.ts +59 -0
  126. package/dist/stores/types.d.ts +37 -0
  127. package/dist/types.d.ts +250 -0
  128. package/docs/challenge/index.md +152 -0
  129. package/docs/challenge/localisation.md +105 -0
  130. package/docs/concepts/actors.md +113 -0
  131. package/docs/concepts/evidence.md +133 -0
  132. package/docs/concepts/the-guard.md +121 -0
  133. package/docs/concepts/threat-model.md +120 -0
  134. package/docs/concepts/verdicts.md +108 -0
  135. package/docs/course/01-first-assessment.md +178 -0
  136. package/docs/course/02-proof-and-suspicion.md +164 -0
  137. package/docs/course/03-verdicts-and-scores.md +160 -0
  138. package/docs/course/04-the-guard.md +200 -0
  139. package/docs/course/05-detectors.md +178 -0
  140. package/docs/course/06-identity.md +161 -0
  141. package/docs/course/07-actors.md +178 -0
  142. package/docs/course/08-traps.md +183 -0
  143. package/docs/course/09-rules.md +224 -0
  144. package/docs/course/10-actions-and-presets.md +216 -0
  145. package/docs/course/11-the-challenge.md +227 -0
  146. package/docs/course/12-going-live.md +211 -0
  147. package/docs/course/13-operating-it.md +238 -0
  148. package/docs/course/14-scaling.md +195 -0
  149. package/docs/course/15-extending.md +259 -0
  150. package/docs/course/16-proving-it.md +266 -0
  151. package/docs/course/index.md +112 -0
  152. package/docs/design/decisions.md +216 -0
  153. package/docs/detection/client-signals.md +96 -0
  154. package/docs/detection/detectors.md +376 -0
  155. package/docs/detection/index.md +134 -0
  156. package/docs/detection/signatures.md +111 -0
  157. package/docs/detection/verification.md +123 -0
  158. package/docs/detection/writing-a-detector.md +145 -0
  159. package/docs/index.md +100 -0
  160. package/docs/integration/adapters.md +138 -0
  161. package/docs/integration/client-ip.md +114 -0
  162. package/docs/integration/index.md +55 -0
  163. package/docs/integration/stores.md +76 -0
  164. package/docs/operations/audit.md +116 -0
  165. package/docs/operations/dashboard.md +725 -0
  166. package/docs/operations/index.md +101 -0
  167. package/docs/operations/metrics.md +74 -0
  168. package/docs/operations/notifications.md +72 -0
  169. package/docs/operations/runtime-changes.md +118 -0
  170. package/docs/policy/actions.md +196 -0
  171. package/docs/policy/index.md +84 -0
  172. package/docs/policy/presets.md +191 -0
  173. package/docs/policy/robots.md +128 -0
  174. package/docs/policy/rules.md +134 -0
  175. package/docs/reference/api.md +126 -0
  176. package/docs/reference/configuration.md +175 -0
  177. package/docs/start/choosing-a-policy.md +127 -0
  178. package/docs/start/first-integration.md +127 -0
  179. package/docs/start/installation.md +63 -0
  180. package/docs/start/upgrading.md +72 -0
  181. package/docs/testing/cli.md +119 -0
  182. package/docs/testing/corpus.md +129 -0
  183. package/docs/testing/index.md +61 -0
  184. package/docs/testing/replay.md +102 -0
  185. package/docs/testing/try-it.md +189 -0
  186. package/package.json +145 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../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":["/** 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"]}
@@ -0,0 +1,27 @@
1
+ import type { BotHandler } from "../core.js";
2
+ /** The parts of a Koa context this adapter touches, described structurally. */
3
+ export interface KoaLikeContext {
4
+ method: string;
5
+ url: string;
6
+ headers: Record<string, string | string[] | undefined>;
7
+ req: import("node:http").IncomingMessage;
8
+ status: number;
9
+ body: unknown;
10
+ set(name: string, value: string): void;
11
+ secure?: boolean;
12
+ }
13
+ export interface KoaAdapterOptions {
14
+ /** Also serve the challenge verification endpoint. Default true. */
15
+ mountChallengeEndpoint?: boolean;
16
+ }
17
+ /**
18
+ * Koa middleware.
19
+ *
20
+ * ```ts
21
+ * app.use(koaBotHandler(handler));
22
+ * ```
23
+ *
24
+ * Place it above everything else. Koa's downstream-then-upstream flow means anything
25
+ * mounted before this still runs for a request that is about to be refused.
26
+ */
27
+ export declare function koaBotHandler(handler: BotHandler, options?: KoaAdapterOptions): (context: KoaLikeContext, next: () => Promise<void>) => Promise<void>;
@@ -0,0 +1,29 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { BotHandler } from "../core.js";
3
+ import type { RequestFacts } from "../types.js";
4
+ export type NextFunction = (error?: unknown) => void;
5
+ export type NodeMiddleware = (request: IncomingMessage, response: ServerResponse, next: NextFunction) => void;
6
+ export interface NodeMiddlewareOptions {
7
+ /**
8
+ * Extracts a TLS fingerprint your edge computed, e.g. `"x-ja3-hash"` or
9
+ * Cloudflare's `"cf-ja3-hash"`. Only set it if the header cannot be forged by a
10
+ * client — that is, your edge always overwrites it.
11
+ */
12
+ tlsFingerprintHeader?: string;
13
+ /** Also handle the challenge verification endpoint. Default true. */
14
+ mountChallengeEndpoint?: boolean;
15
+ /** Extra facts, e.g. a session id for a better actor key. */
16
+ enrich?: (request: IncomingMessage, facts: RequestFacts) => RequestFacts;
17
+ }
18
+ /**
19
+ * Connect/Express middleware. Also works with a bare `node:http` server.
20
+ *
21
+ * Mount it early — ahead of your routes, behind anything that terminates TLS and
22
+ * sets forwarded headers — and after your body parser only if you need one, since
23
+ * this reads no body except on its own verification endpoint.
24
+ *
25
+ * `next()` is always reached unless a response was actually produced, and any
26
+ * unexpected error inside the engine results in the request being served rather than
27
+ * refused. A bot filter that fails closed is an outage with extra steps.
28
+ */
29
+ export declare function botHandler(handler: BotHandler, options?: NodeMiddlewareOptions): NodeMiddleware;
@@ -0,0 +1,39 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { ActionOutcome } from "../actions/types.js";
3
+ /** Largest challenge-solution body we will read. A solution is a few hundred bytes. */
4
+ export declare const MAX_VERIFY_BODY = 4096;
5
+ /**
6
+ * Waits, with the timer unreferenced.
7
+ *
8
+ * The `unref` is what stops a pending `delay` action from holding a process open at
9
+ * shutdown — which in a serverless runtime is the difference between a request that
10
+ * finishes and an invocation that is billed until it is killed.
11
+ */
12
+ export declare function pause(ms: number): Promise<void>;
13
+ /** True when the outcome means the request should reach the application. */
14
+ export declare function isContinue(outcome: ActionOutcome): outcome is Extract<ActionOutcome, {
15
+ kind: "continue";
16
+ }>;
17
+ /**
18
+ * Parses a JSON body defensively.
19
+ *
20
+ * Returns `undefined` rather than throwing on anything malformed. This parses input
21
+ * from a client that is, by construction, already under suspicion.
22
+ */
23
+ export declare function parseJson(text: string): unknown;
24
+ /**
25
+ * Reads at most {@link MAX_VERIFY_BODY} bytes from a Node request stream.
26
+ *
27
+ * The cap is enforced by destroying the socket rather than by buffering and then
28
+ * discarding: a client that keeps sending after the limit is spending its own
29
+ * bandwidth against a buffer that stopped growing. Errors resolve to an empty string,
30
+ * because a malformed submission is a rejected challenge, not an exception.
31
+ */
32
+ export declare function readBoundedBody(request: IncomingMessage): Promise<string>;
33
+ /** The JSON body of a challenge verification response, shared by every adapter. */
34
+ export declare function verificationBody(outcome: {
35
+ ok: true;
36
+ } | {
37
+ ok: false;
38
+ reason: string;
39
+ }): string;
@@ -0,0 +1,172 @@
1
+ import type { Clock } from "./internal/clock.js";
2
+ import type { Assessment } from "./types.js";
3
+ import type { Decision } from "./policy/types.js";
4
+ /**
5
+ * Watching the shape of your traffic change.
6
+ *
7
+ * Counters tell you what is happening; they do not tell you that it is *unusual*.
8
+ * Bot traffic is not a level, it is an event — a scrape starts, a scanner sweeps a
9
+ * range, someone points a stuffing tool at your login form — and the number that
10
+ * matters is not "12% of requests are bots" but "12% today, 2% for the fortnight
11
+ * before". So this module keeps a short window and a longer baseline, compares them
12
+ * on a schedule, and raises a structured anomaly when the comparison clears a bar you
13
+ * set.
14
+ *
15
+ * Three properties make it safe to leave on:
16
+ *
17
+ * **It costs a handful of increments per request.** No allocation, no timestamp
18
+ * sorting, no history beyond a fixed ring of buckets.
19
+ *
20
+ * **It refuses to speak from a small sample.** Every check has a minimum, because a
21
+ * quiet site at 3am produces ratios like "800% more bots" from four requests, and an
22
+ * alerting system that cries wolf at 3am gets muted, which is worse than not having
23
+ * one.
24
+ *
25
+ * **It has a cooldown.** A spike lasting an hour is one event, not sixty.
26
+ */
27
+ /** What an anomaly is worth waking somebody for. */
28
+ export type AnomalySeverity = "info" | "warning" | "critical";
29
+ export interface TrafficAnomaly {
30
+ /** Stable id of the check that fired, e.g. `"bot-share-spike"`. */
31
+ id: string;
32
+ severity: AnomalySeverity;
33
+ /** One sentence, safe to put in an alert. */
34
+ summary: string;
35
+ /** The measure that moved, e.g. `"bot share"`. */
36
+ metric: string;
37
+ /** Value in the recent window, and in the baseline it was compared against. */
38
+ value: number;
39
+ baseline: number;
40
+ /** `value / baseline`, or `undefined` when the baseline was zero. */
41
+ ratio?: number | undefined;
42
+ at: number;
43
+ window: AuditWindow;
44
+ baselineWindow: AuditWindow;
45
+ }
46
+ /** Counters for one stretch of time. */
47
+ export interface AuditWindow {
48
+ /** Milliseconds the window covers, rounded up to whole buckets. */
49
+ spanMs: number;
50
+ requests: number;
51
+ /** Requests that skipped detection: allowlisted, or an ignored path. */
52
+ bypassed: number;
53
+ bots: number;
54
+ humans: number;
55
+ denials: number;
56
+ challenges: number;
57
+ /**
58
+ * Challenges that were solved in this span.
59
+ *
60
+ * Counted separately from `challenges` because the ratio between them is the one
61
+ * measurement that says whether the mitigations are landing on machines or on people.
62
+ * See {@link challengeSolveRate}.
63
+ */
64
+ challengesSolved: number;
65
+ downgrades: number;
66
+ failures: number;
67
+ /** Requests per minute over the span. */
68
+ rate: number;
69
+ /** Bots as a fraction of the requests detection actually ran on, 0–1. */
70
+ botShare: number;
71
+ /**
72
+ * Solved challenges as a fraction of those issued, 0–1. `undefined` below the sample
73
+ * floor, because a rate over three challenges is not a rate.
74
+ *
75
+ * **High is the bad direction**, which is the opposite of what the name suggests to
76
+ * most people. A proof-of-work challenge is trivial for a browser and trivial for a
77
+ * competent scraper; what it costs is a few seconds of somebody's afternoon. So a
78
+ * solve rate near one does not mean the challenges are working — it means almost
79
+ * everything being challenged can pass, and the population that can pass a browser
80
+ * challenge is overwhelmingly people.
81
+ */
82
+ challengeSolveRate: number | undefined;
83
+ }
84
+ export interface AuditContext {
85
+ window: AuditWindow;
86
+ baseline: AuditWindow;
87
+ at: number;
88
+ /** The configured floor for a check to speak at all. */
89
+ minSamples: number;
90
+ }
91
+ export interface AuditCheck {
92
+ id: string;
93
+ /** One line, shown by `describeAudit()` and on the dashboard. */
94
+ description: string;
95
+ /** Returns an anomaly, or `undefined` when nothing is worth saying. */
96
+ evaluate(context: AuditContext): Omit<TrafficAnomaly, "at" | "window" | "baselineWindow"> | undefined;
97
+ }
98
+ export interface AuditOptions {
99
+ /** The recent stretch being judged. Default 300000 (5 minutes). */
100
+ windowMs?: number;
101
+ /**
102
+ * What it is compared against — the *preceding* stretch, not one containing it.
103
+ * Default 3600000 (1 hour).
104
+ *
105
+ * Ending the baseline where the window begins is what makes a spike visible: a
106
+ * baseline that included the window would be partly made of the thing being
107
+ * measured, and a large enough spike would raise its own bar until it stopped
108
+ * looking like one.
109
+ */
110
+ baselineMs?: number;
111
+ /** How often the comparison runs, ms. Default 60000. */
112
+ intervalMs?: number;
113
+ /** Requests needed in the window before any check may speak. Default 50. */
114
+ minSamples?: number;
115
+ /** Silence per check id after it fires, ms. Default 900000 (15 minutes). */
116
+ cooldownMs?: number;
117
+ /** Replaces the built-in checks entirely. */
118
+ checks?: readonly AuditCheck[];
119
+ /** Appended to the built-in checks. */
120
+ extraChecks?: readonly AuditCheck[];
121
+ clock?: Clock;
122
+ }
123
+ export declare class TrafficAudit {
124
+ readonly checks: readonly AuditCheck[];
125
+ /** Effective spans: the requested ones rounded up to whole buckets. */
126
+ private windowMs;
127
+ private baselineMs;
128
+ private readonly bucketMs;
129
+ private readonly buckets;
130
+ private readonly minSamples;
131
+ private readonly cooldownMs;
132
+ private readonly lastFired;
133
+ private readonly clock;
134
+ private timer;
135
+ constructor(options?: AuditOptions);
136
+ /** Starts the periodic comparison. Unreffed: an audit never keeps a process alive. */
137
+ start(intervalMs: number, onAnomaly: (anomaly: TrafficAnomaly) => void): void;
138
+ stop(): void;
139
+ record(assessment: Assessment): void;
140
+ /**
141
+ * A challenge was solved.
142
+ *
143
+ * Recorded here rather than derived from decisions because a solve happens on a
144
+ * *later* request than the challenge that prompted it — usually the next one, from a
145
+ * client that is now carrying a clearance token and will not be challenged again. No
146
+ * amount of looking at decisions finds it.
147
+ */
148
+ recordChallengeSolved(at: number): void;
149
+ recordDecision(decision: Decision, at: number): void;
150
+ /** The two spans as they stand right now. Also what the dashboard draws. */
151
+ summary(now?: number): {
152
+ window: AuditWindow;
153
+ baseline: AuditWindow;
154
+ };
155
+ /**
156
+ * Runs every check. Anomalies come back in the order the checks are configured.
157
+ *
158
+ * Safe to call as often as you like: a check that has fired inside its cooldown is
159
+ * skipped, and a window below `minSamples` produces nothing at all.
160
+ */
161
+ evaluate(now?: number): TrafficAnomaly[];
162
+ private bucketFor;
163
+ private aggregate;
164
+ }
165
+ /**
166
+ * The checks that ship.
167
+ *
168
+ * Each one earns its place by describing a *different* thing going wrong, and each
169
+ * states its floor in the code rather than in a comment. Replace them wholesale with
170
+ * `audit.checks`, or add your own with `audit.extraChecks`.
171
+ */
172
+ export declare const DEFAULT_CHECKS: readonly AuditCheck[];
@@ -0,0 +1,176 @@
1
+ import type { ChallengeCopy } from "./language.js";
2
+ import type { Clock } from "../internal/clock.js";
3
+ import type { BotHandlerStore } from "../stores/types.js";
4
+ import type { ClearanceClaims, ClearanceLevel } from "./token.js";
5
+ export { renderChallengePage } from "./page.js";
6
+ export { parseAcceptLanguage, pickTranslation } from "./language.js";
7
+ export type { ChallengeCopy } from "./language.js";
8
+ export type { ChallengePageOptions, RenderedChallenge } from "./page.js";
9
+ export { DEFAULT_DIFFICULTY, MAX_DIFFICULTY, clampDifficulty, countLeadingZeroBits, solveProofOfWork, verifyProofOfWork } from "./pow.js";
10
+ export { issueToken, verifyToken, newChallenge, newClearance } from "./token.js";
11
+ export type { ChallengeClaims, ClearanceClaims, ClearanceLevel, TokenVerification } from "./token.js";
12
+ export interface ChallengeOptions {
13
+ /**
14
+ * HMAC secrets. The first signs, all of them verify — prepend a new one and keep
15
+ * the old for a token lifetime to rotate without logging anyone out.
16
+ *
17
+ * Required, with no default, on purpose: a library-supplied fallback secret is a
18
+ * library-supplied forgery key, and it would end up in production somewhere.
19
+ */
20
+ secrets: readonly string[];
21
+ /** Leading zero bits demanded. Default 16 — a few tens of ms in a browser. */
22
+ difficulty?: number;
23
+ /** How long a challenge may be solved for, ms. Default 120000. */
24
+ challengeTtlMs?: number;
25
+ /** How long a granted clearance lasts, ms. Default 3600000 (1h). */
26
+ clearanceTtlMs?: number;
27
+ /** Path the solution is POSTed to. Default "/__bothandler/verify". */
28
+ verifyPath?: string;
29
+ /** Cookie carrying the clearance. Default "__bh_clearance". */
30
+ cookieName?: string;
31
+ /** Emit `Secure`. Default true. Set false only for local plaintext development. */
32
+ cookieSecure?: boolean;
33
+ /** SameSite attribute. Default "Lax". */
34
+ cookieSameSite?: "Lax" | "Strict" | "None";
35
+ /** Page heading. */
36
+ title?: string;
37
+ /** Page body copy. */
38
+ message?: string;
39
+ /**
40
+ * HTML shown to anyone the check locks out — no JavaScript, no WebCrypto, a device
41
+ * too slow to finish. Supply something real: a support address, a phone number, a
42
+ * link to a form. Everyone who sees it is a person your site just turned away.
43
+ */
44
+ contactHtml?: string;
45
+ /**
46
+ * Copy for other languages, keyed by language tag — `"ja"`, `"pt-BR"`, `"de"`.
47
+ *
48
+ * The interstitial is the only page this library shows to a member of the public, and
49
+ * it is shown because a *probabilistic* verdict went against them. Somebody who
50
+ * cannot read it cannot find the contact link on it either, which turns a check into
51
+ * a wall.
52
+ *
53
+ * The library ships no translations and will not: a machine-translated apology on a
54
+ * page that just turned somebody away is worse than an honest English one, and only
55
+ * you know which languages your audience reads. Supply the ones you can stand behind
56
+ * and the best match for each visitor's `Accept-Language` is chosen; anything you
57
+ * leave out of a translation falls back to the default text.
58
+ *
59
+ * ```ts
60
+ * translations: {
61
+ * ja: { title: "ブラウザーを確認しています", message: "数秒で完了します。" },
62
+ * "pt-BR": { title: "Verificando seu navegador" },
63
+ * }
64
+ * ```
65
+ *
66
+ * **Key by the primary tag** — `pt`, `zh`, `de` — unless you genuinely have separate
67
+ * regional copy. Matching is exact-tag first and then primary-subtag, and it stops
68
+ * there: a visitor asking for `pt-PT` will *not* be handed `pt-BR`. That looks
69
+ * unhelpful until you consider the case it is protecting — serving Simplified Chinese
70
+ * to somebody who asked for Traditional is a worse failure than serving English, and
71
+ * no rule can tell the two situations apart. Whether one regional variant stands in
72
+ * for another is a judgement about your audience, so it is made by which keys you
73
+ * write rather than by a heuristic here.
74
+ */
75
+ translations?: Record<string, ChallengeCopy>;
76
+ store?: BotHandlerStore;
77
+ clock?: Clock;
78
+ }
79
+ export interface ChallengeResponse {
80
+ status: number;
81
+ headers: Record<string, string>;
82
+ body: string;
83
+ }
84
+ export type SolutionOutcome = {
85
+ ok: true;
86
+ setCookie: string;
87
+ level: ClearanceLevel;
88
+ } | {
89
+ ok: false;
90
+ status: number;
91
+ reason: string;
92
+ };
93
+ /**
94
+ * Issues challenges, verifies solutions and grants clearance.
95
+ *
96
+ * The lifecycle is deliberately stateless up to the moment of success. A challenge is
97
+ * a signed blob the client carries; the server stores nothing while it is being
98
+ * solved, so a flood of unsolved challenges costs nothing but the bytes to send them.
99
+ * Exactly one piece of state is written, at the one moment it is indispensable: the
100
+ * solved challenge's nonce is claimed atomically so a solution cannot be replayed.
101
+ */
102
+ export declare class ChallengeService {
103
+ private readonly options;
104
+ private readonly secrets;
105
+ private readonly difficulty;
106
+ private readonly challengeTtlMs;
107
+ private readonly clock;
108
+ private readonly store;
109
+ readonly verifyPath: string;
110
+ readonly cookieName: string;
111
+ /**
112
+ * How long a granted clearance lasts. Public because the engine mirrors it into its
113
+ * own actor registry, and the two must expire together: a local record that outlives
114
+ * the cookie hands a solved actor a window in which it is never re-challenged.
115
+ */
116
+ readonly clearanceTtlMs: number;
117
+ constructor(options: ChallengeOptions);
118
+ /**
119
+ * Derives the subject a token is bound to.
120
+ *
121
+ * The actor key is hashed rather than embedded, for two independent reasons. Tokens
122
+ * are readable by the client, and the actor key is frequently an IP address — which
123
+ * would mean handing every visitor a cookie containing their own address, and
124
+ * anyone who obtained the cookie a record of where it was issued. Hashing under the
125
+ * signing secret also means a token cannot be correlated across deployments.
126
+ */
127
+ subjectFor(actorKey: string): string;
128
+ /**
129
+ * Every subject this actor could legitimately be carrying, newest first.
130
+ *
131
+ * The subject is derived under a secret, so rotating secrets changes it — and
132
+ * binding verification to `secrets[0]` alone would mean that prepending a new key
133
+ * silently rejected every outstanding cookie as `wrong-actor`. The signature would
134
+ * still verify against the retained old key; only the binding would fail. Every
135
+ * visitor holding valid clearance would be sent back through the interstitial at the
136
+ * moment of rotation, which is exactly the documented promise this class makes and
137
+ * exactly the kind of quiet mass false positive the library exists to avoid.
138
+ *
139
+ * So the same rule the signature follows applies to the binding: the newest secret
140
+ * mints, every configured secret verifies.
141
+ */
142
+ private subjectsFor;
143
+ /**
144
+ * Builds the full interstitial response for an actor.
145
+ *
146
+ * `acceptLanguage` is the visitor's header, and passing it is what lets the page be
147
+ * written in a language they read. It is optional because a caller that has no request
148
+ * to hand — a test, a script — should still be able to render one.
149
+ */
150
+ issue(actorKey: string, options?: {
151
+ acceptLanguage?: string | undefined;
152
+ }): ChallengeResponse;
153
+ /**
154
+ * Validates a submitted solution and, on success, returns the `Set-Cookie` that
155
+ * grants clearance.
156
+ *
157
+ * The checks run in the order that costs least on the way to a rejection:
158
+ * signature, then binding, then expiry, then the hash, then the replay claim. The
159
+ * expensive shared-state round trip happens only for a submission that has already
160
+ * proven itself in every cheaper respect.
161
+ */
162
+ verifySolution(actorKey: string, payload: unknown): Promise<SolutionOutcome>;
163
+ /**
164
+ * Mints a clearance cookie directly, bypassing the puzzle.
165
+ *
166
+ * Use it the moment your application knows something the request cannot show — a
167
+ * completed login, a verified payment, a session you already trust. `operator`
168
+ * level is the only clearance this library treats as conclusive proof of a person,
169
+ * precisely because the assertion comes from your code rather than from the client.
170
+ */
171
+ grant(actorKey: string, level?: ClearanceLevel): string;
172
+ /** Reads and validates the clearance cookie for an actor. Returns `undefined` if there is none valid. */
173
+ read(actorKey: string, cookies: Record<string, string> | undefined): ClearanceClaims | undefined;
174
+ /** A `Set-Cookie` that removes any clearance. Call it on logout. */
175
+ revoke(): string;
176
+ }