@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.
Files changed (186) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/LICENSE +102 -0
  3. package/README.md +308 -0
  4. package/bin/bothandlerjs.mjs +14 -0
  5. package/dist/actions/index.d.ts +22 -0
  6. package/dist/actions/types.d.ts +59 -0
  7. package/dist/adapters/fastify.d.ts +43 -0
  8. package/dist/adapters/fetch.d.ts +51 -0
  9. package/dist/adapters/index.cjs +723 -0
  10. package/dist/adapters/index.cjs.map +1 -0
  11. package/dist/adapters/index.d.ts +8 -0
  12. package/dist/adapters/index.js +692 -0
  13. package/dist/adapters/index.js.map +1 -0
  14. package/dist/adapters/koa.d.ts +27 -0
  15. package/dist/adapters/node.d.ts +29 -0
  16. package/dist/adapters/shared.d.ts +39 -0
  17. package/dist/audit.d.ts +172 -0
  18. package/dist/challenge/index.d.ts +176 -0
  19. package/dist/challenge/language.d.ts +57 -0
  20. package/dist/challenge/page.d.ts +42 -0
  21. package/dist/challenge/pow.d.ts +33 -0
  22. package/dist/challenge/token.d.ts +72 -0
  23. package/dist/cli.cjs +13388 -0
  24. package/dist/cli.cjs.map +1 -0
  25. package/dist/cli.d.ts +5 -0
  26. package/dist/cli.js +13358 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/client/index.cjs +139 -0
  29. package/dist/client/index.cjs.map +1 -0
  30. package/dist/client/index.d.ts +63 -0
  31. package/dist/client/index.js +112 -0
  32. package/dist/client/index.js.map +1 -0
  33. package/dist/config.d.ts +316 -0
  34. package/dist/core.d.ts +443 -0
  35. package/dist/corpus/adversarial.d.ts +2 -0
  36. package/dist/corpus/advertising-email.d.ts +2 -0
  37. package/dist/corpus/ai-crawlers.d.ts +2 -0
  38. package/dist/corpus/benign-bots.d.ts +2 -0
  39. package/dist/corpus/cdn-gateways.d.ts +2 -0
  40. package/dist/corpus/cookies.d.ts +49 -0
  41. package/dist/corpus/crawlers-regional.d.ts +2 -0
  42. package/dist/corpus/crawlers-vertical.d.ts +2 -0
  43. package/dist/corpus/headers.d.ts +403 -0
  44. package/dist/corpus/humans-apps.d.ts +2 -0
  45. package/dist/corpus/humans-browsers.d.ts +2 -0
  46. package/dist/corpus/humans.d.ts +2 -0
  47. package/dist/corpus/index.cjs +4196 -0
  48. package/dist/corpus/index.cjs.map +1 -0
  49. package/dist/corpus/index.d.ts +37 -0
  50. package/dist/corpus/index.js +4132 -0
  51. package/dist/corpus/index.js.map +1 -0
  52. package/dist/corpus/infrastructure.d.ts +17 -0
  53. package/dist/corpus/libraries-extended.d.ts +2 -0
  54. package/dist/corpus/ranges.d.ts +31 -0
  55. package/dist/corpus/reputation.d.ts +21 -0
  56. package/dist/corpus/runner.d.ts +130 -0
  57. package/dist/corpus/schema.d.ts +185 -0
  58. package/dist/corpus/tooling.d.ts +2 -0
  59. package/dist/corpus/unwanted.d.ts +2 -0
  60. package/dist/crawler-ranges.d.ts +113 -0
  61. package/dist/dashboard/client.generated.d.ts +7 -0
  62. package/dist/dashboard/feed.d.ts +186 -0
  63. package/dist/dashboard/index.d.ts +5 -0
  64. package/dist/dashboard/page.d.ts +51 -0
  65. package/dist/dashboard/parse-request.d.ts +28 -0
  66. package/dist/dashboard/preview.d.ts +45 -0
  67. package/dist/dashboard/server.d.ts +70 -0
  68. package/dist/dashboard/types.d.ts +714 -0
  69. package/dist/detectors/accept-signature.d.ts +6 -0
  70. package/dist/detectors/browsing-coherence.d.ts +44 -0
  71. package/dist/detectors/cadence.d.ts +28 -0
  72. package/dist/detectors/clearance.d.ts +25 -0
  73. package/dist/detectors/client-hints.d.ts +19 -0
  74. package/dist/detectors/client-signals.d.ts +27 -0
  75. package/dist/detectors/crawl-breadth.d.ts +28 -0
  76. package/dist/detectors/crawler-verification.d.ts +39 -0
  77. package/dist/detectors/fetch-metadata.d.ts +20 -0
  78. package/dist/detectors/header-integrity.d.ts +26 -0
  79. package/dist/detectors/header-order.d.ts +27 -0
  80. package/dist/detectors/identity-rotation.d.ts +34 -0
  81. package/dist/detectors/index.d.ts +56 -0
  82. package/dist/detectors/ip-intelligence.d.ts +30 -0
  83. package/dist/detectors/known-bots.d.ts +94 -0
  84. package/dist/detectors/probe-signature.d.ts +50 -0
  85. package/dist/detectors/rate-anomaly.d.ts +36 -0
  86. package/dist/detectors/self-identified.d.ts +33 -0
  87. package/dist/detectors/session-integrity.d.ts +24 -0
  88. package/dist/detectors/tls-fingerprint.d.ts +34 -0
  89. package/dist/detectors/trap.d.ts +98 -0
  90. package/dist/detectors/types.d.ts +70 -0
  91. package/dist/detectors/ua-coherence.d.ts +32 -0
  92. package/dist/evidence.d.ts +45 -0
  93. package/dist/facts.d.ts +46 -0
  94. package/dist/index.cjs +9600 -0
  95. package/dist/index.cjs.map +1 -0
  96. package/dist/index.d.ts +63 -0
  97. package/dist/index.js +9444 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/internal/async.d.ts +16 -0
  100. package/dist/internal/clock.d.ts +17 -0
  101. package/dist/internal/crypto.d.ts +26 -0
  102. package/dist/internal/dns.d.ts +100 -0
  103. package/dist/internal/emitter.d.ts +18 -0
  104. package/dist/internal/http.d.ts +22 -0
  105. package/dist/internal/ip.d.ts +73 -0
  106. package/dist/internal/lru.d.ts +34 -0
  107. package/dist/internal/matcher.d.ts +41 -0
  108. package/dist/internal/pattern.d.ts +14 -0
  109. package/dist/internal/ua.d.ts +92 -0
  110. package/dist/metrics.d.ts +120 -0
  111. package/dist/notify/hub.d.ts +70 -0
  112. package/dist/notify/index.d.ts +7 -0
  113. package/dist/notify/redact.d.ts +48 -0
  114. package/dist/notify/sinks.d.ts +62 -0
  115. package/dist/notify/types.d.ts +56 -0
  116. package/dist/policy/index.d.ts +7 -0
  117. package/dist/policy/match.d.ts +20 -0
  118. package/dist/policy/policy.d.ts +123 -0
  119. package/dist/policy/presets.d.ts +161 -0
  120. package/dist/policy/types.d.ts +155 -0
  121. package/dist/robots.d.ts +83 -0
  122. package/dist/state.d.ts +161 -0
  123. package/dist/stores/index.d.ts +5 -0
  124. package/dist/stores/memory.d.ts +33 -0
  125. package/dist/stores/redis.d.ts +59 -0
  126. package/dist/stores/types.d.ts +37 -0
  127. package/dist/types.d.ts +250 -0
  128. package/docs/challenge/index.md +152 -0
  129. package/docs/challenge/localisation.md +105 -0
  130. package/docs/concepts/actors.md +113 -0
  131. package/docs/concepts/evidence.md +133 -0
  132. package/docs/concepts/the-guard.md +121 -0
  133. package/docs/concepts/threat-model.md +120 -0
  134. package/docs/concepts/verdicts.md +108 -0
  135. package/docs/course/01-first-assessment.md +178 -0
  136. package/docs/course/02-proof-and-suspicion.md +164 -0
  137. package/docs/course/03-verdicts-and-scores.md +160 -0
  138. package/docs/course/04-the-guard.md +200 -0
  139. package/docs/course/05-detectors.md +178 -0
  140. package/docs/course/06-identity.md +161 -0
  141. package/docs/course/07-actors.md +178 -0
  142. package/docs/course/08-traps.md +183 -0
  143. package/docs/course/09-rules.md +224 -0
  144. package/docs/course/10-actions-and-presets.md +216 -0
  145. package/docs/course/11-the-challenge.md +227 -0
  146. package/docs/course/12-going-live.md +211 -0
  147. package/docs/course/13-operating-it.md +238 -0
  148. package/docs/course/14-scaling.md +195 -0
  149. package/docs/course/15-extending.md +259 -0
  150. package/docs/course/16-proving-it.md +266 -0
  151. package/docs/course/index.md +112 -0
  152. package/docs/design/decisions.md +216 -0
  153. package/docs/detection/client-signals.md +96 -0
  154. package/docs/detection/detectors.md +376 -0
  155. package/docs/detection/index.md +134 -0
  156. package/docs/detection/signatures.md +111 -0
  157. package/docs/detection/verification.md +123 -0
  158. package/docs/detection/writing-a-detector.md +145 -0
  159. package/docs/index.md +100 -0
  160. package/docs/integration/adapters.md +138 -0
  161. package/docs/integration/client-ip.md +114 -0
  162. package/docs/integration/index.md +55 -0
  163. package/docs/integration/stores.md +76 -0
  164. package/docs/operations/audit.md +116 -0
  165. package/docs/operations/dashboard.md +725 -0
  166. package/docs/operations/index.md +101 -0
  167. package/docs/operations/metrics.md +74 -0
  168. package/docs/operations/notifications.md +72 -0
  169. package/docs/operations/runtime-changes.md +118 -0
  170. package/docs/policy/actions.md +196 -0
  171. package/docs/policy/index.md +84 -0
  172. package/docs/policy/presets.md +191 -0
  173. package/docs/policy/robots.md +128 -0
  174. package/docs/policy/rules.md +134 -0
  175. package/docs/reference/api.md +126 -0
  176. package/docs/reference/configuration.md +175 -0
  177. package/docs/start/choosing-a-policy.md +127 -0
  178. package/docs/start/first-integration.md +127 -0
  179. package/docs/start/installation.md +63 -0
  180. package/docs/start/upgrading.md +72 -0
  181. package/docs/testing/cli.md +119 -0
  182. package/docs/testing/corpus.md +129 -0
  183. package/docs/testing/index.md +61 -0
  184. package/docs/testing/replay.md +102 -0
  185. package/docs/testing/try-it.md +189 -0
  186. 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;