@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,17 @@
|
|
|
1
|
+
import type { TrafficCase } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Traffic from the machinery around your application.
|
|
4
|
+
*
|
|
5
|
+
* Load balancer probes, container orchestration health checks, your own mobile app,
|
|
6
|
+
* your own server-side renderer, webhooks arriving from a payment processor, a
|
|
7
|
+
* browser prefetching a page a person has not asked for yet. None of it is a person,
|
|
8
|
+
* almost none of it is unwelcome, and most of it will be classified as automation
|
|
9
|
+
* because that is exactly what it is.
|
|
10
|
+
*
|
|
11
|
+
* The lesson this section is here to teach is that **the answer is usually the
|
|
12
|
+
* allowlist or `ignorePaths`, not a detector**. A health check from your own load
|
|
13
|
+
* balancer should never reach detection at all: it costs work, it inflates every
|
|
14
|
+
* per-actor counter with perfectly regular traffic, and the one thing you must never
|
|
15
|
+
* do is let a bot policy decide whether your orchestrator thinks you are alive.
|
|
16
|
+
*/
|
|
17
|
+
export declare const INFRASTRUCTURE_CASES: TrafficCase[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Published IP ranges the corpus assumes.
|
|
3
|
+
*
|
|
4
|
+
* Several crawlers publish an exhaustive address list instead of setting PTR records,
|
|
5
|
+
* and the library can only confirm or refute those claims when it has been handed the
|
|
6
|
+
* list. These are the fictional stand-ins the corpus uses, drawn entirely from the
|
|
7
|
+
* documentation ranges reserved by RFC 5737 so that nothing here can ever collide
|
|
8
|
+
* with a real network.
|
|
9
|
+
*
|
|
10
|
+
* The convention, which the cases rely on:
|
|
11
|
+
*
|
|
12
|
+
* - **inside** the range → the claim is confirmed → `verified-bot`
|
|
13
|
+
* - **outside** the range → the claim is refuted → `impersonator`
|
|
14
|
+
*
|
|
15
|
+
* Cases pick their address to land on the side they mean to test.
|
|
16
|
+
*/
|
|
17
|
+
export declare const CORPUS_CRAWLER_RANGES: Readonly<Record<string, readonly string[]>>;
|
|
18
|
+
/** An address inside a crawler's published range. */
|
|
19
|
+
export declare const IN_RANGE: Readonly<Record<string, string>>;
|
|
20
|
+
/** Outside every published range above — the address a forgery would come from. */
|
|
21
|
+
export declare const OUT_OF_RANGE = "192.0.2.66";
|
|
22
|
+
/**
|
|
23
|
+
* Real Googlebot address space, used only as a *shape* for the FCrDNS cases.
|
|
24
|
+
*
|
|
25
|
+
* No lookup ever leaves the process: the runner answers from each case's own `dns`
|
|
26
|
+
* map. The address is realistic so that a reader recognises what is being modelled.
|
|
27
|
+
*/
|
|
28
|
+
export declare const GOOGLEBOT_IP = "66.249.66.1";
|
|
29
|
+
export declare const GOOGLEBOT_PTR = "crawl-66-249-66-1.googlebot.com";
|
|
30
|
+
export declare const BINGBOT_IP = "40.77.167.1";
|
|
31
|
+
export declare const BINGBOT_PTR = "msnbot-40-77-167-1.search.msn.com";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TrafficCase } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Address reputation and prior clearance.
|
|
4
|
+
*
|
|
5
|
+
* Two mechanisms that only work when you have configured them, and that behave very
|
|
6
|
+
* differently from everything else in the corpus:
|
|
7
|
+
*
|
|
8
|
+
* **Address ranges** are operator data. The library ships none, because an
|
|
9
|
+
* address-to-operator mapping goes stale within weeks and a stale mapping is a false
|
|
10
|
+
* positive with a long half-life. A denylist entry is `certain` — not because we
|
|
11
|
+
* deduced anything, but because you decided it — while a datacenter match is capped
|
|
12
|
+
* at `moderate` forever, since consumer VPNs, corporate gateways, Tor exits and
|
|
13
|
+
* privacy relays all live in the same address space as the scrapers.
|
|
14
|
+
*
|
|
15
|
+
* **Clearance** is the only source of human-pointing evidence the library has, and
|
|
16
|
+
* the levels are not interchangeable. A solved proof of work shows that a JavaScript
|
|
17
|
+
* engine ran and CPU was spent — a headless Chrome does both. Only `operator`
|
|
18
|
+
* clearance, which your own application issues on evidence the request does not
|
|
19
|
+
* carry, is treated as conclusive.
|
|
20
|
+
*/
|
|
21
|
+
export declare const REPUTATION_CASES: TrafficCase[];
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { BotHandler } from "../core.js";
|
|
2
|
+
import { ManualClock } from "../internal/clock.js";
|
|
3
|
+
import type { Assessment } from "../types.js";
|
|
4
|
+
import type { Decision } from "../policy/types.js";
|
|
5
|
+
import type { ActionOutcome } from "../actions/types.js";
|
|
6
|
+
import type { DnsResolver } from "../internal/dns.js";
|
|
7
|
+
import type { Audience, TrafficCase } from "./schema.js";
|
|
8
|
+
/**
|
|
9
|
+
* The harness.
|
|
10
|
+
*
|
|
11
|
+
* It takes a factory rather than a handler because the corpus tests **your**
|
|
12
|
+
* configuration, not the defaults. Hand it whatever you run in production and it will
|
|
13
|
+
* tell you what that policy does to every shape of traffic in the corpus — including,
|
|
14
|
+
* and this is the part worth reading, which of them are people.
|
|
15
|
+
*
|
|
16
|
+
* Two things it controls that a real deployment does not:
|
|
17
|
+
*
|
|
18
|
+
* - **DNS.** No lookup leaves the process. Each case declares the answers it wants,
|
|
19
|
+
* so the difference between "the operator's DNS disproves this claim" and "our
|
|
20
|
+
* resolver was briefly unhappy" can actually be tested. Those must reach different
|
|
21
|
+
* verdicts, and only a controlled resolver can prove they do.
|
|
22
|
+
* - **Time.** A `ManualClock` advances by each request's `atMs`, so rate and cadence
|
|
23
|
+
* are exercised deterministically. A corpus that slept would take hours and still
|
|
24
|
+
* be flaky.
|
|
25
|
+
*/
|
|
26
|
+
export interface RunnerOptions {
|
|
27
|
+
/** Builds the handler under test. The runner supplies the resolver and the clock. */
|
|
28
|
+
create: (dependencies: {
|
|
29
|
+
resolver: DnsResolver;
|
|
30
|
+
clock: ManualClock;
|
|
31
|
+
}) => BotHandler;
|
|
32
|
+
cases?: readonly TrafficCase[];
|
|
33
|
+
/**
|
|
34
|
+
* Load the corpus's fictional crawler ranges into the handler. Default true.
|
|
35
|
+
*
|
|
36
|
+
* Without them the `ip-ranges` verification cases cannot pass, because the library
|
|
37
|
+
* ships no address data and correctly refuses to guess.
|
|
38
|
+
*/
|
|
39
|
+
applyCorpusRanges?: boolean;
|
|
40
|
+
/** Epoch ms the first request of every case is stamped with. */
|
|
41
|
+
startedAt?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Capability names the handler under test provides, matched against a case's
|
|
44
|
+
* `requires`. A case naming something absent here is skipped and reported.
|
|
45
|
+
*/
|
|
46
|
+
provides?: readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* Assert each case's `expect.action`. Default true.
|
|
49
|
+
*
|
|
50
|
+
* Turn it off when running the corpus across several policies. Which *action* a
|
|
51
|
+
* case receives is a property of the policy — a case that is allowed under one
|
|
52
|
+
* preset is logged under another and both are correct — whereas the verdict, the
|
|
53
|
+
* certainty and `neverAction` are properties of the traffic and hold everywhere.
|
|
54
|
+
* `neverAction` is always asserted, because it is the invariant, not a preference.
|
|
55
|
+
*/
|
|
56
|
+
assertActions?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface RequestResult {
|
|
59
|
+
assessment: Assessment;
|
|
60
|
+
decision: Decision;
|
|
61
|
+
outcome: ActionOutcome;
|
|
62
|
+
}
|
|
63
|
+
export interface CaseResult {
|
|
64
|
+
case: TrafficCase;
|
|
65
|
+
/** Every request in the case, in order. */
|
|
66
|
+
requests: RequestResult[];
|
|
67
|
+
/** The request expectations are asserted against — the last one. */
|
|
68
|
+
final: RequestResult;
|
|
69
|
+
/** Human-readable expectation violations. Empty means the case passed. */
|
|
70
|
+
failures: string[];
|
|
71
|
+
/** Set when a case marked `human` was denied service. The failure that matters most. */
|
|
72
|
+
falsePositive: boolean;
|
|
73
|
+
/** Why the case did not run, when it did not. Never counted as a pass. */
|
|
74
|
+
skipped?: string;
|
|
75
|
+
durationMs: number;
|
|
76
|
+
}
|
|
77
|
+
export interface AudienceTally {
|
|
78
|
+
total: number;
|
|
79
|
+
passed: number;
|
|
80
|
+
failed: number;
|
|
81
|
+
actions: Record<string, number>;
|
|
82
|
+
}
|
|
83
|
+
export interface Scorecard {
|
|
84
|
+
results: CaseResult[];
|
|
85
|
+
total: number;
|
|
86
|
+
passed: number;
|
|
87
|
+
failed: number;
|
|
88
|
+
/** Human cases that were denied service. Must be empty. */
|
|
89
|
+
falsePositives: CaseResult[];
|
|
90
|
+
/** Cases the handler under test was not configured to exercise. */
|
|
91
|
+
skipped: CaseResult[];
|
|
92
|
+
/**
|
|
93
|
+
* People whose client software declares itself automated.
|
|
94
|
+
*
|
|
95
|
+
* Exempt from the never-deny rule by construction, and listed here so that the
|
|
96
|
+
* exemption is visible in every report rather than buried in a flag.
|
|
97
|
+
*/
|
|
98
|
+
selfDeclaredHumans: CaseResult[];
|
|
99
|
+
byAudience: Record<Audience, AudienceTally>;
|
|
100
|
+
byCategory: Record<string, {
|
|
101
|
+
total: number;
|
|
102
|
+
failed: number;
|
|
103
|
+
}>;
|
|
104
|
+
/** How many cases each detector produced evidence on. */
|
|
105
|
+
detectorCoverage: Record<string, number>;
|
|
106
|
+
/**
|
|
107
|
+
* Detectors installed in the handler that no case exercised.
|
|
108
|
+
*
|
|
109
|
+
* A gap in the corpus, not in the library: an untested detector is one whose next
|
|
110
|
+
* regression nobody will notice.
|
|
111
|
+
*/
|
|
112
|
+
unexercisedDetectors: string[];
|
|
113
|
+
/** Proven-verdict rate among cases that are genuinely automated. */
|
|
114
|
+
provenAutomation: {
|
|
115
|
+
total: number;
|
|
116
|
+
proven: number;
|
|
117
|
+
};
|
|
118
|
+
durationMs: number;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Derives a stable address for a case from its id.
|
|
122
|
+
*
|
|
123
|
+
* Distinct per case, so no case inherits another's behavioural history, and stable
|
|
124
|
+
* across runs, so a failure is reproducible. Drawn from 198.18.0.0/15, which RFC 2544
|
|
125
|
+
* reserves for benchmarking — it cannot collide with anything real, and its meaning is
|
|
126
|
+
* exactly what this is.
|
|
127
|
+
*/
|
|
128
|
+
export declare function addressFor(id: string): string;
|
|
129
|
+
export declare function runCase(handler: BotHandler, clock: ManualClock, item: TrafficCase, startedAt: number, provides: ReadonlySet<string>, assertActions?: boolean): Promise<CaseResult>;
|
|
130
|
+
export declare function runCorpus(options: RunnerOptions): Promise<Scorecard>;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { ActionName } from "../policy/types.js";
|
|
2
|
+
import type { BotClass, Verdict } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The corpus schema.
|
|
5
|
+
*
|
|
6
|
+
* This is a body of *realistic web traffic* — real User-Agent strings, real header
|
|
7
|
+
* sets in the order real clients send them, real behavioural shapes — paired with
|
|
8
|
+
* what the library ought to conclude about each. It exists to answer one question
|
|
9
|
+
* that no unit test can:
|
|
10
|
+
*
|
|
11
|
+
* > If I point this configuration at the actual internet, who gets hurt?
|
|
12
|
+
*
|
|
13
|
+
* The corpus is deliberately separate from the configuration it tests. Point the
|
|
14
|
+
* runner at your own `BotHandler` and it tells you how *your* policy behaves against
|
|
15
|
+
* every shape of traffic in here — not how the defaults behave.
|
|
16
|
+
*
|
|
17
|
+
* ## Adding cases
|
|
18
|
+
*
|
|
19
|
+
* Add to the file matching the audience, or create a new file and register it in
|
|
20
|
+
* `index.ts`. Every case needs `provenance`: where the shape came from. A fixture
|
|
21
|
+
* somebody invented is worth much less than one copied from a log, and six months
|
|
22
|
+
* later nobody can tell them apart unless it is written down.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Who is behind the request — and, more to the point, what it costs to be wrong.
|
|
26
|
+
*
|
|
27
|
+
* This is the axis that matters, because the corpus's central assertion is expressed
|
|
28
|
+
* in terms of it: **no case marked `human` may ever be denied service.** That check
|
|
29
|
+
* runs against every case in the corpus regardless of what its own expectations say,
|
|
30
|
+
* so a new human case protects you the moment it is added.
|
|
31
|
+
*/
|
|
32
|
+
export type Audience =
|
|
33
|
+
/** A person. Being wrong here means turning away a customer. */
|
|
34
|
+
"human"
|
|
35
|
+
/** Automation almost every site wants: search crawlers, link unfurlers, uptime monitors. */
|
|
36
|
+
| "benign-bot"
|
|
37
|
+
/** Honest automation whose treatment is a business decision, not a security one. */
|
|
38
|
+
| "declared-bot"
|
|
39
|
+
/** Automation most sites would rather not serve: aggressive SEO, bulk extraction. */
|
|
40
|
+
| "unwanted-bot"
|
|
41
|
+
/** Scanners, forged identities, credential attacks. */
|
|
42
|
+
| "hostile"
|
|
43
|
+
/** Something in the middle of the path: a proxy, a CDN, a corporate gateway. */
|
|
44
|
+
| "infrastructure";
|
|
45
|
+
/** One HTTP request, as a client would actually send it. */
|
|
46
|
+
export interface CaseRequest {
|
|
47
|
+
method?: string;
|
|
48
|
+
/** Path plus query, as it appears on the request line. */
|
|
49
|
+
path?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Headers **in the order the client sends them**.
|
|
52
|
+
*
|
|
53
|
+
* A tuple list rather than an object, because order is itself a fingerprint and an
|
|
54
|
+
* object literal's key order is too easy to disturb by accident. `headers.ts` builds
|
|
55
|
+
* these for the real browsers.
|
|
56
|
+
*/
|
|
57
|
+
headers: ReadonlyArray<readonly [name: string, value: string]>;
|
|
58
|
+
ip?: string;
|
|
59
|
+
protocol?: "http" | "https";
|
|
60
|
+
httpVersion?: string;
|
|
61
|
+
tlsFingerprint?: string;
|
|
62
|
+
/** Milliseconds after the case's start time. Drives the behavioural detectors. */
|
|
63
|
+
atMs?: number;
|
|
64
|
+
/** The source could not supply the full header set. See `RequestFacts.partialHeaders`. */
|
|
65
|
+
partialHeaders?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export type OneOrMany<T> = T | readonly T[];
|
|
68
|
+
/** What the library should conclude. Every field is optional; absent means "don't care". */
|
|
69
|
+
export interface Expectation {
|
|
70
|
+
verdict?: OneOrMany<Verdict>;
|
|
71
|
+
botClass?: OneOrMany<BotClass>;
|
|
72
|
+
/** Whether the conclusion must rest on proven evidence. */
|
|
73
|
+
certain?: boolean;
|
|
74
|
+
/** Established or claimed identity, e.g. `"googlebot"`. */
|
|
75
|
+
identity?: string;
|
|
76
|
+
minScore?: number;
|
|
77
|
+
maxScore?: number;
|
|
78
|
+
/** Detectors that must all have produced evidence. */
|
|
79
|
+
detectors?: readonly string[];
|
|
80
|
+
/** Detectors that must not have fired. The false-positive guard, stated per case. */
|
|
81
|
+
notDetectors?: readonly string[];
|
|
82
|
+
/** Actions that are acceptable. */
|
|
83
|
+
action?: OneOrMany<ActionName>;
|
|
84
|
+
/** Actions that must never be taken. Enforced in addition to the audience rule. */
|
|
85
|
+
neverAction?: readonly ActionName[];
|
|
86
|
+
/**
|
|
87
|
+
* What the action layer actually did, which is not always what the policy decided.
|
|
88
|
+
*
|
|
89
|
+
* A decision to challenge an actor that already holds clearance is refused and the
|
|
90
|
+
* request is served instead — the guard that stops a proven bot from looping on the
|
|
91
|
+
* challenge forever. That distinction is invisible from `action` alone.
|
|
92
|
+
*/
|
|
93
|
+
outcome?: "continue" | "respond" | "drop";
|
|
94
|
+
}
|
|
95
|
+
export interface TrafficCase {
|
|
96
|
+
/** Stable, unique, kebab-case. Appears in every report; treat it as an identifier. */
|
|
97
|
+
id: string;
|
|
98
|
+
title: string;
|
|
99
|
+
audience: Audience;
|
|
100
|
+
/** Finer grouping within an audience, e.g. `"desktop-browser"`, `"ai-crawler"`. */
|
|
101
|
+
category: string;
|
|
102
|
+
/**
|
|
103
|
+
* Where this shape came from: a published User-Agent list, a vendor's documentation,
|
|
104
|
+
* an observed log line, a specification. Required — a fixture nobody can trace is a
|
|
105
|
+
* fixture nobody can update when the world moves.
|
|
106
|
+
*/
|
|
107
|
+
provenance: string;
|
|
108
|
+
/** Anything a reader needs in order to judge whether the expectation is right. */
|
|
109
|
+
notes?: string;
|
|
110
|
+
/** A single request, or an ordered sequence from one actor. */
|
|
111
|
+
requests: readonly CaseRequest[];
|
|
112
|
+
expect: Expectation;
|
|
113
|
+
tags?: readonly string[];
|
|
114
|
+
/**
|
|
115
|
+
* Configuration this case depends on, as free-text capability names.
|
|
116
|
+
*
|
|
117
|
+
* Some traffic can only be classified by a detector that has been *told* something
|
|
118
|
+
* — the trap detector cannot recognise a honeypot form field whose name it was
|
|
119
|
+
* never given. Rather than quietly failing against a default configuration, such a
|
|
120
|
+
* case declares what it needs; the runner skips it and says so when the handler
|
|
121
|
+
* under test does not provide it. A skipped case is reported, never counted as a
|
|
122
|
+
* pass, because "we did not check" and "it worked" must not look the same.
|
|
123
|
+
*/
|
|
124
|
+
requires?: readonly string[];
|
|
125
|
+
/**
|
|
126
|
+
* Mint a clearance token for this actor before the case runs, and attach it to
|
|
127
|
+
* every request.
|
|
128
|
+
*
|
|
129
|
+
* The token has to be signed by the handler under test, so the corpus cannot carry
|
|
130
|
+
* a literal cookie — it declares the level it wants and the runner asks the handler
|
|
131
|
+
* for one. `operator` is the only level the library treats as conclusive proof of a
|
|
132
|
+
* person, because it is an assertion by your application rather than an inference
|
|
133
|
+
* from the request.
|
|
134
|
+
*/
|
|
135
|
+
clearance?: "pow" | "interaction" | "operator";
|
|
136
|
+
/**
|
|
137
|
+
* A person whose *client software* declares itself automated, and why.
|
|
138
|
+
*
|
|
139
|
+
* The never-deny guarantee attached to `human` cases is a promise about guesses:
|
|
140
|
+
* nobody is refused on the strength of an inference. It is not a promise that the
|
|
141
|
+
* library can see through a client that announces itself as a bot. A podcast
|
|
142
|
+
* application uses one User-Agent both to fetch feeds and to open the links a
|
|
143
|
+
* listener taps; when the listener taps one, the library reads the declaration the
|
|
144
|
+
* software made and is correct to.
|
|
145
|
+
*
|
|
146
|
+
* Setting this exempts the case from the audience rule and, crucially, lists it in
|
|
147
|
+
* the scorecard under its own heading. These are the people the design knowingly
|
|
148
|
+
* cannot protect, and the corpus makes them countable rather than invisible.
|
|
149
|
+
*/
|
|
150
|
+
selfDeclared?: string;
|
|
151
|
+
/**
|
|
152
|
+
* DNS the runner should present while this case runs.
|
|
153
|
+
*
|
|
154
|
+
* Forward-confirmed reverse DNS is the only mechanism that can produce a
|
|
155
|
+
* `verified-bot` or prove an `impersonator`, so a corpus that cannot control DNS
|
|
156
|
+
* cannot test the two most consequential verdicts the library reaches. `reverse`
|
|
157
|
+
* maps address to PTR names; `forward` maps name to addresses. A name absent from
|
|
158
|
+
* either map resolves to NXDOMAIN, which is a *definitive* negative answer — the
|
|
159
|
+
* runner never presents a timeout unless a case asks for one, because "no answer"
|
|
160
|
+
* and "the wrong answer" must lead to different verdicts.
|
|
161
|
+
*/
|
|
162
|
+
dns?: {
|
|
163
|
+
reverse?: Readonly<Record<string, readonly string[]>>;
|
|
164
|
+
forward?: Readonly<Record<string, readonly string[]>>;
|
|
165
|
+
/** Make every lookup time out, to exercise the indeterminate path. */
|
|
166
|
+
unavailable?: boolean;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** Terminal actions. A human case reaching any of these is a corpus failure. */
|
|
170
|
+
export declare const DENYING_ACTIONS: readonly ActionName[];
|
|
171
|
+
/**
|
|
172
|
+
* Declares a case involving a person.
|
|
173
|
+
*
|
|
174
|
+
* Adds the never-deny guarantee automatically, so it cannot be forgotten, and defaults
|
|
175
|
+
* the verdict expectation to "not classified as a bot". Override `expect` for the
|
|
176
|
+
* awkward cases — and there are several, because some real people do look automated.
|
|
177
|
+
*/
|
|
178
|
+
export declare function human(input: Omit<TrafficCase, "audience"> & {
|
|
179
|
+
audience?: never;
|
|
180
|
+
}): TrafficCase;
|
|
181
|
+
/** Declares a case involving automation. No implicit guarantees; say what you mean. */
|
|
182
|
+
export declare function bot(input: TrafficCase): TrafficCase;
|
|
183
|
+
/** Repeats a request `count` times at a fixed interval. For rate and cadence shapes. */
|
|
184
|
+
export declare function repeat(template: CaseRequest, count: number, everyMs: number, pathAt?: (index: number) => string): CaseRequest[];
|
|
185
|
+
export declare function humanPaced(template: CaseRequest, paths: readonly string[]): CaseRequest[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { BotHandler } from "./core.js";
|
|
2
|
+
/**
|
|
3
|
+
* Published crawler address ranges.
|
|
4
|
+
*
|
|
5
|
+
* Twelve of the signatures in `known-bots.ts` verify by address rather than by reverse
|
|
6
|
+
* DNS — every AI crawler among them — and until this file existed nothing in the
|
|
7
|
+
* library ever filled those ranges in. `updateCrawlerRanges()` was a method waiting for
|
|
8
|
+
* a caller, and `crawler-verification` correctly declined to confirm anybody, because a
|
|
9
|
+
* claim it cannot check is a claim it must not endorse.
|
|
10
|
+
*
|
|
11
|
+
* Verifying by range rather than by reverse DNS is better in three ways where it is
|
|
12
|
+
* available: it is a lookup instead of a network round trip on the request path, it
|
|
13
|
+
* cannot be broken by somebody else's DNS having a bad afternoon, and it works for the
|
|
14
|
+
* several crawlers that publish ranges and no useful `PTR` record at all.
|
|
15
|
+
*
|
|
16
|
+
* **This is opt-in, and it makes outbound requests.** The library otherwise talks to
|
|
17
|
+
* nothing but your own DNS resolver, and a dependency-free package quietly fetching
|
|
18
|
+
* URLs on a timer is not something to inherit by accident.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const stop = startCrawlerRangeRefresh(botHandler, { intervalMs: 12 * 60 * 60_000 });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
/** One publisher, and where it says its addresses are. */
|
|
25
|
+
export interface PublishedRangeSource {
|
|
26
|
+
/** The signature id these ranges belong to. Must match a `BotSignature.id`. */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Where the list is published. HTTPS only — see {@link fetchCrawlerRanges}. */
|
|
29
|
+
url: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The lists shipped with the library, as pointers rather than as data.
|
|
33
|
+
*
|
|
34
|
+
* The distinction matters. This library ships **no address data** and refuses to guess
|
|
35
|
+
* any, because a range baked into a release is a range that is wrong by the time
|
|
36
|
+
* somebody installs it — and being wrong here means either failing to verify a real
|
|
37
|
+
* crawler or, far worse, verifying somebody who has since been handed the address. What
|
|
38
|
+
* it ships is the URL each operator publishes, so the answer always comes from the
|
|
39
|
+
* party entitled to give it.
|
|
40
|
+
*
|
|
41
|
+
* Nothing here is guaranteed to stay reachable. A publisher that moves its file simply
|
|
42
|
+
* stops being verifiable, which is the same position the library was in before, and the
|
|
43
|
+
* refresh says so rather than failing quietly. Pass your own `sources` to add, replace
|
|
44
|
+
* or pin any of it.
|
|
45
|
+
*/
|
|
46
|
+
export declare const PUBLISHED_CRAWLER_RANGES: readonly PublishedRangeSource[];
|
|
47
|
+
export interface RefreshOptions {
|
|
48
|
+
/** Which lists to fetch. Defaults to {@link PUBLISHED_CRAWLER_RANGES}. */
|
|
49
|
+
sources?: readonly PublishedRangeSource[];
|
|
50
|
+
/** How long any one request may take. Default 10 seconds. */
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
/**
|
|
53
|
+
* The fetcher. Defaults to the global `fetch`.
|
|
54
|
+
*
|
|
55
|
+
* Injectable because a test must not reach the internet, and because some
|
|
56
|
+
* deployments only reach the outside world through a proxy they configure
|
|
57
|
+
* themselves.
|
|
58
|
+
*/
|
|
59
|
+
fetch?: typeof globalThis.fetch;
|
|
60
|
+
/** Attributed on the `range-change` event, like any other runtime change. */
|
|
61
|
+
by?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface RefreshResult {
|
|
64
|
+
updated: Array<{
|
|
65
|
+
id: string;
|
|
66
|
+
prefixes: number;
|
|
67
|
+
}>;
|
|
68
|
+
/** Sources that could not be used, and why. Never throws — see below. */
|
|
69
|
+
failed: Array<{
|
|
70
|
+
id: string;
|
|
71
|
+
reason: string;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Fetches one published list and returns the prefixes in it.
|
|
76
|
+
*
|
|
77
|
+
* Two formats, because those are the two anybody publishes: a JSON document with a
|
|
78
|
+
* `prefixes` array of `{ ipv4Prefix }` / `{ ipv6Prefix }` objects, which is what Google
|
|
79
|
+
* standardised and what the AI crawlers copied; and a plain-text list of one address or
|
|
80
|
+
* CIDR per line, which is what the older monitoring services publish. Anything else is
|
|
81
|
+
* an error rather than a guess.
|
|
82
|
+
*/
|
|
83
|
+
export declare function fetchCrawlerRanges(source: PublishedRangeSource, options?: RefreshOptions): Promise<string[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Fetches every published list and installs it.
|
|
86
|
+
*
|
|
87
|
+
* **Never throws and never partially applies.** Each source is independent: one
|
|
88
|
+
* publisher being down, having moved its file or serving something unrecognisable
|
|
89
|
+
* leaves every other crawler's ranges exactly as they were, and leaves *that* crawler's
|
|
90
|
+
* ranges as they were too. The failure is reported in the result and raised as a
|
|
91
|
+
* warning on the handler, which is where the dashboard's notices panel picks it up.
|
|
92
|
+
*
|
|
93
|
+
* That is the same fail-open rule the rest of the library follows: an outage in
|
|
94
|
+
* something the detection consults must degrade detection, never take down the site it
|
|
95
|
+
* protects — and here "degrade" means a crawler goes back to being unverifiable, which
|
|
96
|
+
* is exactly the state it was in before this was ever called.
|
|
97
|
+
*/
|
|
98
|
+
export declare function refreshCrawlerRanges(handler: BotHandler, options?: RefreshOptions): Promise<RefreshResult>;
|
|
99
|
+
export interface ScheduleOptions extends RefreshOptions {
|
|
100
|
+
/** How often to refresh. Default 12 hours; the minimum is one hour. */
|
|
101
|
+
intervalMs?: number;
|
|
102
|
+
/** Fetch immediately as well as on the interval. Default true. */
|
|
103
|
+
immediate?: boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Refreshes on a schedule, and returns the way to stop.
|
|
107
|
+
*
|
|
108
|
+
* Twice a day by default, because these lists change on the order of weeks and a
|
|
109
|
+
* library that polls somebody else's endpoint more often than that is being rude with
|
|
110
|
+
* your egress and their bandwidth. The timer is unreferenced, so it never keeps a
|
|
111
|
+
* process alive on its own.
|
|
112
|
+
*/
|
|
113
|
+
export declare function startCrawlerRangeRefresh(handler: BotHandler, options?: ScheduleOptions): () => void;
|