@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,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choosing which language to challenge somebody in.
|
|
3
|
+
*
|
|
4
|
+
* The interstitial is the only page this library shows to a member of the public, and
|
|
5
|
+
* it is shown to them because a *probabilistic* verdict went against them. Serving
|
|
6
|
+
* "Checking your browser" in English to somebody whose browser has been asking for
|
|
7
|
+
* Japanese since the first request is the same unfairness the guard exists to prevent,
|
|
8
|
+
* applied to the one screen where it is most visible: a person who cannot read the page
|
|
9
|
+
* cannot find the contact link on it either.
|
|
10
|
+
*
|
|
11
|
+
* The library ships no translations and will not. A machine-translated apology on a
|
|
12
|
+
* page that just turned somebody away is worse than an honest English one — and only
|
|
13
|
+
* the operator knows which languages their audience actually reads. What this does is
|
|
14
|
+
* pick between the translations *you* supply.
|
|
15
|
+
*
|
|
16
|
+
* Pure and header-shaped, so it can be tested by calling it.
|
|
17
|
+
*/
|
|
18
|
+
/** One language's copy. Anything omitted falls back to the default text. */
|
|
19
|
+
export interface ChallengeCopy {
|
|
20
|
+
title?: string;
|
|
21
|
+
message?: string;
|
|
22
|
+
contactHtml?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The `lang` attribute to put on the document. Defaults to the key this copy is filed
|
|
25
|
+
* under.
|
|
26
|
+
*
|
|
27
|
+
* It matters more than it looks: a screen reader picks its voice and its pronunciation
|
|
28
|
+
* rules from this attribute, so Japanese text announced as `lang="en"` is read aloud
|
|
29
|
+
* by an English voice and is unintelligible. Getting the copy right and the attribute
|
|
30
|
+
* wrong helps nobody.
|
|
31
|
+
*/
|
|
32
|
+
lang?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The language tags in an `Accept-Language` header, best first.
|
|
36
|
+
*
|
|
37
|
+
* Lower-cased, `q` honoured, malformed entries dropped rather than fatal — this is a
|
|
38
|
+
* client-supplied header and the page it decides is one somebody is already having a
|
|
39
|
+
* bad time with. `*` is dropped too: it means "anything", which is what the default is
|
|
40
|
+
* for.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseAcceptLanguage(header: string | undefined): string[];
|
|
43
|
+
/**
|
|
44
|
+
* The best available translation for what the client asked for.
|
|
45
|
+
*
|
|
46
|
+
* Exact tag first, then the primary subtag: somebody asking for `pt-BR` gets Brazilian
|
|
47
|
+
* Portuguese if you supply it and European Portuguese if that is all you have, which is
|
|
48
|
+
* far better than English. Somebody asking for `pt` never gets `pt-BR` silently,
|
|
49
|
+
* because a regional variant is a claim about an audience rather than a fallback for
|
|
50
|
+
* one.
|
|
51
|
+
*
|
|
52
|
+
* `undefined` means none matched, and the caller uses its defaults.
|
|
53
|
+
*/
|
|
54
|
+
export declare function pickTranslation(translations: Record<string, ChallengeCopy> | undefined, accepted: readonly string[]): {
|
|
55
|
+
tag: string;
|
|
56
|
+
copy: ChallengeCopy;
|
|
57
|
+
} | undefined;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface ChallengePageOptions {
|
|
2
|
+
/** Signed challenge blob the client must solve and return. */
|
|
3
|
+
challenge: string;
|
|
4
|
+
/** Leading zero bits required. */
|
|
5
|
+
difficulty: number;
|
|
6
|
+
/** Where the solution is POSTed. */
|
|
7
|
+
verifyPath: string;
|
|
8
|
+
/** Shown as the page heading. Default "Checking your browser". */
|
|
9
|
+
title?: string;
|
|
10
|
+
/** Shown under the heading. Keep it short and non-accusatory. */
|
|
11
|
+
message?: string;
|
|
12
|
+
/** Where a person who cannot complete the check should turn. Strongly recommended. */
|
|
13
|
+
contactHtml?: string;
|
|
14
|
+
/** Page language for the `lang` attribute. Default "en". */
|
|
15
|
+
lang?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RenderedChallenge {
|
|
18
|
+
html: string;
|
|
19
|
+
/**
|
|
20
|
+
* Nonce for the inline script. Put it in your CSP as `script-src 'nonce-…'`.
|
|
21
|
+
* The action layer does this for you; it is exposed for hand-rolled responses.
|
|
22
|
+
*/
|
|
23
|
+
scriptNonce: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Renders the interstitial that carries the proof-of-work challenge.
|
|
27
|
+
*
|
|
28
|
+
* Design constraints, in the order they mattered:
|
|
29
|
+
*
|
|
30
|
+
* 1. **No external resources.** No CDN, no font, no image, no analytics. A page shown
|
|
31
|
+
* to a client under suspicion must not become a way to make that client fetch
|
|
32
|
+
* something else, and it has to work when the rest of your site is being shielded.
|
|
33
|
+
* 2. **Accessible.** People using screen readers hit these pages, and a check they
|
|
34
|
+
* cannot perceive is indistinguishable from a broken site. The status is a live
|
|
35
|
+
* region, focus is managed, and the failure state gives a real way to get help.
|
|
36
|
+
* 3. **Honest with the visitor.** No fake progress bar, no "verifying you are human"
|
|
37
|
+
* when what is actually being verified is that a JavaScript engine is present.
|
|
38
|
+
* 4. **A visible way out.** `contactHtml` is not decoration. Anyone on a browser
|
|
39
|
+
* without WebCrypto, with JavaScript disabled, or on a device too slow to finish
|
|
40
|
+
* is a person your site has just locked out; they need somewhere to go.
|
|
41
|
+
*/
|
|
42
|
+
export declare function renderChallengePage(options: ChallengePageOptions): RenderedChallenge;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof of work.
|
|
3
|
+
*
|
|
4
|
+
* The client must find a counter such that `SHA-256(nonce + ":" + counter)` begins
|
|
5
|
+
* with `difficulty` zero bits. Verification is one hash; solving takes on average
|
|
6
|
+
* `2^difficulty` of them.
|
|
7
|
+
*
|
|
8
|
+
* **What this actually buys, stated plainly.** It does not identify anyone and it
|
|
9
|
+
* does not prove a human is present — a headless Chrome solves it as readily as a
|
|
10
|
+
* person's phone, just paying for the CPU. What it does is convert a scrape from
|
|
11
|
+
* free into merely cheap, and change the shape of the attack: a stateless scraper
|
|
12
|
+
* pulling a million pages must now run a JavaScript engine and burn CPU on every
|
|
13
|
+
* single one. That is often enough to make bulk extraction not worth doing, and it
|
|
14
|
+
* costs a real visitor a fraction of a second, once.
|
|
15
|
+
*
|
|
16
|
+
* Difficulty is in *bits*, so each step doubles the work. The default of 16 is around
|
|
17
|
+
* 65k hashes — tens of milliseconds in any modern browser. Past about 20 you are
|
|
18
|
+
* charging real people a visible delay, and the oldest and slowest devices — which
|
|
19
|
+
* disproportionately belong to the users least able to replace them — pay the most.
|
|
20
|
+
*/
|
|
21
|
+
/** Default difficulty in leading zero bits. ~65k hashes; a few tens of ms in a browser. */
|
|
22
|
+
export declare const DEFAULT_DIFFICULTY = 16;
|
|
23
|
+
/** Refuse to issue beyond this. Above it the wait becomes a usability problem for real visitors. */
|
|
24
|
+
export declare const MAX_DIFFICULTY = 24;
|
|
25
|
+
export declare function countLeadingZeroBits(digest: Uint8Array): number;
|
|
26
|
+
/** True when `solution` satisfies the challenge. One hash — cheap enough for the request path. */
|
|
27
|
+
export declare function verifyProofOfWork(nonce: string, solution: string, difficulty: number): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Solves a challenge. Present for tests, examples and load simulation — the real
|
|
30
|
+
* solver is the browser-side script, and no server-side path calls this.
|
|
31
|
+
*/
|
|
32
|
+
export declare function solveProofOfWork(nonce: string, difficulty: number, maxIterations?: number): string | undefined;
|
|
33
|
+
export declare function clampDifficulty(difficulty: number): number;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed, stateless tokens.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is **signed and not encrypted**, and that is a deliberate,
|
|
5
|
+
* documented choice rather than an omission. The client can read every claim, so
|
|
6
|
+
* nothing secret may ever go in one — no user id, no email, no internal path. What
|
|
7
|
+
* the signature buys is integrity: the client cannot change the expiry, cannot move
|
|
8
|
+
* a token to a different actor, and cannot mint one.
|
|
9
|
+
*
|
|
10
|
+
* Key rotation is built in. `secrets[0]` signs; every entry verifies. To rotate,
|
|
11
|
+
* prepend the new secret and keep the old one for at least one token lifetime, then
|
|
12
|
+
* drop it.
|
|
13
|
+
*/
|
|
14
|
+
export interface ClearanceClaims {
|
|
15
|
+
v: 1;
|
|
16
|
+
/** Actor this token is bound to. A token stolen from one actor is invalid for another. */
|
|
17
|
+
sub: string;
|
|
18
|
+
/** Issued-at, ms since epoch. */
|
|
19
|
+
iat: number;
|
|
20
|
+
/** Expiry, ms since epoch. */
|
|
21
|
+
exp: number;
|
|
22
|
+
/** Unique id, so a solution can be spent exactly once. */
|
|
23
|
+
jti: string;
|
|
24
|
+
/** What was actually demonstrated. See {@link ClearanceLevel}. */
|
|
25
|
+
lvl: ClearanceLevel;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* What a clearance token proves — and, just as importantly, what it does not.
|
|
29
|
+
*
|
|
30
|
+
* - `pow` — the client ran JavaScript, has WebCrypto, and spent measurable CPU. This
|
|
31
|
+
* rules out cheap stateless scrapers. It does **not** prove a person is present: a
|
|
32
|
+
* headless browser solves a proof of work exactly as well as a human's laptop, only
|
|
33
|
+
* paying for the electricity. Treat it as a cost imposed, not as an identity.
|
|
34
|
+
* - `interaction` — a trusted input event was observed. Stronger, still forgeable by
|
|
35
|
+
* a driven browser.
|
|
36
|
+
* - `operator` — your own application asserted this is a human, e.g. an authenticated
|
|
37
|
+
* session. The only level this library treats as conclusive, because the assertion
|
|
38
|
+
* comes from you rather than from the client.
|
|
39
|
+
*/
|
|
40
|
+
export type ClearanceLevel = "pow" | "interaction" | "operator";
|
|
41
|
+
export interface ChallengeClaims {
|
|
42
|
+
v: 1;
|
|
43
|
+
sub: string;
|
|
44
|
+
iat: number;
|
|
45
|
+
exp: number;
|
|
46
|
+
/** Random value the proof of work is computed over. */
|
|
47
|
+
nonce: string;
|
|
48
|
+
/** Required leading zero bits in the digest. */
|
|
49
|
+
diff: number;
|
|
50
|
+
}
|
|
51
|
+
export type TokenVerification<T> = {
|
|
52
|
+
ok: true;
|
|
53
|
+
payload: T;
|
|
54
|
+
} | {
|
|
55
|
+
ok: false;
|
|
56
|
+
reason: "malformed" | "bad-signature" | "expired" | "wrong-actor";
|
|
57
|
+
};
|
|
58
|
+
export declare function issueToken<T extends object>(payload: T, secrets: readonly string[]): string;
|
|
59
|
+
/**
|
|
60
|
+
* Verifies signature and expiry.
|
|
61
|
+
*
|
|
62
|
+
* The order matters and is not arbitrary: the signature is checked *before* the
|
|
63
|
+
* claims are trusted for anything. Reading `exp` from an unverified token to decide
|
|
64
|
+
* whether to bother checking the signature is a classic way to turn a signed token
|
|
65
|
+
* into an unsigned one.
|
|
66
|
+
*/
|
|
67
|
+
export declare function verifyToken<T extends {
|
|
68
|
+
exp: number;
|
|
69
|
+
sub: string;
|
|
70
|
+
}>(token: string, secrets: readonly string[], now: number, expectedSubject?: string | readonly string[]): TokenVerification<T>;
|
|
71
|
+
export declare function newChallenge(subject: string, difficulty: number, ttlMs: number, now: number): ChallengeClaims;
|
|
72
|
+
export declare function newClearance(subject: string, level: ClearanceLevel, ttlMs: number, now: number): ClearanceClaims;
|