@osqd/bothandlerjs 0.5.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.
@@ -61,6 +61,14 @@ export interface CaseRequest {
61
61
  tlsFingerprint?: string;
62
62
  /** Milliseconds after the case's start time. Drives the behavioural detectors. */
63
63
  atMs?: number;
64
+ /**
65
+ * What the application answered, if the case is about that.
66
+ *
67
+ * The engine decides before a response exists, so this is reported back afterwards the
68
+ * way an adapter reports it. Only cases about response shape need it — a scan that is
69
+ * almost all misses being the one that matters.
70
+ */
71
+ status?: number;
64
72
  /** The source could not supply the full header set. See `RequestFacts.partialHeaders`. */
65
73
  partialHeaders?: boolean;
66
74
  }
@@ -81,6 +81,37 @@ export interface RefreshResult {
81
81
  * an error rather than a guess.
82
82
  */
83
83
  export declare function fetchCrawlerRanges(source: PublishedRangeSource, options?: RefreshOptions): Promise<string[]>;
84
+ /** Where a reputation or hosting-provider address list is published. */
85
+ export interface AddressListSource {
86
+ /** The range set to install it as. `denylist` blocks; `datacenter` corroborates. */
87
+ id: "denylist" | "datacenter" | "allowlist";
88
+ url: string;
89
+ }
90
+ export interface AddressListOptions extends RefreshOptions {
91
+ /** Entries to accept before refusing the list. Default 100,000. */
92
+ maxPrefixes?: number;
93
+ }
94
+ /**
95
+ * Fetches a published address list — a reputation feed, a hosting provider's own ranges.
96
+ *
97
+ * The same hardened path as {@link fetchCrawlerRanges}: HTTPS only, a size cap, comments
98
+ * stripped, JSON `prefixes` documents or one prefix per line, and a list that is empty,
99
+ * oversized or contains a block big enough to matter is refused **whole** rather than in
100
+ * part. It is a separate entry point because the limits differ — a reputation feed is
101
+ * thousands of entries where a crawler's is hundreds, and calling one a crawler's list in
102
+ * an error message helps nobody.
103
+ *
104
+ * It does not install anything. Hand the result to `updateRanges("denylist", …)` when you
105
+ * are ready, which is the same two-step the crawler path takes and for the same reason:
106
+ * fetching is the part that can fail, and installing is the part that changes behaviour.
107
+ *
108
+ * **A denylist entry is `certain`.** It does not corroborate anything — it decides, and it
109
+ * blocks people. A feed is somebody else's judgement about an address, refreshed on
110
+ * somebody else's schedule, and an address that was a bot last month may be a customer's
111
+ * home connection this month. Read what you are subscribing to, and prefer `datacenter`
112
+ * for anything you have not decided about yourself.
113
+ */
114
+ export declare function fetchAddressList(source: AddressListSource, options?: AddressListOptions): Promise<string[]>;
84
115
  /**
85
116
  * Fetches every published list and installs it.
86
117
  *
@@ -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;
@@ -18,7 +18,15 @@ export type { RateAnomalyOptions } from "./rate-anomaly.js";
18
18
  export { cadenceDetector } from "./cadence.js";
19
19
  export type { CadenceOptions } from "./cadence.js";
20
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";
21
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";
22
30
  export { sessionIntegrityDetector } from "./session-integrity.js";
23
31
  export type { SessionIntegrityOptions } from "./session-integrity.js";
24
32
  export { identityRotationDetector } from "./identity-rotation.js";
@@ -35,6 +35,17 @@ export type BotCategory = "search" | "ai" | "seo" | "social" | "monitoring" | "a
35
35
  * to reach the page, and frequently does not know it is arriving as a bot at all.
36
36
  */
37
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"
38
49
  /**
39
50
  * Research and measurement: universities, internet-measurement projects, plagiarism
40
51
  * and citation indexes.
@@ -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;
@@ -3878,7 +3878,10 @@ button[disabled] { opacity: .5; cursor: default; }
3878
3878
  .tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--s1); }
3879
3879
 
3880
3880
  /* --- layout ------------------------------------------------------------- */
3881
- main { padding: 18px 20px 64px; max-width: 1680px; margin: 0 auto; }
3881
+ /* The top padding is the gap under the sticky header. At 18px the counter row sat almost
3882
+ against the header's border and read as part of it; the tiles carry their own border, so
3883
+ two lines were meeting with nothing between them. */
3884
+ main { padding: 28px 20px 64px; max-width: 1680px; margin: 0 auto; }
3882
3885
  .stack { display: grid; gap: 16px; }
3883
3886
  /* Everything above the feed is drawn by script once the first snapshot arrives, which
3884
3887
  inserts a block of content above what is already laid out. The browser's scroll