@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
package/dist/core.d.ts ADDED
@@ -0,0 +1,443 @@
1
+ import { ActorRegistry } from "./state.js";
2
+ import { ChallengeService } from "./challenge/index.js";
3
+ import { TrafficAudit } from "./audit.js";
4
+ import { NotificationHub } from "./notify/hub.js";
5
+ import { Policy } from "./policy/policy.js";
6
+ import type { GuardSettings } from "./policy/policy.js";
7
+ import type { ActionOutcome } from "./actions/types.js";
8
+ import type { Rule } from "./policy/types.js";
9
+ import type { Assessment, RequestFacts } from "./types.js";
10
+ import type { BotHandlerConfig, ResolvedConfig } from "./config.js";
11
+ import type { TrafficAnomaly } from "./audit.js";
12
+ import type { DashboardOptions, DashboardServer } from "./dashboard/types.js";
13
+ import type { MetricsSnapshot, PrometheusOptions } from "./metrics.js";
14
+ import type { BotHandlerStore } from "./stores/types.js";
15
+ import type { Decision } from "./policy/types.js";
16
+ import type { SolutionOutcome } from "./challenge/index.js";
17
+ export interface HandleResult {
18
+ assessment: Assessment;
19
+ decision: Decision;
20
+ outcome: ActionOutcome;
21
+ }
22
+ /**
23
+ * Everything the engine will tell you about, and the whole integration surface.
24
+ *
25
+ * Each of these has a matching `onX` in the config — the same mechanism, registered
26
+ * for you at construction. Subscribe here when you want to add a listener later,
27
+ * add several, or remove one; use the config form when you just want a callback.
28
+ *
29
+ * They are deliberately *derived* rather than raw: `denial` and `downgrade` exist
30
+ * because "a request was refused" and "the guard refused a rule" are the two things
31
+ * an operator wants an alert on, and making every subscriber re-derive them from
32
+ * `decision` is how two integrations end up disagreeing about what a denial is.
33
+ */
34
+ export interface BotHandlerEvents extends Record<string, unknown> {
35
+ /** Every assessment, including the ones that concluded nothing. */
36
+ assessment: Assessment;
37
+ /** Every decision, with the assessment behind it. */
38
+ decision: {
39
+ assessment: Assessment;
40
+ decision: Decision;
41
+ };
42
+ /** A request that was actually denied: `block`, `drop` or `redirect`. */
43
+ denial: {
44
+ assessment: Assessment;
45
+ decision: Decision;
46
+ };
47
+ /** The guard replaced a terminal action with something recoverable. */
48
+ downgrade: {
49
+ assessment: Assessment;
50
+ decision: Decision;
51
+ };
52
+ /** A challenge was issued, solved or rejected. */
53
+ challenge: {
54
+ phase: "issued" | "solved" | "rejected";
55
+ actorKey?: string | undefined;
56
+ };
57
+ /**
58
+ * A detector threw or timed out.
59
+ *
60
+ * `requestId` is empty when the failure was noticed before the assessment was
61
+ * assembled — the failure list on the assessment is the per-request record; this
62
+ * event is the operational one, for the alert that says a resolver is down.
63
+ */
64
+ "detector-failure": {
65
+ detector: string;
66
+ reason: string;
67
+ message: string;
68
+ requestId: string;
69
+ };
70
+ /** The rule set was replaced at runtime. */
71
+ "policy-change": {
72
+ rules: readonly string[];
73
+ warnings: readonly string[];
74
+ by?: string | undefined;
75
+ };
76
+ /**
77
+ * The guard settings were changed at runtime.
78
+ *
79
+ * Separate from `policy-change` on purpose. "Which rules exist" and "how far a rule
80
+ * is allowed to go" are different powers with different consequences, and an
81
+ * operator wiring an alert almost always wants the second one and not the first.
82
+ * `before` and `after` both travel so the alert can say what actually moved.
83
+ */
84
+ "guard-change": {
85
+ before: GuardSettings & {
86
+ suspectThreshold: number;
87
+ };
88
+ after: GuardSettings & {
89
+ suspectThreshold: number;
90
+ };
91
+ by?: string | undefined;
92
+ };
93
+ /**
94
+ * A range set was replaced at runtime — an allowlist entry added, a crawler's
95
+ * published ranges refreshed.
96
+ *
97
+ * Worth an event of its own because the allowlist is the one list that stops
98
+ * detection *running*: an address on it is not judged leniently, it is not judged at
99
+ * all. A change to it is a change to what your bot handling can see.
100
+ */
101
+ "range-change": {
102
+ name: string;
103
+ size: number;
104
+ entries: readonly string[];
105
+ by?: string | undefined;
106
+ };
107
+ /**
108
+ * An actor's behavioural memory was changed by hand — forgotten, or granted human
109
+ * clearance.
110
+ *
111
+ * The remedy for a false positive that has stuck to somebody, and therefore exactly
112
+ * the operation an audit trail wants to have seen.
113
+ */
114
+ "actor-change": {
115
+ key: string;
116
+ action: "forget" | "clear";
117
+ until?: number | undefined;
118
+ by?: string | undefined;
119
+ };
120
+ /** The audit noticed the traffic change shape. */
121
+ anomaly: TrafficAnomaly;
122
+ warning: string;
123
+ error: {
124
+ error: unknown;
125
+ source: string;
126
+ };
127
+ }
128
+ /**
129
+ * Who asked for a runtime change.
130
+ *
131
+ * Every mutating method takes one, and every one of them puts it in the warning and in
132
+ * the event. The library has no idea who anybody is — it has no user model and does not
133
+ * want one — so this is a string the caller supplies, and the caller is the thing that
134
+ * *does* know: an authenticating dashboard, a deploy pipeline, an admin CLI. An absent
135
+ * `by` is not a failure; it means whatever made the change could not say who.
136
+ */
137
+ export interface ChangeContext {
138
+ by?: string | undefined;
139
+ }
140
+ /** Options for a single assessment. */
141
+ export interface AssessOptions {
142
+ /**
143
+ * Whether this request is part of your traffic. Default true.
144
+ *
145
+ * `record: false` is a **dry run**: the detectors all run and the verdict is real,
146
+ * but nothing is written down. No actor state moves, no counter increments, no
147
+ * `assessment` event fires and no notification is sent — so asking "what would this
148
+ * request be judged as?" does not become a row in the answer to "what is my traffic
149
+ * doing?".
150
+ *
151
+ * It is what the dashboard's request tester uses, and what to reach for anywhere else
152
+ * you want the engine's opinion about a request that is not happening: a support
153
+ * ticket, a test, a rule you are drafting. The one thing a dry run cannot see is
154
+ * history — it gets an actor with no past, because the alternative is to record the
155
+ * request against a real one, which is the thing it promised not to do.
156
+ */
157
+ record?: boolean;
158
+ }
159
+ /** Description of a registered detector, for documentation and diagnostics. */
160
+ export interface DetectorDescription {
161
+ id: string;
162
+ description: string;
163
+ cost: "cheap" | "io";
164
+ stage: "always" | "confirming";
165
+ }
166
+ /**
167
+ * The engine.
168
+ *
169
+ * Three separable steps, in a fixed order, each usable on its own:
170
+ *
171
+ * 1. {@link assess} — gather evidence and reach a verdict. Reads the request, touches
172
+ * no response, and is safe to call anywhere, including from a log processor
173
+ * replaying yesterday's traffic.
174
+ * 2. {@link decide} — apply the policy, subject to the safety guard.
175
+ * 3. {@link handle} — do both, then turn the decision into an outcome an adapter can
176
+ * apply.
177
+ *
178
+ * Keeping them apart is what makes the library testable and what makes a
179
+ * monitor-only deployment a first-class mode rather than a configuration trick: call
180
+ * `assess` alone and you have a detector with no opinions about your traffic at all.
181
+ *
182
+ * Everything is failure-isolated. A detector that throws, a store that will not
183
+ * answer, a notification sink that hangs — each degrades exactly itself and is
184
+ * reported through `onError`. Nothing in this file can turn a bad day for a
185
+ * dependency into a bad day for the site it is protecting.
186
+ */
187
+ export declare class BotHandler {
188
+ readonly config: ResolvedConfig;
189
+ readonly registry: ActorRegistry;
190
+ readonly store: BotHandlerStore;
191
+ readonly policy: Policy;
192
+ readonly challenge: ChallengeService | undefined;
193
+ readonly notifications: NotificationHub;
194
+ /**
195
+ * The traffic audit, or `undefined` when it was switched off with `audit: false`.
196
+ *
197
+ * Exposed so you can ask it questions on your own schedule — `summary()` for the
198
+ * two windows as they stand, `evaluate()` to run the checks now — which is what a
199
+ * health endpoint or a cron job wants rather than waiting for the timer.
200
+ */
201
+ readonly audit: TrafficAudit | undefined;
202
+ private readonly signatures;
203
+ private readonly resolver;
204
+ private readonly handlers;
205
+ private readonly cheapDetectors;
206
+ private readonly ioDetectors;
207
+ private readonly confirmingDetectors;
208
+ private readonly events;
209
+ private readonly ignoreExact;
210
+ private readonly ignorePatterns;
211
+ private readonly isHuman;
212
+ private readonly meter;
213
+ /** Hoisted out of the metrics object: read once per detector per request. */
214
+ private readonly timing;
215
+ constructor(options?: BotHandlerConfig);
216
+ /**
217
+ * Runs the audit's checks now and emits whatever they found.
218
+ *
219
+ * The timer does this on a schedule; calling it yourself is for the cases a timer
220
+ * cannot serve — a health endpoint that should report the current picture, a cron
221
+ * job, a test with a manual clock.
222
+ */
223
+ runAudit(): TrafficAnomaly[];
224
+ private raiseAnomaly;
225
+ on<K extends keyof BotHandlerEvents & string>(event: K, listener: (payload: BotHandlerEvents[K]) => void): () => void;
226
+ /**
227
+ * A point-in-time copy of the counters. Cheap enough to call on every scrape.
228
+ *
229
+ * Returns `undefined` when metrics were switched off with `metrics: false`.
230
+ */
231
+ metrics(): MetricsSnapshot | undefined;
232
+ /** The same counters in Prometheus text exposition format. */
233
+ prometheus(options?: PrometheusOptions): string | undefined;
234
+ /**
235
+ * Starts the operator dashboard on a listener of its own.
236
+ *
237
+ * ```ts
238
+ * const dashboard = await botHandler.serveDashboard({
239
+ * port: 9674,
240
+ * auth: { username: "ops", password: process.env.DASH_PASSWORD! },
241
+ * });
242
+ * console.log(dashboard.url);
243
+ * ```
244
+ *
245
+ * **Its own listener, not a route in your application.** Mounting it inside the app
246
+ * it reports on puts it behind the bot handler, and that arrangement has three
247
+ * separate failure modes: reading the dashboard shows up in the dashboard, a
248
+ * challenge served to your site can lock you out of the tool you are using to read
249
+ * about it, and the page becomes reachable at whatever authentication your public
250
+ * site happens to have. A second port is the cheapest fix for all three.
251
+ *
252
+ * **Loopback by default, and it refuses to bind anywhere else without an explicit
253
+ * `auth`.** This page shows client addresses and, per request, the exact evidence
254
+ * behind the verdict — which is a tuning guide for anyone building a scraper
255
+ * against you. See {@link DashboardOptions.host}.
256
+ *
257
+ * The returned handle carries the URL to open (with the real port, which matters if
258
+ * you passed 0) and a `close()` that stops the listener, drops the event streams and
259
+ * unsubscribes from this handler.
260
+ */
261
+ serveDashboard(options?: DashboardOptions): Promise<DashboardServer>;
262
+ /**
263
+ * Replaces an IP range set while the process runs.
264
+ *
265
+ * Published crawler ranges are the reason this exists. Operators revise them, a
266
+ * stale list turns a verified crawler into an accused impersonator, and requiring a
267
+ * restart to pick up a new one means the list is refreshed roughly never. Fetch
268
+ * them on whatever schedule you like and hand them here.
269
+ *
270
+ * The swap is atomic — the new set is fully parsed and validated before it replaces
271
+ * the old one — so a request assessed mid-update sees one list or the other, never
272
+ * a half-built one. Invalid input throws and leaves the existing set in place,
273
+ * because a range set that silently matches nothing is worse than a stale one.
274
+ */
275
+ updateRanges(name: string, entries: readonly string[], context?: ChangeContext): void;
276
+ /** The ranges in a set, as written. `undefined` when no set of that name exists. */
277
+ rangeEntries(name: string): readonly string[] | undefined;
278
+ /**
279
+ * Discards one actor's behavioural memory.
280
+ *
281
+ * The remedy for a false positive that has stuck: a person whose actor key collected
282
+ * a `confirmed-bot` — a shared office address, a phone that reused an IP — carries
283
+ * `priorConfirmations` for the rest of the window, and any rule reading
284
+ * `minPriorConfirmations` keeps matching them. Until this existed the only cure was
285
+ * `registry.clear()`, which throws away every actor's history to fix one.
286
+ *
287
+ * It is not an allowlist: the next request from this actor is assessed exactly as any
288
+ * first request would be.
289
+ */
290
+ forgetActor(key: string, context?: ChangeContext): void;
291
+ /**
292
+ * Grants an actor human clearance for a while, as though it had solved a challenge.
293
+ *
294
+ * The `clearance` detector reads it, so this is an assertion about a person made by
295
+ * an operator on evidence the request does not carry — the same category of claim as
296
+ * `isHuman`, and it expires the same way a solved challenge does.
297
+ */
298
+ clearActor(key: string, forMs: number, context?: ChangeContext): void;
299
+ /** Convenience for `updateRanges("crawler:<id>", …)`, matching a signature id. */
300
+ updateCrawlerRanges(signatureId: string, entries: readonly string[], context?: ChangeContext): void;
301
+ /**
302
+ * Replaces the rule list while the process runs.
303
+ *
304
+ * The dashboard's policy editor is the reason this exists, and the shape of it is
305
+ * deliberate: you may change **which rules exist**, and nothing about **how far a
306
+ * rule is allowed to go**. `falsePositivePolicy`, `fallbackAction` and
307
+ * `terminalScoreThreshold` are fixed at construction. A dashboard — or anything
308
+ * else holding a reference to this handler — therefore cannot relax the guard that
309
+ * stops an unproven verdict from denying somebody. Loosening that is a deploy, on
310
+ * purpose, reviewed by whoever reviews deploys.
311
+ *
312
+ * Validation runs first and throws on a rule that could never work; the swap only
313
+ * happens if every rule survives it, so a bad edit leaves the running policy exactly
314
+ * as it was. Warnings — a duplicate id, a rule that can never match — come back to
315
+ * the caller *and* go to `onWarning`, because a policy changed at runtime should
316
+ * leave a trace in the same place a policy loaded at startup does.
317
+ */
318
+ updatePolicy(rules: readonly Rule[], context?: ChangeContext): {
319
+ warnings: string[];
320
+ };
321
+ /**
322
+ * Changes the guard settings while the process runs.
323
+ *
324
+ * The one runtime change that can start denying people, and it is kept apart from
325
+ * {@link updatePolicy} for exactly that reason: a separate method, a separate
326
+ * dashboard control, a separate event. Fields left out keep their current value.
327
+ *
328
+ * Everything is validated before anything is applied, so a rejected change leaves
329
+ * the running guard exactly as it was — the same contract `updatePolicy` has. What
330
+ * is refused and why is documented on {@link Policy.replaceGuard}; the short version
331
+ * is that a terminal `fallbackAction` would turn every downgrade into the denial the
332
+ * downgrade exists to prevent.
333
+ *
334
+ * It is announced twice on purpose: a `warning`, which lands wherever your startup
335
+ * warnings land and in the dashboard's notices, and a `guard-change` event carrying
336
+ * both the before and the after.
337
+ */
338
+ updateGuard(settings: Partial<GuardSettings> & {
339
+ suspectThreshold?: number;
340
+ }, context?: ChangeContext): {
341
+ guard: GuardSettings & {
342
+ suspectThreshold: number;
343
+ };
344
+ };
345
+ /**
346
+ * Tells the other replicas that this actor has been proven a bot.
347
+ *
348
+ * Fire and forget, off the request path, and failure is silence: a store that is
349
+ * unavailable costs the *other* instances a fact they would have liked, and costs
350
+ * this request nothing at all. The window is the registry's own, so a shared count
351
+ * ages out at the same rate the local one does rather than accumulating for ever.
352
+ */
353
+ private publishConfirmation;
354
+ /**
355
+ * Asks the store what other replicas already know about an actor we have just met.
356
+ *
357
+ * Once per actor per instance, and deliberately *not* awaited: making it synchronous
358
+ * would put a network round trip on the request path, which is the one thing the
359
+ * behavioural state is arranged to avoid.
360
+ *
361
+ * So the request that triggers the read is assessed with whatever has arrived by the
362
+ * time the detectors finish — which for a fast store is usually the answer, and for a
363
+ * slow or distant one is nothing. Every request after it has the number. Neither
364
+ * outcome is wrong: an actor nobody here has seen before is an actor with no local
365
+ * history either, so the first request was always going to be judged on the request
366
+ * alone.
367
+ *
368
+ * The read subtracts nothing. This instance has not confirmed anything about a
369
+ * brand-new actor yet, so whatever the store holds was put there by somebody else,
370
+ * and the two counts add up cleanly from here.
371
+ */
372
+ private loadSharedConfirmations;
373
+ /** Names and sizes of every configured range set. */
374
+ listRanges(): Array<{
375
+ name: string;
376
+ size: number;
377
+ }>;
378
+ /** Every registered detector, for documentation and for checking what is actually installed. */
379
+ describeDetectors(): DetectorDescription[];
380
+ /** Recovers the client address from a socket address and headers, honouring the proxy config. */
381
+ resolveIp(socketAddress: string | undefined, headers: Record<string, string | undefined>): string;
382
+ actorKeyFor(facts: RequestFacts): string;
383
+ isAllowlisted(ip: string): boolean;
384
+ isIgnoredPath(path: string): boolean;
385
+ /**
386
+ * Gathers evidence and reaches a verdict. Never throws, never writes a response.
387
+ *
388
+ * Detectors run in three phases, ordered by what they cost:
389
+ *
390
+ * 1. **Cheap** — pure string and header inspection, sequential, microseconds.
391
+ * 2. **I/O** — concurrent, each under its own timeout, so one slow lookup cannot
392
+ * hold the request open.
393
+ * 3. **Confirming** — only when the request actually claimed an identity worth
394
+ * checking. A request that named no crawler resolves no DNS.
395
+ */
396
+ assess(facts: RequestFacts, options?: AssessOptions): Promise<Assessment>;
397
+ /** Applies the policy. Pure — no I/O, no side effects beyond the downgrade notification. */
398
+ decide(assessment: Assessment): Decision;
399
+ /** Assess, decide, and turn the decision into an outcome for an adapter to apply. */
400
+ handle(facts: RequestFacts): Promise<HandleResult>;
401
+ /** True when this request is the challenge verification endpoint. */
402
+ isChallengeEndpoint(facts: Pick<RequestFacts, "method" | "path">): boolean;
403
+ /**
404
+ * Verifies a submitted challenge solution.
405
+ *
406
+ * Deliberately its own entry point rather than something `handle` detects: the
407
+ * verification endpoint must not itself be assessed, or a client that has just been
408
+ * challenged would be challenged again for trying to answer.
409
+ */
410
+ verifyChallenge(facts: RequestFacts, body: unknown): Promise<SolutionOutcome>;
411
+ /**
412
+ * Mints a clearance cookie for an actor, bypassing the challenge.
413
+ *
414
+ * Call it the moment your application knows something the request cannot show — a
415
+ * completed login, a verified payment. `operator` clearance is the only conclusive
416
+ * human signal this library recognises.
417
+ */
418
+ grantClearance(facts: RequestFacts, level?: "pow" | "interaction" | "operator"): string | undefined;
419
+ /**
420
+ * Runs one detector.
421
+ *
422
+ * Returns `undefined` when the detector completed synchronously — the common case,
423
+ * and the one worth keeping off the microtask queue — or a promise the caller must
424
+ * await. Failures are absorbed here in both paths: a detector can throw, reject or
425
+ * hang, and none of those may reach the request.
426
+ */
427
+ private run;
428
+ private collect;
429
+ private recordFailure;
430
+ private bypassed;
431
+ /**
432
+ * Raises a warning through the handler's own channel.
433
+ *
434
+ * Public because things outside this class legitimately have something to say: an
435
+ * adapter that has spotted a misconfiguration, the crawler-range refresher reporting
436
+ * a publisher it could not reach. Both used to call `config.onWarning` directly,
437
+ * which reaches the callback and *not* the `warning` event — so a subscriber, and
438
+ * with it the dashboard's notices panel, never heard about either. One channel, one
439
+ * set of listeners.
440
+ */
441
+ warn(message: string): void;
442
+ private fail;
443
+ }
@@ -0,0 +1,2 @@
1
+ import type { TrafficCase } from "./schema.js";
2
+ export declare const ADVERSARIAL_CASES: TrafficCase[];
@@ -0,0 +1,2 @@
1
+ import type { TrafficCase } from "./schema.js";
2
+ export declare const ADVERTISING_EMAIL_CASES: TrafficCase[];
@@ -0,0 +1,2 @@
1
+ import type { TrafficCase } from "./schema.js";
2
+ export declare const AI_CRAWLER_CASES: TrafficCase[];
@@ -0,0 +1,2 @@
1
+ import type { TrafficCase } from "./schema.js";
2
+ export declare const BENIGN_BOT_CASES: TrafficCase[];
@@ -0,0 +1,2 @@
1
+ import type { TrafficCase } from "./schema.js";
2
+ export declare const CDN_GATEWAY_CASES: TrafficCase[];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Realistic cookie jars.
3
+ *
4
+ * A browser that has been to a commercial site once is carrying a dozen cookies it
5
+ * never asked for, and their *shape* is as characteristic as any header: Google
6
+ * Analytics' `_ga` encodes a client id and a first-seen timestamp, Meta's `_fbp`
7
+ * encodes a version, a subdomain index and a creation time, a TCF consent string is
8
+ * base64 with a version prefix. Automation carries none of this, or carries a single
9
+ * hand-set session cookie and nothing else.
10
+ *
11
+ * The values below are structurally correct and entirely synthetic. They exist so a
12
+ * fixture looks like a request from somebody who has actually used the web, rather
13
+ * than like `Cookie: sid=abc`.
14
+ */
15
+ export interface CookieJarOptions {
16
+ /** Seed for the synthetic identifiers, so two visitors differ. */
17
+ visitor?: string;
18
+ /** Epoch seconds the visitor was first seen. */
19
+ firstSeen?: number;
20
+ /** Epoch seconds of this session's start. */
21
+ sessionStart?: number;
22
+ /** Include Google Analytics 4 cookies. Present on most of the commercial web. */
23
+ analytics?: boolean;
24
+ /** Include Meta's advertising cookies. */
25
+ advertising?: boolean;
26
+ /** Include a consent-management platform's cookies. */
27
+ consent?: boolean;
28
+ /** Include Cloudflare's bot-management cookies, present on a large slice of sites. */
29
+ cloudflare?: boolean;
30
+ /** Include product-analytics and support-widget cookies. */
31
+ productAnalytics?: boolean;
32
+ /** Application cookies: session, cart, CSRF. */
33
+ application?: boolean;
34
+ /** Extra pairs appended verbatim. */
35
+ extra?: readonly (readonly [string, string])[];
36
+ }
37
+ /**
38
+ * Builds a `Cookie` header value.
39
+ *
40
+ * Order matters a little: browsers send cookies sorted by path length descending and
41
+ * then by creation time, which in practice means the host-wide analytics cookies set
42
+ * on the first visit come before the application cookies set later. Reproducing that
43
+ * ordering is the kind of detail a hand-written fixture never has.
44
+ */
45
+ export declare function cookieJar(options?: CookieJarOptions): string;
46
+ /** A first-time visitor: no analytics history, only whatever this page set. */
47
+ export declare function freshVisitorJar(visitor?: string): string;
48
+ /** A returning customer of a commercial site — the fullest jar in ordinary use. */
49
+ export declare function returningCustomerJar(visitor?: string): string;
@@ -0,0 +1,2 @@
1
+ import type { TrafficCase } from "./schema.js";
2
+ export declare const REGIONAL_CRAWLER_CASES: TrafficCase[];
@@ -0,0 +1,2 @@
1
+ import type { TrafficCase } from "./schema.js";
2
+ export declare const VERTICAL_CRAWLER_CASES: TrafficCase[];