@osqd/bothandlerjs 0.4.0 → 0.6.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/dist/adapters/index.cjs +3 -0
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/cli.cjs +634 -25
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +634 -25
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/core.d.ts +15 -0
- package/dist/corpus/index.cjs +84 -0
- package/dist/corpus/index.cjs.map +1 -1
- package/dist/corpus/index.js +84 -0
- package/dist/corpus/index.js.map +1 -1
- package/dist/corpus/schema.d.ts +8 -0
- package/dist/crawler-ranges.d.ts +31 -0
- package/dist/dashboard/client/feed.d.ts +16 -0
- package/dist/dashboard/client/format.d.ts +18 -0
- package/dist/dashboard/client/pager.d.ts +32 -0
- package/dist/dashboard/client/store.d.ts +60 -0
- package/dist/dashboard/client.generated.d.ts +1 -1
- package/dist/detectors/crawler-verification.d.ts +34 -1
- package/dist/detectors/id-enumeration.d.ts +31 -0
- package/dist/detectors/index.d.ts +12 -1
- package/dist/detectors/known-bots.d.ts +60 -1
- package/dist/detectors/parameter-sweep.d.ts +39 -0
- package/dist/detectors/probe-volume.d.ts +26 -0
- package/dist/detectors/transport-coherence.d.ts +31 -0
- package/dist/element/index.cjs +349 -40
- package/dist/element/index.cjs.map +1 -1
- package/dist/element/index.js +349 -40
- package/dist/element/index.js.map +1 -1
- package/dist/index.cjs +576 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +571 -31
- package/dist/index.js.map +1 -1
- package/dist/state.d.ts +76 -1
- package/dist/types.d.ts +36 -0
- package/docs/course/05-detectors.md +4 -0
- package/docs/detection/detectors.md +122 -0
- package/docs/detection/signatures.md +44 -0
- package/docs/operations/dashboard.md +26 -0
- package/package.json +1 -1
package/dist/state.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ActorSnapshot, RequestFacts } from "./types.js";
|
|
|
12
12
|
*/
|
|
13
13
|
export declare const MAX_TRACKED_ARRIVALS = 32;
|
|
14
14
|
export declare const MAX_TRACKED_PATHS = 64;
|
|
15
|
+
export declare const MAX_TRACKED_QUERIES = 64;
|
|
15
16
|
export declare const MAX_TRACKED_USER_AGENTS = 4;
|
|
16
17
|
export declare class ActorState {
|
|
17
18
|
readonly key: string;
|
|
@@ -53,6 +54,45 @@ export declare class ActorState {
|
|
|
53
54
|
private readonly paths;
|
|
54
55
|
private pathsOverflowed;
|
|
55
56
|
private pathsSaturatedAtTotal;
|
|
57
|
+
/**
|
|
58
|
+
* Distinct *parameterised* requests: the path together with its query.
|
|
59
|
+
*
|
|
60
|
+
* Counted apart from `paths` because the two answer different questions and a scraper
|
|
61
|
+
* lives in the gap between them. `/products?page=1` through `?page=200` is one path and
|
|
62
|
+
* two hundred requests, so breadth reads it as somebody rereading a single page — which
|
|
63
|
+
* is exactly what enumerating a catalogue looks like from the path alone.
|
|
64
|
+
*/
|
|
65
|
+
private readonly queries;
|
|
66
|
+
private queriesOverflowed;
|
|
67
|
+
/**
|
|
68
|
+
* Which HTTP methods this actor has used.
|
|
69
|
+
*
|
|
70
|
+
* A browser navigating issues GET. Something that has issued nothing but HEAD across a
|
|
71
|
+
* long visit is checking what exists rather than reading it, and that is a fact about
|
|
72
|
+
* the actor rather than about any one of its requests — which is why it is kept here.
|
|
73
|
+
*/
|
|
74
|
+
private readonly methods;
|
|
75
|
+
/**
|
|
76
|
+
* Numeric walks in progress, by path shape: `/user/#` against the ids requested under it.
|
|
77
|
+
*
|
|
78
|
+
* Three numbers per shape, deliberately — a count, a lowest and a highest — rather than
|
|
79
|
+
* the ids themselves. What separates enumeration from reading is not which ids were
|
|
80
|
+
* asked for but whether they *cover a range*: thirty requests spanning thirty
|
|
81
|
+
* consecutive ids is a walk, and thirty scattered across a hundred thousand is somebody
|
|
82
|
+
* following links. Both are answerable from a count and a span, and only the count and
|
|
83
|
+
* the span survive an actor asking for ten thousand of them.
|
|
84
|
+
*/
|
|
85
|
+
private readonly walks;
|
|
86
|
+
/**
|
|
87
|
+
* What the application answered, for the requests anybody bothered to tell us about.
|
|
88
|
+
*
|
|
89
|
+
* The engine decides *before* the response exists, so this arrives afterwards and only
|
|
90
|
+
* when the adapter reports it. Kept as two counters rather than a list because the one
|
|
91
|
+
* question worth asking is a ratio: an actor whose requests are almost all misses is
|
|
92
|
+
* looking for something rather than reading anything.
|
|
93
|
+
*/
|
|
94
|
+
private responsesSeen;
|
|
95
|
+
private missesSeen;
|
|
56
96
|
private readonly userAgents;
|
|
57
97
|
constructor(key: string, now: number);
|
|
58
98
|
/** Records an arrival. Called exactly once per request, by the engine. */
|
|
@@ -60,6 +100,41 @@ export declare class ActorState {
|
|
|
60
100
|
/** Distinct paths seen, by hash. Saturates at {@link PATH_CAP}; `pathsSaturated` says whether it did. */
|
|
61
101
|
get distinctPaths(): number;
|
|
62
102
|
get pathsSaturated(): boolean;
|
|
103
|
+
/** Distinct path-and-query combinations seen. Saturates at {@link QUERY_CAP}. */
|
|
104
|
+
get distinctQueries(): number;
|
|
105
|
+
get queriesSaturated(): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Records what the application answered. Called after the response, if at all.
|
|
108
|
+
*
|
|
109
|
+
* 404 and 410 only. A 403 is usually this library's own doing and counting it would
|
|
110
|
+
* make the detector that reads this argue with itself; a 500 is the site's problem and
|
|
111
|
+
* says nothing about the client.
|
|
112
|
+
*/
|
|
113
|
+
recordOutcome(status: number): void;
|
|
114
|
+
/** Responses reported for this actor. Zero unless something is reporting them. */
|
|
115
|
+
get responses(): number;
|
|
116
|
+
/** Of those, how many were 404 or 410. */
|
|
117
|
+
get misses(): number;
|
|
118
|
+
/**
|
|
119
|
+
* Files a request under the shape of its path, if that path carries a number.
|
|
120
|
+
*
|
|
121
|
+
* The last numeric segment is the one taken to be the identifier: in `/api/v2/orders/42`
|
|
122
|
+
* the version is part of the shape and the order id is what is being walked.
|
|
123
|
+
*/
|
|
124
|
+
private noteWalk;
|
|
125
|
+
/**
|
|
126
|
+
* The path shape this actor has walked hardest, with how far it reached.
|
|
127
|
+
*
|
|
128
|
+
* `span` is inclusive of both ends, so a walk of 1 to 30 spans 30. Comparing the count
|
|
129
|
+
* against it is what separates covering a range from visiting a few points in one.
|
|
130
|
+
*/
|
|
131
|
+
densestWalk(): {
|
|
132
|
+
template: string;
|
|
133
|
+
count: number;
|
|
134
|
+
span: number;
|
|
135
|
+
} | undefined;
|
|
136
|
+
/** Every HTTP method this actor has used, in first-seen order. */
|
|
137
|
+
get methodsSeen(): readonly string[];
|
|
63
138
|
/**
|
|
64
139
|
* Requests seen when {@link distinctPaths} stopped being able to grow, or 0 if it
|
|
65
140
|
* still can. Over that many requests the distinct count is exact, so it is the only
|
|
@@ -153,7 +228,7 @@ export declare class ActorRegistry {
|
|
|
153
228
|
* A read, and only a read: it neither records a request against an actor nor moves
|
|
154
229
|
* one up the LRU, so watching the list cannot change what it lists.
|
|
155
230
|
*/
|
|
156
|
-
top(limit: number, now: number): ActorSummary[];
|
|
231
|
+
top(limit: number, now: number, offset?: number): ActorSummary[];
|
|
157
232
|
forget(key: string): void;
|
|
158
233
|
clear(): void;
|
|
159
234
|
/** Marks an actor as holding valid human clearance until `until`. */
|
package/dist/types.d.ts
CHANGED
|
@@ -178,6 +178,42 @@ export interface ActorSnapshot {
|
|
|
178
178
|
requests: number;
|
|
179
179
|
/** Distinct paths seen inside the window — breadth of crawl. */
|
|
180
180
|
distinctPaths: number;
|
|
181
|
+
/**
|
|
182
|
+
* Distinct path-and-query combinations seen from this actor.
|
|
183
|
+
*
|
|
184
|
+
* Separate from `distinctPaths` because a scraper lives in the gap between them:
|
|
185
|
+
* `/products?page=1` through `?page=200` is one path and two hundred requests.
|
|
186
|
+
*/
|
|
187
|
+
distinctQueries: number;
|
|
188
|
+
/** Whether `distinctQueries` stopped being able to grow. */
|
|
189
|
+
queriesSaturated: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Every HTTP method this actor has used.
|
|
192
|
+
*
|
|
193
|
+
* A browser navigating issues GET; an actor whose whole visit is HEAD is checking what
|
|
194
|
+
* exists rather than reading it.
|
|
195
|
+
*/
|
|
196
|
+
methodsSeen: readonly string[];
|
|
197
|
+
/**
|
|
198
|
+
* Responses reported back for this actor, and how many of them were misses.
|
|
199
|
+
*
|
|
200
|
+
* Both zero unless something calls `recordOutcome` — the engine decides before a
|
|
201
|
+
* response exists, so this is knowledge only the application has.
|
|
202
|
+
*/
|
|
203
|
+
responses: number;
|
|
204
|
+
/** Of `responses`, how many were 404 or 410. */
|
|
205
|
+
misses: number;
|
|
206
|
+
/**
|
|
207
|
+
* The path shape this actor has walked hardest — `/user/#` — with how many requests
|
|
208
|
+
* went to it and how wide a range of numbers they covered.
|
|
209
|
+
*
|
|
210
|
+
* Undefined when no path carried a number. `span` is inclusive, so ids 1 to 30 span 30.
|
|
211
|
+
*/
|
|
212
|
+
walk?: {
|
|
213
|
+
template: string;
|
|
214
|
+
count: number;
|
|
215
|
+
span: number;
|
|
216
|
+
} | undefined;
|
|
181
217
|
/** First and last sighting, ms since epoch. */
|
|
182
218
|
firstSeen: number;
|
|
183
219
|
lastSeen: number;
|
|
@@ -82,7 +82,11 @@ fails closed is an outage with extra steps.
|
|
|
82
82
|
| `rate-anomaly` | `moderate` | Arrivals in a short window. Reports; never concludes |
|
|
83
83
|
| `cadence` | `moderate` | Coefficient of variation of the gaps. Catches the polite scraper pacing itself *under* your rate limit |
|
|
84
84
|
| `crawl-breadth` | `weak` | Distinct paths against total requests: reading a site against enumerating it |
|
|
85
|
+
| `parameter-sweep` | `weak` | Distinct query strings against the paths they sit on. Catches the collection that leaves the path unchanged — `?page=1..200` |
|
|
85
86
|
| `session-integrity` | `moderate` | A "browser" that never carries a cookie |
|
|
87
|
+
| `id-enumeration` | `moderate` | A contiguous run of numeric ids under one path shape — walking `/user/1..n` rather than following links |
|
|
88
|
+
| `probe-volume` | `moderate` | The share of an actor's requests answered 404. Needs `recordOutcome`; the Node adapter wires it up |
|
|
89
|
+
| `transport-coherence` | `moderate` | The HTTP version and the verbs across a visit: a "Chrome" on HTTP/1.0, a visit made only of HEAD |
|
|
86
90
|
| `identity-rotation` | `moderate` | One actor, several User-Agents. **Off by default** — under an IP actor key this fires on every corporate NAT |
|
|
87
91
|
| `browsing-coherence` | `moderate` | The only detector arguing *for* the client. Human-pointing, so it discounts |
|
|
88
92
|
|
|
@@ -211,6 +211,27 @@ new BotHandler({
|
|
|
211
211
|
The library ships **no address data** and refuses to guess any; see
|
|
212
212
|
[design decisions](../design/decisions.md).
|
|
213
213
|
|
|
214
|
+
**Loading a feed.** `fetchAddressList` reads a published list — a reputation feed, a hosting
|
|
215
|
+
provider's own ranges — over the same hardened path the crawler ranges use: HTTPS only, a
|
|
216
|
+
size cap, `#` and `;` comments stripped, JSON `prefixes` documents or one prefix per line,
|
|
217
|
+
and a list that is empty, oversized or contains a block big enough to matter is refused
|
|
218
|
+
**whole** rather than in part.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { fetchAddressList } from "@osqd/bothandlerjs";
|
|
222
|
+
|
|
223
|
+
const prefixes = await fetchAddressList({ id: "denylist", url: "https://example.org/drop.txt" });
|
|
224
|
+
detector.updateRanges("denylist", prefixes);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Two steps, on purpose: fetching is the part that can fail, and installing is the part that
|
|
228
|
+
changes what happens to somebody. Nothing is fetched on a schedule unless you schedule it.
|
|
229
|
+
|
|
230
|
+
And think hard before pointing that at `denylist` rather than `datacenter`. A denylist entry
|
|
231
|
+
does not corroborate anything — it decides, and it blocks people. A feed is somebody else's
|
|
232
|
+
judgement about an address, refreshed on somebody else's schedule, and an address that was a
|
|
233
|
+
bot last month may be a customer's home connection this month.
|
|
234
|
+
|
|
214
235
|
### `trap`
|
|
215
236
|
|
|
216
237
|
**cheap · always · ceiling `certain`**
|
|
@@ -276,6 +297,107 @@ walking a sitemap almost never revisits, so its ratio sits near one.
|
|
|
276
297
|
`weak`, because a *welcome* crawler produces exactly this shape and so does a person on a
|
|
277
298
|
first visit to a documentation site.
|
|
278
299
|
|
|
300
|
+
### `id-enumeration`
|
|
301
|
+
|
|
302
|
+
**cheap · always · ceiling `moderate`**
|
|
303
|
+
|
|
304
|
+
Somebody working through the identifiers rather than following the links.
|
|
305
|
+
|
|
306
|
+
`crawl-breadth` sees this as "many distinct paths" — which is also what it sees when a
|
|
307
|
+
person reads a documentation site, so it stays `weak` and nothing separates the two.
|
|
308
|
+
Measured before this existed: `/user/1` through `/user/120` in order scored 57, a hundred
|
|
309
|
+
and twenty scattered ids scored 57, and ordinary article paths scored 57.
|
|
310
|
+
|
|
311
|
+
What separates them is not *which* ids were asked for but whether they **cover a range**.
|
|
312
|
+
People arrive at ids through links, and links do not densely enumerate an integer interval;
|
|
313
|
+
a harvester does nothing else. Thirty requests reaching from id 1 to id 33 is a walk; thirty
|
|
314
|
+
scattered across a hundred thousand is somebody reading.
|
|
315
|
+
|
|
316
|
+
It costs three numbers per path shape — a count, a lowest and a highest — rather than a
|
|
317
|
+
list of every id seen, which is what makes it affordable for an actor that asks for ten
|
|
318
|
+
thousand of them. The last numeric segment is taken as the identifier, so in
|
|
319
|
+
`/api/v2/orders/42` the version is part of the shape and the order id is the walk. Numbers
|
|
320
|
+
too large to be a counter are ignored: nobody walks epoch seconds.
|
|
321
|
+
|
|
322
|
+
`moderate`, with the bar set high on purpose. Products in one category often carry
|
|
323
|
+
consecutive ids, so somebody browsing a catalogue produces a smaller version of this.
|
|
324
|
+
|
|
325
|
+
### `probe-volume`
|
|
326
|
+
|
|
327
|
+
**cheap · always · ceiling `moderate`**
|
|
328
|
+
|
|
329
|
+
An actor that is looking for something rather than reading anything.
|
|
330
|
+
|
|
331
|
+
The oldest tell there is for a scanner, and the one this library could not see. Every
|
|
332
|
+
verdict here is reached *before* the response exists — that is what lets it shape the
|
|
333
|
+
response, and it is also what hides the status code from it. So the application reports it
|
|
334
|
+
back:
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
const { outcome } = await handler.handle(facts);
|
|
338
|
+
// …your application answers…
|
|
339
|
+
handler.recordOutcome(facts, response.statusCode);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The bundled Node adapter does this for you. Nothing else depends on it: every other
|
|
343
|
+
detector works unchanged if you never call it, and this one is simply absent.
|
|
344
|
+
|
|
345
|
+
Counts **404 and 410 only**. A 403 is usually this library's own doing, and counting it
|
|
346
|
+
would let a rule that challenges an actor manufacture the evidence for having challenged
|
|
347
|
+
it. A 500 is the site's problem and says nothing about the client.
|
|
348
|
+
|
|
349
|
+
`moderate`, because a site that has just moved its URLs produces exactly this shape from
|
|
350
|
+
perfectly ordinary readers, and so does a feed reader working through removed articles.
|
|
351
|
+
Eighty per cent of at least twenty reported responses, by default.
|
|
352
|
+
|
|
353
|
+
### `transport-coherence`
|
|
354
|
+
|
|
355
|
+
**cheap · always · ceiling `moderate`**
|
|
356
|
+
|
|
357
|
+
How a claimed browser *moves*, rather than what it says.
|
|
358
|
+
|
|
359
|
+
The header checks read one request against the client it claims to be. This reads the
|
|
360
|
+
transport underneath and the verbs across a visit — harder to copy, because neither is in
|
|
361
|
+
the part of a request most tooling lets you set.
|
|
362
|
+
|
|
363
|
+
Two things. A claimed browser that negotiated **HTTP/1.0**, which no shipping browser has
|
|
364
|
+
offered in over a decade. And a visit made **entirely of HEAD**: one HEAD is a browser
|
|
365
|
+
checking a link it is about to follow or a cache revalidating, but a whole visit of them is
|
|
366
|
+
something checking what exists without reading any of it.
|
|
367
|
+
|
|
368
|
+
Both were measured as blind spots before this existed — a client claiming Chrome 120 over
|
|
369
|
+
HTTP/1.0, and one whose whole visit was HEAD, each scored exactly what the honest control
|
|
370
|
+
scored.
|
|
371
|
+
|
|
372
|
+
Capped at `moderate`, for different reasons each. HTTP/1.0 is not always the client's
|
|
373
|
+
doing: a few older load balancers speak it to the origin, and behind one of those every
|
|
374
|
+
request looks like this — which is what `transportCoherenceDetector({ legacyHttp: false })`
|
|
375
|
+
is for. An all-HEAD visit is a stronger shape, but a link checker is a real and mostly
|
|
376
|
+
harmless thing to be.
|
|
377
|
+
|
|
378
|
+
### `parameter-sweep`
|
|
379
|
+
|
|
380
|
+
**cheap · always · ceiling `weak`**
|
|
381
|
+
|
|
382
|
+
The collection `crawl-breadth` cannot see.
|
|
383
|
+
|
|
384
|
+
Breadth counts distinct *paths*, and a path carries no query string — so the shape it reads
|
|
385
|
+
as "somebody rereading one page" is also the shape of enumerating a catalogue.
|
|
386
|
+
`/products?page=1` through `?page=200` is one path and two hundred requests. Measured on
|
|
387
|
+
the same two hundred requests expressed both ways: as distinct paths they scored 62 and
|
|
388
|
+
were called `suspected-bot`; as `?page=N` they scored 55 and passed as `unknown`. Paginated
|
|
389
|
+
collection is not an exotic case — it is how catalogues, search results and APIs are
|
|
390
|
+
actually taken.
|
|
391
|
+
|
|
392
|
+
So this counts the other thing: distinct parameterisations, and how many of them stack onto
|
|
393
|
+
a single path. Both halves matter. A high variant count on its own is ordinary — a shop's
|
|
394
|
+
own visitors filter and sort — and it is the *concentration* that separates a person
|
|
395
|
+
changing their mind from a machine walking an index.
|
|
396
|
+
|
|
397
|
+
`weak`, for the same reason as breadth: a person paging through search results produces a
|
|
398
|
+
smaller version of exactly this. Its value is as a second signal beside an actor that has
|
|
399
|
+
already failed something sharper.
|
|
400
|
+
|
|
279
401
|
### `identity-rotation`
|
|
280
402
|
|
|
281
403
|
**cheap · always · ceiling `moderate` · off by default**
|
|
@@ -41,6 +41,26 @@ individual name does not.
|
|
|
41
41
|
| `library` | curl, wget, python-requests, Go-http-client, okhttp | usually challenge |
|
|
42
42
|
| `headless` | HeadlessChrome, Playwright, Puppeteer, Selenium | usually challenge |
|
|
43
43
|
| `embedded` | Smart TVs, set-top boxes, game consoles | allow |
|
|
44
|
+
| `commerce` | idealo, Kelkoo, PriceRunner, Trivago, Skyscanner, Indeedbot | a commercial decision |
|
|
45
|
+
| `academic` | Crossref, OpenAlex, university web-science crawls | usually allow |
|
|
46
|
+
| `accessibility` | Siteimprove and other WCAG auditors | allow |
|
|
47
|
+
| `email-security` | Proofpoint, Mimecast, Barracuda, Cisco Secure Email | allow |
|
|
48
|
+
|
|
49
|
+
`email-security` is a mail or messaging gateway checking a link somebody was sent, before
|
|
50
|
+
they are allowed to click it — and it is separate because of who pays when it is blocked. A
|
|
51
|
+
social preview that fails costs a card; one of these failing tells a real person, in their
|
|
52
|
+
inbox, that their mail contained a link that could not be verified, and they were never the
|
|
53
|
+
one crawling. They arrive with none of a browser's marks — from a datacentre, once, no
|
|
54
|
+
cookie, no referer — because the request is automation acting on a human's behalf.
|
|
55
|
+
|
|
56
|
+
The other three are separate from their nearest neighbours because the decision is. A price
|
|
57
|
+
comparator is not an SEO auditor — nothing there is auditing your site for you, and the
|
|
58
|
+
same crawler is a distribution channel to one retailer and a competitor's research tool to
|
|
59
|
+
the next, which is why nothing in `commerce` is marked benign. A citation index is not a
|
|
60
|
+
model being trained, and an operator refusing the second is often glad of the first. And an
|
|
61
|
+
accessibility crawler is not an uptime probe: it is usually commissioned by the site's own
|
|
62
|
+
owner and then forgotten about, so blocking it does not reduce load, it makes an
|
|
63
|
+
accessibility report look clean by removing the evidence.
|
|
44
64
|
|
|
45
65
|
```ts
|
|
46
66
|
{ id: "no-ai", match: { category: "ai" }, action: "block", reason: "Not for model training." }
|
|
@@ -54,6 +74,7 @@ What, if anything, can check the claim:
|
|
|
54
74
|
type Verification =
|
|
55
75
|
| { kind: "fcrdns"; domains: readonly string[] } // reverse DNS, forward-confirmed
|
|
56
76
|
| { kind: "ip-ranges"; publishedAt?: string } // an address list the operator publishes
|
|
77
|
+
| { kind: "proof"; via: string } // something only you can check
|
|
57
78
|
| { kind: "none" }; // no published mechanism
|
|
58
79
|
```
|
|
59
80
|
|
|
@@ -62,6 +83,29 @@ them, and for those the claim is **unfalsifiable**. The library neither confirms
|
|
|
62
83
|
accuses — it records what the client said and lets the policy decide what a self-declared
|
|
63
84
|
identity is worth. See [verifying a crawler](verification.md).
|
|
64
85
|
|
|
86
|
+
**But you may be able to check what this library cannot.** Your CDN has often already
|
|
87
|
+
verified the crawler and says so in a header it adds; some bots now sign their requests;
|
|
88
|
+
you may hold ASN data. None of those belong inside a detection library — two need a
|
|
89
|
+
network dependency and the third needs a key it has no business fetching — so instead you
|
|
90
|
+
supply the answer:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
new BotHandler({
|
|
94
|
+
crawlerVerification: {
|
|
95
|
+
verifiers: {
|
|
96
|
+
// Whatever you can prove, however you prove it.
|
|
97
|
+
gptbot: (ctx) => (ctx.facts.headers["cf-verified-bot"] === "gptbot" ? "verified" : "unknown"),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
A verifier returns `"verified"`, `"refuted"` or `"unknown"`, runs before the built-in check
|
|
104
|
+
for that signature, and a definite answer settles it — which also means no DNS lookup.
|
|
105
|
+
`"unknown"` falls through to whatever the library can do on its own, and so does a throw:
|
|
106
|
+
an unreachable key server must never read as an accusation, or an outage becomes a wave of
|
|
107
|
+
blocked crawlers.
|
|
108
|
+
|
|
65
109
|
## What a name is worth
|
|
66
110
|
|
|
67
111
|
A matched signature makes `self-identified` produce `certain` evidence, and this is the
|
|
@@ -29,6 +29,12 @@ User-Agent, the verdict, the score, the action and the rule that chose it. Filte
|
|
|
29
29
|
proven, suspected, human, guard stops, denied, mitigated or served; or type into the
|
|
30
30
|
search box.
|
|
31
31
|
|
|
32
|
+
The feed pages, fifty requests at a time. The newest page follows the stream; stepping
|
|
33
|
+
back holds the list still while you read it — a feed that renumbers itself under somebody
|
|
34
|
+
paging through it cannot be read — and says **held while you read** so the stillness is
|
|
35
|
+
not mistaken for quiet. Requests keep arriving and keep being counted; they are at the
|
|
36
|
+
front when you return to it.
|
|
37
|
+
|
|
32
38
|
The search takes fields, and negation, because the two cases people actually reach for
|
|
33
39
|
it are an address that also appears inside a User-Agent and a path that is a prefix of
|
|
34
40
|
ten others:
|
|
@@ -108,6 +114,17 @@ regularity (near zero is a metronome, which no person is), prior confirmations,
|
|
|
108
114
|
whether they hold clearance. **In feed** sends one to the live feed as an `actor:` filter,
|
|
109
115
|
which makes it a shareable URL like every other view.
|
|
110
116
|
|
|
117
|
+
This screen pages too, twenty-five at a time, busiest first. The registry holds far more
|
|
118
|
+
clients than the feed's ring holds requests, and paging is what reaches them: the feed
|
|
119
|
+
already shows you what is loudest, and the population behind it is the reason this screen
|
|
120
|
+
exists. Ranking something that is still moving means a client can shift between pages
|
|
121
|
+
while you read; the order is a snapshot of a live list, not a stable index.
|
|
122
|
+
|
|
123
|
+
The **Actors tracked** counter above the tab strip is the way in: it is a button, so
|
|
124
|
+
pressing it — or reaching it with the keyboard and pressing Enter — opens this screen.
|
|
125
|
+
Where the `registry` section is switched off there is no Actors screen to open, and the
|
|
126
|
+
counter stays an ordinary tile rather than offering to go somewhere that does not exist.
|
|
127
|
+
|
|
111
128
|
**Statistics** — a traffic timeline (1m/5m/15m/1h) split by outcome, which says how
|
|
112
129
|
much history the window actually holds rather than drawing a flat line through time it
|
|
113
130
|
never had; the assessment-latency histogram with mean, p95 and max; and the **score
|
|
@@ -383,6 +400,15 @@ capped at `maxEventsPerSecond` (100 by default, `0` to remove it). What is cappe
|
|
|
383
400
|
still see every request, and the feed says how many were not streamed. A thinned feed
|
|
384
401
|
must never look like a quiet one.
|
|
385
402
|
|
|
403
|
+
**And it does not stop at saying so.** Next to that count is **Load them**, which fetches
|
|
404
|
+
the ring and merges what is missing back into the feed, in the order it happened. Nothing
|
|
405
|
+
was ever lost — the cap and the lag guard both keep entries off the *stream* and leave the
|
|
406
|
+
ring alone — so this is the same data arriving by a different road. It is a button rather
|
|
407
|
+
than something automatic on purpose: entries are skipped exactly when the origin is
|
|
408
|
+
busiest, and a dashboard that answered every skip by re-fetching the whole ring would be a
|
|
409
|
+
load amplifier pointed at the process it is watching, which is what the cap exists to
|
|
410
|
+
prevent.
|
|
411
|
+
|
|
386
412
|
**A viewer that stops reading is not allowed to cost you memory.** A socket that has
|
|
387
413
|
stopped draining — a laptop that slept with the tab open, a phone in a tunnel, a proxy
|
|
388
414
|
that stopped reading — used to accumulate frames in this process, one queue per viewer,
|