@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.
Files changed (44) hide show
  1. package/dist/adapters/index.cjs +3 -0
  2. package/dist/adapters/index.cjs.map +1 -1
  3. package/dist/adapters/index.js +3 -0
  4. package/dist/adapters/index.js.map +1 -1
  5. package/dist/cli.cjs +634 -25
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +634 -25
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config.d.ts +10 -0
  10. package/dist/core.d.ts +15 -0
  11. package/dist/corpus/index.cjs +84 -0
  12. package/dist/corpus/index.cjs.map +1 -1
  13. package/dist/corpus/index.js +84 -0
  14. package/dist/corpus/index.js.map +1 -1
  15. package/dist/corpus/schema.d.ts +8 -0
  16. package/dist/crawler-ranges.d.ts +31 -0
  17. package/dist/dashboard/client/feed.d.ts +16 -0
  18. package/dist/dashboard/client/format.d.ts +18 -0
  19. package/dist/dashboard/client/pager.d.ts +32 -0
  20. package/dist/dashboard/client/store.d.ts +60 -0
  21. package/dist/dashboard/client.generated.d.ts +1 -1
  22. package/dist/detectors/crawler-verification.d.ts +34 -1
  23. package/dist/detectors/id-enumeration.d.ts +31 -0
  24. package/dist/detectors/index.d.ts +12 -1
  25. package/dist/detectors/known-bots.d.ts +60 -1
  26. package/dist/detectors/parameter-sweep.d.ts +39 -0
  27. package/dist/detectors/probe-volume.d.ts +26 -0
  28. package/dist/detectors/transport-coherence.d.ts +31 -0
  29. package/dist/element/index.cjs +349 -40
  30. package/dist/element/index.cjs.map +1 -1
  31. package/dist/element/index.js +349 -40
  32. package/dist/element/index.js.map +1 -1
  33. package/dist/index.cjs +576 -31
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +571 -31
  37. package/dist/index.js.map +1 -1
  38. package/dist/state.d.ts +76 -1
  39. package/dist/types.d.ts +36 -0
  40. package/docs/course/05-detectors.md +4 -0
  41. package/docs/detection/detectors.md +122 -0
  42. package/docs/detection/signatures.md +44 -0
  43. package/docs/operations/dashboard.md +26 -0
  44. package/package.json +1 -1
@@ -1,5 +1,38 @@
1
- import type { Detector } from "./types.js";
1
+ import type { Detector, DetectionContext } from "./types.js";
2
+ import type { BotSignature } from "./known-bots.js";
3
+ /**
4
+ * What an operator's own check concluded about a claimed identity.
5
+ *
6
+ * Three answers, and the third is not a formality. "I could not tell" has to be
7
+ * expressible and has to mean *silence* — a verifier that returned false for both "this
8
+ * is a forgery" and "my key server timed out" would turn an outage into an accusation.
9
+ */
10
+ export type VerificationOutcome = "verified" | "refuted" | "unknown";
11
+ /**
12
+ * Your own answer to "is this really who it says it is".
13
+ *
14
+ * Called with the same context the built-in checks get, for one claimed signature. It
15
+ * may be async: the natural implementations are a lookup or a signature check.
16
+ */
17
+ export type CrawlerVerifier = (ctx: DetectionContext, signature: BotSignature) => VerificationOutcome | Promise<VerificationOutcome>;
2
18
  export interface CrawlerVerificationOptions {
19
+ /**
20
+ * Verifiers of your own, by signature id — `{ googlebot: ..., gptbot: ... }`.
21
+ *
22
+ * Most of this database cannot be checked from inside a request: the operator
23
+ * publishes no DNS proof and no range list, and the claim is simply unfalsifiable.
24
+ * That is most bots, and until now it meant the library had nothing to offer an
25
+ * operator who *could* check — because their CDN had already verified the crawler and
26
+ * said so in a header, because the bot signs its requests, or because they hold the
27
+ * ASN data. Writing a whole detector to say so meant reimplementing the confirm and
28
+ * refute semantics in this file, including the part where an inconclusive answer must
29
+ * stay silent.
30
+ *
31
+ * A verifier here runs before the built-in check for that signature and a definite
32
+ * answer settles it, which also means no DNS lookup. `unknown` falls through to
33
+ * whatever this library can do on its own.
34
+ */
35
+ verifiers?: Readonly<Record<string, CrawlerVerifier>>;
3
36
  /**
4
37
  * Treat an address with no PTR record as a forged claim. Default true.
5
38
  *
@@ -0,0 +1,31 @@
1
+ import type { Detector } from "./types.js";
2
+ export interface IdEnumerationOptions {
3
+ /** Requests to one path shape before a walk is worth reporting. Default 30. */
4
+ minRequests?: number;
5
+ /**
6
+ * How completely those requests must cover the range they span. Default 0.9 — thirty
7
+ * requests reaching from id 1 to id 33 report; the same thirty scattered across a
8
+ * thousand ids do not.
9
+ */
10
+ density?: number;
11
+ }
12
+ /**
13
+ * Somebody working through the identifiers rather than following the links.
14
+ *
15
+ * `crawl-breadth` sees this as "many distinct paths", which is what it also sees when a
16
+ * person reads a documentation site — so it stays `weak` and nothing separates the two.
17
+ * Measured: `/user/1` through `/user/120` in order scored exactly the same as a hundred
18
+ * and twenty scattered ids, and the same again as ordinary article paths. All three
19
+ * `unknown`, all three 57.
20
+ *
21
+ * What separates them is not which ids were asked for but whether they *cover a range*.
22
+ * People arrive at ids through links, and links do not densely enumerate an integer
23
+ * interval; a harvester does nothing else. Thirty requests reaching from id 1 to id 33 is
24
+ * a walk. Thirty scattered across a hundred thousand is somebody reading.
25
+ *
26
+ * `moderate`, and the bar is set high on purpose. The awkward case is real: products in
27
+ * one category often carry consecutive ids, so somebody browsing a catalogue can produce a
28
+ * smaller version of this. Thirty requests covering ninety per cent of their own span is
29
+ * meant to be past what that produces, and it is still a shape rather than a motive.
30
+ */
31
+ export declare function idEnumerationDetector(options?: IdEnumerationOptions): Detector;
@@ -1,3 +1,4 @@
1
+ import type { CrawlerVerificationOptions } from "./crawler-verification.js";
1
2
  import type { Detector } from "./types.js";
2
3
  export type { DetectionContext, Detector, DetectorResult } from "./types.js";
3
4
  export { evidence, absenceIsMeaningful } from "./types.js";
@@ -17,7 +18,15 @@ export type { RateAnomalyOptions } from "./rate-anomaly.js";
17
18
  export { cadenceDetector } from "./cadence.js";
18
19
  export type { CadenceOptions } from "./cadence.js";
19
20
  export { crawlBreadthDetector } from "./crawl-breadth.js";
21
+ export { parameterSweepDetector } from "./parameter-sweep.js";
22
+ export { transportCoherenceDetector } from "./transport-coherence.js";
23
+ export { probeVolumeDetector } from "./probe-volume.js";
24
+ export { idEnumerationDetector } from "./id-enumeration.js";
20
25
  export type { CrawlBreadthOptions } from "./crawl-breadth.js";
26
+ export type { ParameterSweepOptions } from "./parameter-sweep.js";
27
+ export type { TransportCoherenceOptions } from "./transport-coherence.js";
28
+ export type { ProbeVolumeOptions } from "./probe-volume.js";
29
+ export type { IdEnumerationOptions } from "./id-enumeration.js";
21
30
  export { sessionIntegrityDetector } from "./session-integrity.js";
22
31
  export type { SessionIntegrityOptions } from "./session-integrity.js";
23
32
  export { identityRotationDetector } from "./identity-rotation.js";
@@ -53,4 +62,6 @@ export type { BotSignature, BotCategory, Verification } from "./known-bots.js";
53
62
  * `clearanceDetector` is not here either, because it needs the challenge service —
54
63
  * the engine adds it automatically once `challenge.secrets` is configured.
55
64
  */
56
- export declare function defaultDetectors(): Detector[];
65
+ export declare function defaultDetectors(options?: {
66
+ crawlerVerification?: CrawlerVerificationOptions;
67
+ }): Detector[];
@@ -16,7 +16,46 @@ import { MultiPatternMatcher } from "../internal/matcher.js";
16
16
  */
17
17
  export type BotCategory = "search" | "ai" | "seo" | "social" | "monitoring" | "archive" | "feed" | "security" | "advertising" | "library" | "headless"
18
18
  /** A real browser engine embedded in a desktop application, with a person driving it. */
19
- | "embedded" | "other";
19
+ | "embedded"
20
+ /**
21
+ * Price, stock and catalogue collection: comparison shopping, marketplace feeds,
22
+ * repricing tools.
23
+ *
24
+ * Its own category because it is the one kind of crawling a shop has a commercial
25
+ * opinion about rather than a technical one. It is not `seo` — nothing here is
26
+ * auditing your site for you — and it is not `scraper`, which is a *behavioural*
27
+ * verdict this library reaches on its own. This is a client that says what it is.
28
+ */
29
+ | "commerce"
30
+ /**
31
+ * Accessibility auditing: contrast, landmarks, ARIA, WCAG conformance.
32
+ *
33
+ * Separated from `monitoring` because the answer is almost always different. A site
34
+ * owner who blocks uptime probes still wants the tool their accessibility team runs
35
+ * to reach the page, and frequently does not know it is arriving as a bot at all.
36
+ */
37
+ | "accessibility"
38
+ /**
39
+ * A mail or messaging gateway checking a link on somebody's behalf.
40
+ *
41
+ * Its own category because of who pays when it is blocked. A social preview that fails
42
+ * costs a card; one of these failing tells a real person, in their inbox, that the link
43
+ * they were sent could not be verified — and they were never the one crawling. They also
44
+ * arrive with none of a browser's marks: from a datacentre, once, with no cookie and no
45
+ * referer, moments after a message was delivered, which is a shape that reads as
46
+ * automation because it *is* automation, acting for a human.
47
+ */
48
+ | "email-security"
49
+ /**
50
+ * Research and measurement: universities, internet-measurement projects, plagiarism
51
+ * and citation indexes.
52
+ *
53
+ * Distinct from `ai` on purpose. Both read the whole page and neither sends a person,
54
+ * but the decision differs: an operator refusing to feed a commercial model may be
55
+ * perfectly happy to appear in a citation index, and folding the two together forces
56
+ * one answer onto two questions.
57
+ */
58
+ | "academic" | "other";
20
59
  /** Every category, for anything that has to enumerate them — a rule editor, a report. */
21
60
  export declare const BOT_CATEGORIES: readonly BotCategory[];
22
61
  /**
@@ -40,6 +79,26 @@ export type Verification = {
40
79
  } | {
41
80
  kind: "ip-ranges";
42
81
  publishedAt?: string;
82
+ }
83
+ /**
84
+ * The operator publishes a proof this library cannot check by itself, and you can.
85
+ *
86
+ * A signed request under [Web Bot Auth](https://www.rfc-editor.org/rfc/rfc9421), a
87
+ * CDN that has already verified the crawler and says so in a header it adds, an ASN
88
+ * lookup against data you hold — all of them are conclusive, and none of them are
89
+ * something a detection library should be doing on its own: two need a network
90
+ * dependency and the third needs a key it has no business fetching.
91
+ *
92
+ * So the claim is marked verifiable-by-you, and stays *unverified* until you supply a
93
+ * verifier for it in `crawlerVerification.verifiers`. Marked and unsupplied behaves
94
+ * exactly like `none`: neither confirmed nor accused.
95
+ *
96
+ * `via` names the mechanism, for the operator reading this table to know what they
97
+ * would have to write.
98
+ */
99
+ | {
100
+ kind: "proof";
101
+ via: string;
43
102
  } | {
44
103
  kind: "none";
45
104
  };
@@ -0,0 +1,39 @@
1
+ import type { Detector } from "./types.js";
2
+ export interface ParameterSweepOptions {
3
+ /**
4
+ * Distinct path-and-query combinations at or above which a sweep is worth reporting.
5
+ * Default 25. Cannot exceed {@link MAX_TRACKED_QUERIES}, where the count saturates;
6
+ * asking for more throws rather than never firing.
7
+ */
8
+ threshold?: number;
9
+ /**
10
+ * How many variants must sit on one path before this is a sweep rather than browsing.
11
+ * Default 8 — that is, twenty-five variants across three paths reports, and
12
+ * twenty-five variants across twenty paths does not.
13
+ */
14
+ variantsPerPath?: number;
15
+ /** Minimum requests before the shape means anything. Default 20. */
16
+ minRequests?: number;
17
+ }
18
+ /**
19
+ * The scraping that `crawl-breadth` cannot see.
20
+ *
21
+ * Breadth counts distinct *paths*, and a path has no query string on it. So the shape
22
+ * it reads as "somebody rereading one page" is also the shape of enumerating a
23
+ * catalogue: `/products?page=1` through `?page=200` is one path and two hundred
24
+ * requests. Measured, on the same two hundred requests expressed both ways — as
25
+ * distinct paths it scored 62 and was called `suspected-bot`; as `?page=N` it scored 55
26
+ * and passed as `unknown`. Paginated collection is not an exotic case, it is how
27
+ * catalogues, search results and APIs are actually taken.
28
+ *
29
+ * So this counts the other thing: distinct parameterisations, and how many of them
30
+ * stack onto a single path. Both halves are needed. A high variant count alone is
31
+ * ordinary — a shop's own visitors filter and sort — and it is the *concentration* that
32
+ * separates a person changing their mind from a machine walking an index.
33
+ *
34
+ * `weak`, and deliberately. A person paging through search results produces a smaller
35
+ * version of exactly this, and someone with a slow connection retrying looks similar
36
+ * again. It is a shape, not a motive; its value is as a second signal beside an actor
37
+ * that has already failed something sharper.
38
+ */
39
+ export declare function parameterSweepDetector(options?: ParameterSweepOptions): Detector;
@@ -0,0 +1,26 @@
1
+ import type { Detector } from "./types.js";
2
+ export interface ProbeVolumeOptions {
3
+ /** Reported responses before the ratio means anything. Default 20. */
4
+ minResponses?: number;
5
+ /** Share of them that must be misses. Default 0.8. */
6
+ missRatio?: number;
7
+ }
8
+ /**
9
+ * An actor that is looking for something rather than reading anything.
10
+ *
11
+ * The oldest tell there is for a scanner, and the one this library could not see: it
12
+ * decides *before* the response exists, which is what lets it shape the response and also
13
+ * what hides the status code from it. Fed back through `recordOutcome`, the shape is
14
+ * unmistakable — a person browsing a site does not generate forty misses in a row, and a
15
+ * wordlist does almost nothing else.
16
+ *
17
+ * Counts 404 and 410 only. A 403 is usually this library's own doing, and counting it
18
+ * would let a rule that challenges an actor manufacture the evidence for challenging it.
19
+ * A 500 is the site's problem and says nothing about the client.
20
+ *
21
+ * `moderate`, not higher. A site that has just moved its URLs produces this from perfectly
22
+ * ordinary readers, and so does a feed reader working through a list of removed articles.
23
+ * It is also entirely absent unless the application reports outcomes, which is why nothing
24
+ * else depends on it.
25
+ */
26
+ export declare function probeVolumeDetector(options?: ProbeVolumeOptions): Detector;
@@ -0,0 +1,31 @@
1
+ import type { Detector } from "./types.js";
2
+ export interface TransportCoherenceOptions {
3
+ /**
4
+ * Report a claimed browser arriving over HTTP/1.0. Default true.
5
+ *
6
+ * Turn it off if something in front of this application speaks HTTP/1.0 to it. A few
7
+ * older load balancers and reverse proxies still do, and where that is true every
8
+ * request arrives that way — so the signal says something about your infrastructure
9
+ * rather than about your visitors, and a detector that fires on all of them is worse
10
+ * than one that fires on none.
11
+ */
12
+ legacyHttp?: boolean;
13
+ /** Requests an actor must have made before an all-HEAD visit means anything. Default 8. */
14
+ minHeadRequests?: number;
15
+ }
16
+ /**
17
+ * How a claimed browser *moves*, rather than what it says.
18
+ *
19
+ * The header checks read one request against the client it claims to be. This reads the
20
+ * transport underneath and the verbs across a visit, which are harder to copy because
21
+ * they are not in the part of the request most tools let you set.
22
+ *
23
+ * Two things, both measured as blind spots before this existed — a client claiming
24
+ * Chrome 120 over HTTP/1.0, and one whose entire visit is HEAD, each scored exactly what
25
+ * the honest control scored.
26
+ *
27
+ * Neither goes above `moderate`, and the reasons are different. HTTP/1.0 can be an
28
+ * intermediary's doing rather than the client's. An all-HEAD visit is a strong shape but a
29
+ * link checker is a real and mostly harmless thing to be.
30
+ */
31
+ export declare function transportCoherenceDetector(options?: TransportCoherenceOptions): Detector;