@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.
- package/CHANGELOG.md +516 -0
- package/LICENSE +102 -0
- package/README.md +308 -0
- package/bin/bothandlerjs.mjs +14 -0
- package/dist/actions/index.d.ts +22 -0
- package/dist/actions/types.d.ts +59 -0
- package/dist/adapters/fastify.d.ts +43 -0
- package/dist/adapters/fetch.d.ts +51 -0
- package/dist/adapters/index.cjs +723 -0
- package/dist/adapters/index.cjs.map +1 -0
- package/dist/adapters/index.d.ts +8 -0
- package/dist/adapters/index.js +692 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/koa.d.ts +27 -0
- package/dist/adapters/node.d.ts +29 -0
- package/dist/adapters/shared.d.ts +39 -0
- package/dist/audit.d.ts +172 -0
- package/dist/challenge/index.d.ts +176 -0
- package/dist/challenge/language.d.ts +57 -0
- package/dist/challenge/page.d.ts +42 -0
- package/dist/challenge/pow.d.ts +33 -0
- package/dist/challenge/token.d.ts +72 -0
- package/dist/cli.cjs +13388 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +13358 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/index.cjs +139 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.ts +63 -0
- package/dist/client/index.js +112 -0
- package/dist/client/index.js.map +1 -0
- package/dist/config.d.ts +316 -0
- package/dist/core.d.ts +443 -0
- package/dist/corpus/adversarial.d.ts +2 -0
- package/dist/corpus/advertising-email.d.ts +2 -0
- package/dist/corpus/ai-crawlers.d.ts +2 -0
- package/dist/corpus/benign-bots.d.ts +2 -0
- package/dist/corpus/cdn-gateways.d.ts +2 -0
- package/dist/corpus/cookies.d.ts +49 -0
- package/dist/corpus/crawlers-regional.d.ts +2 -0
- package/dist/corpus/crawlers-vertical.d.ts +2 -0
- package/dist/corpus/headers.d.ts +403 -0
- package/dist/corpus/humans-apps.d.ts +2 -0
- package/dist/corpus/humans-browsers.d.ts +2 -0
- package/dist/corpus/humans.d.ts +2 -0
- package/dist/corpus/index.cjs +4196 -0
- package/dist/corpus/index.cjs.map +1 -0
- package/dist/corpus/index.d.ts +37 -0
- package/dist/corpus/index.js +4132 -0
- package/dist/corpus/index.js.map +1 -0
- package/dist/corpus/infrastructure.d.ts +17 -0
- package/dist/corpus/libraries-extended.d.ts +2 -0
- package/dist/corpus/ranges.d.ts +31 -0
- package/dist/corpus/reputation.d.ts +21 -0
- package/dist/corpus/runner.d.ts +130 -0
- package/dist/corpus/schema.d.ts +185 -0
- package/dist/corpus/tooling.d.ts +2 -0
- package/dist/corpus/unwanted.d.ts +2 -0
- package/dist/crawler-ranges.d.ts +113 -0
- package/dist/dashboard/client.generated.d.ts +7 -0
- package/dist/dashboard/feed.d.ts +186 -0
- package/dist/dashboard/index.d.ts +5 -0
- package/dist/dashboard/page.d.ts +51 -0
- package/dist/dashboard/parse-request.d.ts +28 -0
- package/dist/dashboard/preview.d.ts +45 -0
- package/dist/dashboard/server.d.ts +70 -0
- package/dist/dashboard/types.d.ts +714 -0
- package/dist/detectors/accept-signature.d.ts +6 -0
- package/dist/detectors/browsing-coherence.d.ts +44 -0
- package/dist/detectors/cadence.d.ts +28 -0
- package/dist/detectors/clearance.d.ts +25 -0
- package/dist/detectors/client-hints.d.ts +19 -0
- package/dist/detectors/client-signals.d.ts +27 -0
- package/dist/detectors/crawl-breadth.d.ts +28 -0
- package/dist/detectors/crawler-verification.d.ts +39 -0
- package/dist/detectors/fetch-metadata.d.ts +20 -0
- package/dist/detectors/header-integrity.d.ts +26 -0
- package/dist/detectors/header-order.d.ts +27 -0
- package/dist/detectors/identity-rotation.d.ts +34 -0
- package/dist/detectors/index.d.ts +56 -0
- package/dist/detectors/ip-intelligence.d.ts +30 -0
- package/dist/detectors/known-bots.d.ts +94 -0
- package/dist/detectors/probe-signature.d.ts +50 -0
- package/dist/detectors/rate-anomaly.d.ts +36 -0
- package/dist/detectors/self-identified.d.ts +33 -0
- package/dist/detectors/session-integrity.d.ts +24 -0
- package/dist/detectors/tls-fingerprint.d.ts +34 -0
- package/dist/detectors/trap.d.ts +98 -0
- package/dist/detectors/types.d.ts +70 -0
- package/dist/detectors/ua-coherence.d.ts +32 -0
- package/dist/evidence.d.ts +45 -0
- package/dist/facts.d.ts +46 -0
- package/dist/index.cjs +9600 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +9444 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/async.d.ts +16 -0
- package/dist/internal/clock.d.ts +17 -0
- package/dist/internal/crypto.d.ts +26 -0
- package/dist/internal/dns.d.ts +100 -0
- package/dist/internal/emitter.d.ts +18 -0
- package/dist/internal/http.d.ts +22 -0
- package/dist/internal/ip.d.ts +73 -0
- package/dist/internal/lru.d.ts +34 -0
- package/dist/internal/matcher.d.ts +41 -0
- package/dist/internal/pattern.d.ts +14 -0
- package/dist/internal/ua.d.ts +92 -0
- package/dist/metrics.d.ts +120 -0
- package/dist/notify/hub.d.ts +70 -0
- package/dist/notify/index.d.ts +7 -0
- package/dist/notify/redact.d.ts +48 -0
- package/dist/notify/sinks.d.ts +62 -0
- package/dist/notify/types.d.ts +56 -0
- package/dist/policy/index.d.ts +7 -0
- package/dist/policy/match.d.ts +20 -0
- package/dist/policy/policy.d.ts +123 -0
- package/dist/policy/presets.d.ts +161 -0
- package/dist/policy/types.d.ts +155 -0
- package/dist/robots.d.ts +83 -0
- package/dist/state.d.ts +161 -0
- package/dist/stores/index.d.ts +5 -0
- package/dist/stores/memory.d.ts +33 -0
- package/dist/stores/redis.d.ts +59 -0
- package/dist/stores/types.d.ts +37 -0
- package/dist/types.d.ts +250 -0
- package/docs/challenge/index.md +152 -0
- package/docs/challenge/localisation.md +105 -0
- package/docs/concepts/actors.md +113 -0
- package/docs/concepts/evidence.md +133 -0
- package/docs/concepts/the-guard.md +121 -0
- package/docs/concepts/threat-model.md +120 -0
- package/docs/concepts/verdicts.md +108 -0
- package/docs/course/01-first-assessment.md +178 -0
- package/docs/course/02-proof-and-suspicion.md +164 -0
- package/docs/course/03-verdicts-and-scores.md +160 -0
- package/docs/course/04-the-guard.md +200 -0
- package/docs/course/05-detectors.md +178 -0
- package/docs/course/06-identity.md +161 -0
- package/docs/course/07-actors.md +178 -0
- package/docs/course/08-traps.md +183 -0
- package/docs/course/09-rules.md +224 -0
- package/docs/course/10-actions-and-presets.md +216 -0
- package/docs/course/11-the-challenge.md +227 -0
- package/docs/course/12-going-live.md +211 -0
- package/docs/course/13-operating-it.md +238 -0
- package/docs/course/14-scaling.md +195 -0
- package/docs/course/15-extending.md +259 -0
- package/docs/course/16-proving-it.md +266 -0
- package/docs/course/index.md +112 -0
- package/docs/design/decisions.md +216 -0
- package/docs/detection/client-signals.md +96 -0
- package/docs/detection/detectors.md +376 -0
- package/docs/detection/index.md +134 -0
- package/docs/detection/signatures.md +111 -0
- package/docs/detection/verification.md +123 -0
- package/docs/detection/writing-a-detector.md +145 -0
- package/docs/index.md +100 -0
- package/docs/integration/adapters.md +138 -0
- package/docs/integration/client-ip.md +114 -0
- package/docs/integration/index.md +55 -0
- package/docs/integration/stores.md +76 -0
- package/docs/operations/audit.md +116 -0
- package/docs/operations/dashboard.md +725 -0
- package/docs/operations/index.md +101 -0
- package/docs/operations/metrics.md +74 -0
- package/docs/operations/notifications.md +72 -0
- package/docs/operations/runtime-changes.md +118 -0
- package/docs/policy/actions.md +196 -0
- package/docs/policy/index.md +84 -0
- package/docs/policy/presets.md +191 -0
- package/docs/policy/robots.md +128 -0
- package/docs/policy/rules.md +134 -0
- package/docs/reference/api.md +126 -0
- package/docs/reference/configuration.md +175 -0
- package/docs/start/choosing-a-policy.md +127 -0
- package/docs/start/first-integration.md +127 -0
- package/docs/start/installation.md +63 -0
- package/docs/start/upgrading.md +72 -0
- package/docs/testing/cli.md +119 -0
- package/docs/testing/corpus.md +129 -0
- package/docs/testing/index.md +61 -0
- package/docs/testing/replay.md +102 -0
- package/docs/testing/try-it.md +189 -0
- package/package.json +145 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
|
|
3
|
+
Every option `BotHandler` takes, grouped by what it is for.
|
|
4
|
+
|
|
5
|
+
← [Documentation](../index.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
new BotHandler({
|
|
11
|
+
// Detection
|
|
12
|
+
detectors, extraDetectors, signatures, extraSignatures,
|
|
13
|
+
suspectThreshold: 60, strictEvidence, detectorTimeoutMs: 300,
|
|
14
|
+
|
|
15
|
+
// Policy
|
|
16
|
+
preset, rules, defaultAction: "allow", defaultActionParams,
|
|
17
|
+
falsePositivePolicy: "strict", fallbackAction, terminalScoreThreshold: 85,
|
|
18
|
+
|
|
19
|
+
// Addresses — invalid CIDRs throw at construction, never match silently
|
|
20
|
+
allowlist, denylist, datacenterRanges, crawlerRanges: { gptbot: ["1.2.3.0/24"] },
|
|
21
|
+
|
|
22
|
+
// Identity and scope
|
|
23
|
+
proxy: { trustProxy, trustedProxies, hops, header },
|
|
24
|
+
actorKey: (facts) => facts.ip,
|
|
25
|
+
ignorePaths, isHuman,
|
|
26
|
+
|
|
27
|
+
// Observation
|
|
28
|
+
audit: { windowMs: 300_000, baselineMs: 3_600_000, minSamples: 50 },
|
|
29
|
+
onAssessment, onDecision, onDenial, onDowngrade, onChallenge,
|
|
30
|
+
onDetectorFailure, onPolicyChange, onGuardChange, onRangeChange, onActorChange, onAnomaly,
|
|
31
|
+
|
|
32
|
+
// Infrastructure
|
|
33
|
+
challenge, store, notifications, handlers, resolver, clock,
|
|
34
|
+
shareConfirmations: false, // proof crosses replicas; suspicion does not
|
|
35
|
+
actorWindowMs: 900_000, maxActors: 20_000, exposeVerdictHeaders: false,
|
|
36
|
+
metrics: true, // or { perDetectorTiming: true }
|
|
37
|
+
onError, onWarning,
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Detection
|
|
44
|
+
|
|
45
|
+
| Option | Default | |
|
|
46
|
+
| ------ | ------- | - |
|
|
47
|
+
| `detectors` | the default set | replaces it **entirely** |
|
|
48
|
+
| `extraDetectors` | — | appended; ignored when `detectors` is given |
|
|
49
|
+
| `signatures` | `BOT_SIGNATURES` | replaces the [signature database](../detection/signatures.md) |
|
|
50
|
+
| `extraSignatures` | — | appended to it |
|
|
51
|
+
| `suspectThreshold` | `60` | score at or above which an unproven request is `suspected-bot` |
|
|
52
|
+
| `strictEvidence` | `true` outside production | reject `certain` evidence carrying no `deterministicBasis` |
|
|
53
|
+
| `detectorTimeoutMs` | `300` | budget per `io` detector; exceeding it drops the detector, not the request |
|
|
54
|
+
|
|
55
|
+
`strictEvidence` downgrades to a warning when `NODE_ENV` is `production`, so a third-party
|
|
56
|
+
detector with a missing basis cannot take a live site down. See
|
|
57
|
+
[evidence](../concepts/evidence.md).
|
|
58
|
+
|
|
59
|
+
## Policy
|
|
60
|
+
|
|
61
|
+
| Option | Default | |
|
|
62
|
+
| ------ | ------- | - |
|
|
63
|
+
| `preset` | — | a named [starting policy](../policy/presets.md) |
|
|
64
|
+
| `rules` | — | your own [rules](../policy/rules.md), evaluated **before** any preset's |
|
|
65
|
+
| `defaultAction` | `"allow"` | when nothing matches |
|
|
66
|
+
| `defaultActionParams` | — | params for it |
|
|
67
|
+
| `falsePositivePolicy` | `"strict"` | [the guard's](../concepts/the-guard.md) mode |
|
|
68
|
+
| `fallbackAction` | `"challenge"` | substituted when the guard stops a terminal action; **cannot itself be terminal** |
|
|
69
|
+
| `terminalScoreThreshold` | `85` | score needed for a terminal action under `balanced` |
|
|
70
|
+
|
|
71
|
+
## Addresses
|
|
72
|
+
|
|
73
|
+
| Option | |
|
|
74
|
+
| ------ | - |
|
|
75
|
+
| `allowlist` | exempt from detection **entirely** — your monitors, your office, your CI |
|
|
76
|
+
| `denylist` | treated as proven automation; an explicit local decision |
|
|
77
|
+
| `datacenterRanges` | hosting-provider ranges; none ships with the library |
|
|
78
|
+
| `crawlerRanges` | published ranges keyed by signature id — see [verification](../detection/verification.md) |
|
|
79
|
+
|
|
80
|
+
Invalid CIDRs **throw at construction**. A range that silently matches nothing is a control
|
|
81
|
+
you believe you have and do not.
|
|
82
|
+
|
|
83
|
+
`allowlist` is the strongest thing here: an address on it is not judged leniently, it is not
|
|
84
|
+
judged at all. Do not put loopback on it — see [the client IP](../integration/client-ip.md).
|
|
85
|
+
|
|
86
|
+
## Identity and scope
|
|
87
|
+
|
|
88
|
+
| Option | Default | |
|
|
89
|
+
| ------ | ------- | - |
|
|
90
|
+
| `proxy` | `{ trustProxy: false }` | **read [the client IP](../integration/client-ip.md) first** |
|
|
91
|
+
| `actorKey` | the client address | the single most valuable thing to replace |
|
|
92
|
+
| `ignorePaths` | — | health checks, your own polling endpoints, static assets |
|
|
93
|
+
| `isHuman` | — | your application declaring a request human |
|
|
94
|
+
|
|
95
|
+
`isHuman` produces `certain` human evidence — the only conclusive human signal available,
|
|
96
|
+
because it comes from you and not from the client. An authenticated session, a completed
|
|
97
|
+
payment, whatever bar you set:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
isHuman: (facts) => Boolean(facts.session && sessions.get(facts.session)?.authenticated)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Observation
|
|
104
|
+
|
|
105
|
+
`audit` takes [`AuditOptions`](../operations/audit.md) or `false`. Every `onX` hook mirrors
|
|
106
|
+
an [event](../operations/index.md#events-and-hooks) — the config form is convenient, the
|
|
107
|
+
emitter form can be subscribed to later and removed.
|
|
108
|
+
|
|
109
|
+
## Infrastructure
|
|
110
|
+
|
|
111
|
+
| Option | Default | |
|
|
112
|
+
| ------ | ------- | - |
|
|
113
|
+
| `challenge` | — | enables the [challenge action](../challenge/index.md); without it those rules degrade to `tag` |
|
|
114
|
+
| `store` | in-memory | see [stores](../integration/stores.md) |
|
|
115
|
+
| `shareConfirmations` | `false` | proof crosses replicas; suspicion does not. Needs a shared store |
|
|
116
|
+
| `notifications` | — | [sinks, filtering, redaction](../operations/notifications.md) |
|
|
117
|
+
| `handlers` | — | [`custom` action](../policy/actions.md#custom) handlers |
|
|
118
|
+
| `resolver`, `clock` | system | injected for tests and the [corpus](../testing/corpus.md) |
|
|
119
|
+
| `actorWindowMs` | `900_000` | how long an idle [actor](../concepts/actors.md) is remembered |
|
|
120
|
+
| `maxActors` | `20_000` | a memory budget; past it the least recently seen actor is evicted |
|
|
121
|
+
| `exposeVerdictHeaders` | `false` | **leave it off** — see below |
|
|
122
|
+
| `metrics` | `true` | or `{ perDetectorTiming: true }` |
|
|
123
|
+
| `onError`, `onWarning` | — | |
|
|
124
|
+
|
|
125
|
+
### `exposeVerdictHeaders`
|
|
126
|
+
|
|
127
|
+
An `X-Bot-Score` in the *response* is a live feedback signal for anyone tuning a scraper
|
|
128
|
+
against you: change one header, watch the number fall, iterate. Request-side tagging gives
|
|
129
|
+
your application the same information and tells the client nothing. It is off by default and
|
|
130
|
+
should stay off.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Performance and memory
|
|
135
|
+
|
|
136
|
+
Detection on a clean browser request costs about **9 µs** to assess and **12 µs** end to
|
|
137
|
+
end, and allocates a few hundred bytes. `npm run bench` reports the median of several
|
|
138
|
+
rounds; `npm run bench:guard` enforces a ratio-based budget so a regression fails the build
|
|
139
|
+
rather than shipping quietly.
|
|
140
|
+
|
|
141
|
+
Five things got it there, each a trap the next person will meet too:
|
|
142
|
+
|
|
143
|
+
- `header-order` computed an HMAC fingerprint on every request before knowing whether it had
|
|
144
|
+
anything to report — it cost more than the other twelve detectors combined.
|
|
145
|
+
- `randomBytes` was called per request for the request id; it is now drawn from a pooled
|
|
146
|
+
buffer.
|
|
147
|
+
- The User-Agent tokeniser ran eagerly, when a mainstream browser string never needs it.
|
|
148
|
+
- The notification event — an ISO timestamp included — was built before checking whether any
|
|
149
|
+
sink was listening.
|
|
150
|
+
- Per-detector timing allocated a closure per detector per request *whether or not it was
|
|
151
|
+
switched on*: 8 µs a request to support a feature that is off by default. The benchmark
|
|
152
|
+
caught that one, which is what a benchmark is for.
|
|
153
|
+
|
|
154
|
+
Signatures compile once into an Aho–Corasick automaton with a flat transition table for the
|
|
155
|
+
root node, so matching is O(input length) regardless of how many signatures exist, and text
|
|
156
|
+
that matches nothing allocates nothing at all.
|
|
157
|
+
|
|
158
|
+
DNS runs only when a request actually claimed a verifiable identity, and results are cached:
|
|
159
|
+
successes for an hour, transient failures for a minute, definitive absences for an hour.
|
|
160
|
+
|
|
161
|
+
Per-actor state is a fixed budget: 32 arrival timestamps, 64 path *hashes* (not strings), up
|
|
162
|
+
to 4 User-Agents. At 20,000 tracked actors that is tens of megabytes, not hundreds. Every
|
|
163
|
+
structure keyed by anything a client controls is a bounded LRU — an unbounded map here would
|
|
164
|
+
be a remote OOM.
|
|
165
|
+
|
|
166
|
+
The trade-off: `requestsWithin` saturates at 32, so a very heavy actor is reported as "at
|
|
167
|
+
least 32" with `undercounted: true` rather than at its true rate. Deliberate — that series
|
|
168
|
+
exists to describe an actor cheaply, and exact counting belongs to the
|
|
169
|
+
[`rate-limit`](../policy/actions.md#rate-limit) action, which uses the store.
|
|
170
|
+
|
|
171
|
+
## Related
|
|
172
|
+
|
|
173
|
+
- [API](api.md) — the methods and the exported names
|
|
174
|
+
- [The guard](../concepts/the-guard.md) — the four options that decide how far a rule may go
|
|
175
|
+
- [Runtime changes](../operations/runtime-changes.md) — what can be changed without a restart
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Choosing a policy
|
|
2
|
+
|
|
3
|
+
The decision, start to finish.
|
|
4
|
+
|
|
5
|
+
← [Documentation](../index.md) · [Getting started](../index.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Start with the question, not the preset
|
|
10
|
+
|
|
11
|
+
Not "how do I block bots" but: **what is this site for, and what would it cost me to be
|
|
12
|
+
wrong?**
|
|
13
|
+
|
|
14
|
+
| If being wrong about a *person* costs you… | and being wrong about a *bot* costs you… | start with |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| a reader | some bandwidth | [`allow-crawlers`](../policy/presets.md#allow-crawlers) |
|
|
17
|
+
| a customer | your content, scraped | [`protect-content`](../policy/presets.md#protect-content) |
|
|
18
|
+
| a customer | your pricing, in a competitor's spreadsheet | [`protect-data`](../policy/presets.md#protect-data) |
|
|
19
|
+
| a broken integration | an API bill | [`protect-api`](../policy/presets.md#protect-api) |
|
|
20
|
+
| a lost signup | a compromised account | [`protect-auth`](../policy/presets.md#protect-auth), on those routes |
|
|
21
|
+
| your search traffic | your work in a training corpus | [`decline-ai-training`](../policy/presets.md#decline-ai-training) |
|
|
22
|
+
|
|
23
|
+
Whatever the answer, **the first week is [`monitor-only`](../policy/presets.md#monitor-only)**.
|
|
24
|
+
|
|
25
|
+
## The four questions worth answering explicitly
|
|
26
|
+
|
|
27
|
+
### 1. Which crawlers do you actually want?
|
|
28
|
+
|
|
29
|
+
This is a business decision, not a security one, and it deserves to be a visible rule you
|
|
30
|
+
can point at rather than a threshold somebody tuned.
|
|
31
|
+
|
|
32
|
+
Search and social crawlers bring readers. AI training crawlers take work and return nothing
|
|
33
|
+
you can measure. SEO crawlers serve your competitors. Feed readers and link unfurlers are
|
|
34
|
+
somebody's actual reading habit.
|
|
35
|
+
|
|
36
|
+
If you decline any of them, **say so in [`robots.txt`](../policy/robots.md)** — the ones
|
|
37
|
+
worth declining honour it, and a rule that blocks a crawler nobody told is load with no
|
|
38
|
+
compliance.
|
|
39
|
+
|
|
40
|
+
### 2. What happens to suspicion?
|
|
41
|
+
|
|
42
|
+
The [guard](../concepts/the-guard.md) settles the top of the range: unproven never means
|
|
43
|
+
denied. Below that, you choose between `tag`, `delay`, `rate-limit` and `challenge`, and the
|
|
44
|
+
right answer depends on who your unusual visitors are.
|
|
45
|
+
|
|
46
|
+
A `challenge` needs a browser. On an API it breaks your customers' integrations and stops
|
|
47
|
+
nobody, which is why [`protect-api`](../policy/presets.md#protect-api) uses rate limits
|
|
48
|
+
instead. A `delay` excludes nobody at all and is the quiet choice on a form.
|
|
49
|
+
|
|
50
|
+
Look at [`bothandler_score_bucket`](../operations/metrics.md) before you move a threshold. It
|
|
51
|
+
tells you how many requests sit in the ten points you are about to cross.
|
|
52
|
+
|
|
53
|
+
### 3. Where does the policy *not* apply?
|
|
54
|
+
|
|
55
|
+
`ignorePaths` for health checks and static assets. `allowlist` for your monitors, your
|
|
56
|
+
office and your CI — and remember that an allowlisted address is not judged leniently, it is
|
|
57
|
+
not judged at all.
|
|
58
|
+
|
|
59
|
+
And the routes that need a *different* policy get their own handler:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
app.use(botHandler(siteDetector));
|
|
63
|
+
app.use("/login", botHandler(authDetector));
|
|
64
|
+
app.use("/checkout", botHandler(authDetector));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 4. Who is allowed to change it, and how fast?
|
|
68
|
+
|
|
69
|
+
An incident is when you will want to change a rule without a deploy. Decide now whether
|
|
70
|
+
[the dashboard's editor](../operations/runtime-changes.md) is enabled, who can reach it, and
|
|
71
|
+
whether the [guard panel](../concepts/the-guard.md) is enabled separately — it is a different
|
|
72
|
+
power with different consequences.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Check it before it meets anybody
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx @osqd/bothandlerjs check --preset protect-content --audience human
|
|
80
|
+
npx @osqd/bothandlerjs replay /var/log/nginx/access.log --preset protect-content
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The first is [526 shapes of real traffic](../testing/corpus.md); the second is *yours*. Both
|
|
84
|
+
print every request the policy would have refused, with the evidence.
|
|
85
|
+
|
|
86
|
+
For your own configuration rather than a preset:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { runCorpus } from "@osqd/bothandlerjs/corpus";
|
|
90
|
+
const scorecard = await runCorpus({
|
|
91
|
+
create: ({ resolver, clock }) => new BotHandler({ ...myConfig, resolver, clock }),
|
|
92
|
+
assertActions: false,
|
|
93
|
+
});
|
|
94
|
+
if (scorecard.falsePositives.length > 0) throw new Error("this policy turns people away");
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Owning it rather than naming it
|
|
98
|
+
|
|
99
|
+
A preset is an array of [rules](../policy/rules.md). Once you have changed one thing, take
|
|
100
|
+
the whole set:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { protectContent } from "@osqd/bothandlerjs";
|
|
104
|
+
|
|
105
|
+
const rules = protectContent()
|
|
106
|
+
.filter((rule) => rule.id !== "http-client-challenge") // our partners use curl
|
|
107
|
+
.concat({ id: "partner-allow", match: { path: "/api/partner" }, action: "allow" });
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Your own rules are evaluated **before** a preset's, so `rules` plus `preset` is a valid way
|
|
111
|
+
to prepend exceptions without copying anything.
|
|
112
|
+
|
|
113
|
+
## What to watch once it is live
|
|
114
|
+
|
|
115
|
+
| Signal | Means |
|
|
116
|
+
| ------ | ----- |
|
|
117
|
+
| `bothandler_downgrades_total` rising | your rules ask for more than the evidence supports |
|
|
118
|
+
| `challenge-solve-rate` high | the challenges are taxing people, not filtering bots |
|
|
119
|
+
| `verdicts_total{verdict="unknown"}` falling | your traffic changed, or your detection did |
|
|
120
|
+
| a `denial-spike` anomaly | read the denials before assuming they are all bots |
|
|
121
|
+
|
|
122
|
+
## Next
|
|
123
|
+
|
|
124
|
+
- [Presets](../policy/presets.md) — all eight, in detail
|
|
125
|
+
- [Rules](../policy/rules.md) — the grammar
|
|
126
|
+
- [Actions](../policy/actions.md) — ordered by what each costs a person
|
|
127
|
+
- [Threat model](../concepts/threat-model.md) — what none of this can do
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Your first integration
|
|
2
|
+
|
|
3
|
+
Working in five minutes, safe in one week.
|
|
4
|
+
|
|
5
|
+
← [Documentation](../index.md) · [Getting started](../index.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The five minutes
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { BotHandler } from "@osqd/bothandlerjs";
|
|
13
|
+
import { botHandler } from "@osqd/bothandlerjs/adapters";
|
|
14
|
+
|
|
15
|
+
const detector = new BotHandler({
|
|
16
|
+
preset: "monitor-only", // watch first; enforce later
|
|
17
|
+
allowlist: ["10.0.0.0/8"], // your monitors and CI
|
|
18
|
+
ignorePaths: ["/healthz", "/metrics"],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
app.use(botHandler(detector));
|
|
22
|
+
|
|
23
|
+
await detector.serveDashboard({ port: 9674 });
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Open **http://localhost:9674/**. Every request lands in the feed with its verdict, and any
|
|
27
|
+
row opens to show the [evidence](../concepts/evidence.md) behind it.
|
|
28
|
+
|
|
29
|
+
Nothing is being withheld from anybody — [`monitor-only`](../policy/presets.md#monitor-only)
|
|
30
|
+
never acts. That is deliberate, and it is step one of four.
|
|
31
|
+
|
|
32
|
+
## The four steps
|
|
33
|
+
|
|
34
|
+
### 1. Watch, for a week
|
|
35
|
+
|
|
36
|
+
Run `monitor-only` on real traffic and look at the dashboard. You are looking for the
|
|
37
|
+
integration you forgot about: the partner's nightly sync, the status-page prober, the
|
|
38
|
+
marketing team's link checker, your own server-side renderer.
|
|
39
|
+
|
|
40
|
+
Every bot policy that has caused an outage was deployed straight to enforcement by someone
|
|
41
|
+
who was sure they knew what their traffic looked like.
|
|
42
|
+
|
|
43
|
+
### 2. Fix the client address
|
|
44
|
+
|
|
45
|
+
If anything sits in front of your process — a load balancer, a CDN, nginx, a service mesh —
|
|
46
|
+
this is the setting that matters most:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
proxy: { trustProxy: true, trustedProxies: ["10.0.0.0/8"] }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The client address becomes the [actor key](../concepts/actors.md), which every rate limit,
|
|
53
|
+
allowlist entry and behavioural signal depends on. Getting it wrong is silent. Read
|
|
54
|
+
[the client IP](../integration/client-ip.md) — it is short and it is the page most worth
|
|
55
|
+
reading twice.
|
|
56
|
+
|
|
57
|
+
### 3. Check the policy you are about to enable
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx @osqd/bothandlerjs check --preset protect-content
|
|
61
|
+
npx @osqd/bothandlerjs replay /var/log/nginx/access.log --preset protect-content
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The first runs your policy against [526 shapes of real traffic](../testing/corpus.md); the
|
|
65
|
+
second runs it against yours. Read the list of would-be-denied requests. If any of them is a
|
|
66
|
+
person, the policy is wrong — and you found out from a log file.
|
|
67
|
+
|
|
68
|
+
Put the `check` in CI. It exits non-zero when a case marked as a person is denied.
|
|
69
|
+
|
|
70
|
+
### 4. Enable it
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const detector = new BotHandler({
|
|
74
|
+
preset: "protect-content",
|
|
75
|
+
proxy: { trustProxy: true, trustedProxies: ["10.0.0.0/8"] },
|
|
76
|
+
challenge: {
|
|
77
|
+
secrets: [process.env.BOT_CHALLENGE_SECRET!],
|
|
78
|
+
contactHtml: '<p>Locked out? Email <a href="mailto:support@example.com">support@example.com</a>.</p>',
|
|
79
|
+
},
|
|
80
|
+
allowlist: ["10.0.0.0/8"],
|
|
81
|
+
ignorePaths: ["/healthz", "/metrics"],
|
|
82
|
+
onDowngrade: ({ decision }) => log.warn(`rule ${decision.rule} asked for more than its evidence`),
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then watch `bothandler_downgrades_total` — see [metrics](../operations/metrics.md).
|
|
87
|
+
|
|
88
|
+
## What a working configuration does
|
|
89
|
+
|
|
90
|
+
| Request | Result |
|
|
91
|
+
| ------- | ------ |
|
|
92
|
+
| A real Chrome navigation | `200` — served, untouched |
|
|
93
|
+
| `curl https://yoursite/` | challenge page — proven `http-client` |
|
|
94
|
+
| A `Googlebot` UA from an address DNS refutes | `403` — proven `impersonator` |
|
|
95
|
+
| `sqlmap` | `403` — self-identified scanner |
|
|
96
|
+
| A hit on a [trap](../detection/detectors.md) link | `403` — no person can reach it |
|
|
97
|
+
| `GET /healthz` | `200` — never assessed |
|
|
98
|
+
| Real Googlebot, confirmed by DNS | `200` — explicitly allowed |
|
|
99
|
+
|
|
100
|
+
## Three things people wish they had done sooner
|
|
101
|
+
|
|
102
|
+
**Replace `actorKey`.** An address is a poor identity — shared by an office, changed by a
|
|
103
|
+
phone every few minutes. A session id makes every behavioural detector sharper:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
actorKey: (facts) => facts.session ?? facts.ip
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Add `isHuman`.** Your authenticated session is the only conclusive human signal that
|
|
110
|
+
exists. Telling the library about it stops it second-guessing your own customers:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
isHuman: (facts) => Boolean(sessions.get(facts.session ?? "")?.authenticated)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Mount [`protect-auth`](../policy/presets.md#protect-auth) separately** on login, signup and
|
|
117
|
+
checkout — and *only* there. Site-wide it refuses your webhooks and your renderer.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
app.use("/login", botHandler(authDetector));
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Next
|
|
124
|
+
|
|
125
|
+
- [Choosing a policy](choosing-a-policy.md)
|
|
126
|
+
- [Adapters](../integration/adapters.md) — Fastify, Koa, Hono, Next.js, Workers
|
|
127
|
+
- [The dashboard](../operations/dashboard.md) — before you expose it anywhere real
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
← [Documentation](../index.md) · [Getting started](../index.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @osqd/bothandlerjs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Node 18 or later.** ESM and CJS builds, TypeScript declarations included.
|
|
12
|
+
|
|
13
|
+
## Zero runtime dependencies
|
|
14
|
+
|
|
15
|
+
The library imports nothing but `node:` builtins, and CI fails if that ever stops being
|
|
16
|
+
true. Nothing here can hand your project a transitive advisory, an install script, or a
|
|
17
|
+
version conflict with something you already run.
|
|
18
|
+
|
|
19
|
+
Redis, if you use it, is your client passed in: `RedisStore` describes the five commands it
|
|
20
|
+
needs structurally and imports neither `ioredis` nor `node-redis`.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { RedisStore } from "@osqd/bothandlerjs";
|
|
24
|
+
new BotHandler({ store: new RedisStore(redis) });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## The entry points
|
|
28
|
+
|
|
29
|
+
| Import | Contains |
|
|
30
|
+
| ------ | -------- |
|
|
31
|
+
| `@osqd/bothandlerjs` | the engine, detectors, presets, robots, stores, notifiers, challenge, dashboard |
|
|
32
|
+
| `@osqd/bothandlerjs/adapters` | [Express, Fastify, Koa, Fetch](../integration/adapters.md) |
|
|
33
|
+
| `@osqd/bothandlerjs/client` | the browser-side [client signals](../detection/client-signals.md) script |
|
|
34
|
+
| `@osqd/bothandlerjs/corpus` | [`runCorpus`](../testing/corpus.md) and all 526 cases |
|
|
35
|
+
| `@osqd/bothandlerjs/cli` | the [command line](../testing/cli.md) |
|
|
36
|
+
|
|
37
|
+
## Runtimes
|
|
38
|
+
|
|
39
|
+
| Runtime | Notes |
|
|
40
|
+
| ------- | ----- |
|
|
41
|
+
| Node 18+ | everything works |
|
|
42
|
+
| Bun, Deno | use the [Fetch adapter](../integration/adapters.md) |
|
|
43
|
+
| Cloudflare Workers | enable `nodejs_compat` — the engine uses `node:crypto` |
|
|
44
|
+
| Vercel Edge, Netlify Edge | Fetch adapter; set `clientIp` explicitly |
|
|
45
|
+
|
|
46
|
+
On every Fetch runtime, two things differ. There is no socket, so the client address comes
|
|
47
|
+
from a header — read [the client IP](../integration/client-ip.md) before you deploy. And
|
|
48
|
+
header order is normalised, so `header-order` returns nothing there; drop it.
|
|
49
|
+
|
|
50
|
+
## Without installing anything
|
|
51
|
+
|
|
52
|
+
Every CLI command runs through `npx`:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx @osqd/bothandlerjs explain "curl/8.4.0"
|
|
56
|
+
npx @osqd/bothandlerjs check --preset protect-content
|
|
57
|
+
npx @osqd/bothandlerjs replay /var/log/nginx/access.log
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Next
|
|
61
|
+
|
|
62
|
+
- [Your first integration](first-integration.md)
|
|
63
|
+
- [Choosing a policy](choosing-a-policy.md)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Upgrading
|
|
2
|
+
|
|
3
|
+
← [Documentation](../index.md) · [Getting started](../index.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Versioning
|
|
8
|
+
|
|
9
|
+
Semantic versioning, with one clarification that matters for a library like this:
|
|
10
|
+
|
|
11
|
+
**A new detector, or a signature added to the database, is a minor release.** It can change
|
|
12
|
+
what your policy does to a request that was previously unrecognised — that is the point of
|
|
13
|
+
installing it — but it cannot change the [guard's](../concepts/the-guard.md) guarantee, and
|
|
14
|
+
it cannot turn a probabilistic verdict into a proven one.
|
|
15
|
+
|
|
16
|
+
**A change to how a tier is assigned is a major release.** Moving something from `strong` to
|
|
17
|
+
`certain` changes what may be denied, which is exactly the boundary this library exists to
|
|
18
|
+
hold still.
|
|
19
|
+
|
|
20
|
+
## Before you upgrade
|
|
21
|
+
|
|
22
|
+
Run your own configuration against the corpus and compare:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @osqd/bothandlerjs check --preset protect-content --json > before.json
|
|
26
|
+
npm install @osqd/bothandlerjs@latest
|
|
27
|
+
npx @osqd/bothandlerjs check --preset protect-content --json > after.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or, for a configuration that is not a preset:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { runCorpus } from "@osqd/bothandlerjs/corpus";
|
|
34
|
+
const scorecard = await runCorpus({
|
|
35
|
+
create: ({ resolver, clock }) => new BotHandler({ ...myConfig, resolver, clock }),
|
|
36
|
+
assertActions: false,
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The invariant — nothing marked as a person is denied — holds across versions and is checked
|
|
41
|
+
on every release. What can move is which *bots* a policy catches, and the scorecard shows
|
|
42
|
+
that as a diff rather than a surprise.
|
|
43
|
+
|
|
44
|
+
## 0.x
|
|
45
|
+
|
|
46
|
+
The library is pre-1.0. The engine, the evidence model, the guard and the policy grammar are
|
|
47
|
+
stable and are what everything else is built on; the surfaces most likely to change before
|
|
48
|
+
1.0 are the dashboard's options and the shape of the corpus scorecard.
|
|
49
|
+
|
|
50
|
+
Pin an exact version if you depend on either:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{ "dependencies": { "@osqd/bothandlerjs": "0.2.0" } }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## When a signature changes hands
|
|
57
|
+
|
|
58
|
+
Not a library upgrade, but the same class of problem: a crawler's published ranges go stale,
|
|
59
|
+
and a stale list turns a verified crawler into an accused impersonator.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { startCrawlerRangeRefresh } from "@osqd/bothandlerjs";
|
|
63
|
+
const stop = startCrawlerRangeRefresh(botHandler); // twice a day
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See [verification](../detection/verification.md) and
|
|
67
|
+
[runtime changes](../operations/runtime-changes.md).
|
|
68
|
+
|
|
69
|
+
## Related
|
|
70
|
+
|
|
71
|
+
- [The corpus](../testing/corpus.md) — the regression suite you can run yourself
|
|
72
|
+
- [Design decisions](../design/decisions.md) — what a future change would be undoing
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# The CLI
|
|
2
|
+
|
|
3
|
+
Five commands, no server, no installation.
|
|
4
|
+
|
|
5
|
+
← [Documentation](../index.md) · [Testing](index.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
bothandlerjs replay <file> [options] Replay an access log and report what would have happened
|
|
11
|
+
bothandlerjs check [--preset <p>] Run a policy against the traffic corpus: who would it hurt?
|
|
12
|
+
bothandlerjs explain [request] Assess one request and show the evidence behind the verdict
|
|
13
|
+
bothandlerjs robots [options] Generate a robots.txt from a policy preset
|
|
14
|
+
bothandlerjs detectors [--preset <p>] List the detectors a configuration installs
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## `explain`
|
|
20
|
+
|
|
21
|
+
The question that arrives by ticket rather than by traffic.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bothandlerjs explain "curl/8.4.0"
|
|
25
|
+
pbpaste | bothandlerjs explain --preset protect-data
|
|
26
|
+
bothandlerjs explain --ip 203.0.113.9 --url /checkout "Mozilla/5.0 ..."
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
It takes a User-Agent, a `curl` command copied out of devtools, or a raw header block — from
|
|
30
|
+
an argument or from stdin — and prints the verdict, the rule that would fire, and every
|
|
31
|
+
piece of [evidence](../concepts/evidence.md) behind it.
|
|
32
|
+
|
|
33
|
+
It runs as a **[dry run](../detection/index.md)**, so nothing is recorded and asking does
|
|
34
|
+
not change the answer. It has no history by construction, so what it answers is *what would
|
|
35
|
+
this look like as a first request* — which is what a ticket is asking anyway.
|
|
36
|
+
|
|
37
|
+
## `check`
|
|
38
|
+
|
|
39
|
+
The CI step.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx @osqd/bothandlerjs check --preset protect-content
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
protect-content against 522 shapes of real traffic
|
|
47
|
+
|
|
48
|
+
human 181 cases 3 tag, 175 allow, 3 challenge
|
|
49
|
+
benign-bot 144 cases 6 allow, 86 tag, 48 block, 2 challenge, 2 rate-limit
|
|
50
|
+
declared-bot 32 cases 18 tag, 2 rate-limit, 3 allow, 9 block
|
|
51
|
+
unwanted-bot 105 cases 8 tag, 11 rate-limit, 3 block, 70 challenge, 13 allow
|
|
52
|
+
hostile 27 cases 6 challenge, 17 block, 4 allow
|
|
53
|
+
infrastructure 33 cases 7 allow, 12 tag, 14 challenge
|
|
54
|
+
|
|
55
|
+
No case marked as a person was denied service.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The question this library is organised around, asked before a deploy rather than after one:
|
|
59
|
+
*if I point this configuration at the actual internet, who gets hurt?*
|
|
60
|
+
|
|
61
|
+
**It exits non-zero if any case marked `human` is denied service**, which is what makes it a
|
|
62
|
+
CI step rather than a report.
|
|
63
|
+
|
|
64
|
+
| Option | |
|
|
65
|
+
| ------ | - |
|
|
66
|
+
| `--preset <name>` | which policy to test (default `protect-content`) |
|
|
67
|
+
| `--audience <a>` | `human` \| `benign-bot` \| `declared-bot` \| `unwanted-bot` \| `hostile` \| `infrastructure` |
|
|
68
|
+
| `--json` | the scorecard, for a pipeline |
|
|
69
|
+
| `--strict` | also fail on differing actions, not only the invariants |
|
|
70
|
+
|
|
71
|
+
`--audience human` is the one to reach for first: it shows only the part that matters.
|
|
72
|
+
|
|
73
|
+
See [the corpus](corpus.md) for what it is running against.
|
|
74
|
+
|
|
75
|
+
## `replay`
|
|
76
|
+
|
|
77
|
+
Your policy against your own traffic. See [log replay](replay.md) — including the reason
|
|
78
|
+
JSON Lines is much better than CLF.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx @osqd/bothandlerjs replay /var/log/nginx/access.log --preset protect-content
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Option | |
|
|
85
|
+
| ------ | - |
|
|
86
|
+
| `--preset <name>` | default `protect-content` |
|
|
87
|
+
| `--limit <n>` | stop after n parsed lines |
|
|
88
|
+
| `--show <n>` | how many would-be-denied requests to print in full (default 10) |
|
|
89
|
+
| `--format <f>` | `clf` \| `json` (detected from the first line by default) |
|
|
90
|
+
| `--json` | the report, machine-readable |
|
|
91
|
+
|
|
92
|
+
## `robots`
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
bothandlerjs robots --preset decline-ai-training --sitemap https://example.com/sitemap.xml > public/robots.txt
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Generates a `robots.txt` from a preset's rules (default preset: `protect-data`), with the
|
|
99
|
+
trap paths disallowed for you.
|
|
100
|
+
|
|
101
|
+
Notes about rules that could not be reflected go to **stderr**, so a redirect into the file
|
|
102
|
+
stays clean while the reasoning still reaches whoever ran the command. Read them — see
|
|
103
|
+
[robots.txt](../policy/robots.md) for why an unreflected rule matters.
|
|
104
|
+
|
|
105
|
+
## `detectors`
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
bothandlerjs detectors --preset protect-api
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Lists what a configuration actually installs, with each detector's cost and stage. Useful
|
|
112
|
+
for the question "is `identity-rotation` on?", which has caused more confusion than any
|
|
113
|
+
other single setting.
|
|
114
|
+
|
|
115
|
+
## Related
|
|
116
|
+
|
|
117
|
+
- [The corpus](corpus.md) · [Log replay](replay.md)
|
|
118
|
+
- [Detectors](../detection/detectors.md) — what `detectors` is listing
|
|
119
|
+
- [robots.txt](../policy/robots.md) — what `robots` is generating
|