captchakraken 2.6.1 → 2.9.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 (38) hide show
  1. package/README.md +90 -7
  2. package/dist/cli-invocation.d.ts +16 -16
  3. package/dist/cli-invocation.js +33 -21
  4. package/dist/errors.d.ts +1 -1
  5. package/dist/humanize.d.ts +377 -0
  6. package/dist/humanize.js +716 -0
  7. package/dist/index.d.ts +33 -0
  8. package/dist/index.js +44 -1
  9. package/dist/model-name.d.ts +1 -1
  10. package/dist/model-name.js +1 -1
  11. package/dist/playwright-types.d.ts +18 -0
  12. package/dist/solver.d.ts +314 -16
  13. package/dist/solver.js +1208 -278
  14. package/dist/timing.d.ts +48 -0
  15. package/dist/timing.js +103 -0
  16. package/dist/trajectory.d.ts +28 -1
  17. package/dist/trajectory.js +105 -1
  18. package/dist/types.d.ts +113 -2
  19. package/package.json +2 -1
  20. package/python/README.md +43 -0
  21. package/python/pyproject.toml +1 -1
  22. package/python/src/captchakraken/__init__.py +36 -1
  23. package/python/src/captchakraken/cli.py +66 -4
  24. package/python/src/captchakraken/config.py +28 -2
  25. package/python/src/captchakraken/errors.py +24 -0
  26. package/python/src/captchakraken/humanize.py +929 -0
  27. package/python/src/captchakraken/keyframes.py +184 -12
  28. package/python/src/captchakraken/models.json +180 -18
  29. package/python/src/captchakraken/page_solver.py +1132 -321
  30. package/python/src/captchakraken/pinned_model.json +3 -2
  31. package/python/src/captchakraken/planner.py +204 -16
  32. package/python/src/captchakraken/prompts.py +186 -4
  33. package/python/src/captchakraken/server_manager.py +9 -4
  34. package/python/src/captchakraken/solver.py +98 -58
  35. package/python/src/captchakraken/timing.py +8 -3
  36. package/python/src/captchakraken/tool_calls/find_grid.py +357 -20
  37. package/python/src/captchakraken/trajectory.py +117 -0
  38. package/python/src/captchakraken/updater.py +62 -1
package/README.md CHANGED
@@ -19,6 +19,26 @@ nothing at all.
19
19
  > Full docs — demo videos, accuracy, self-hosting — live in the main repo
20
20
  > **[CaptchaKraken](https://github.com/JWriter20/CaptchaKraken)**.
21
21
 
22
+ ## Watch it work
23
+
24
+ <p align="center">
25
+ <img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/demo/hcaptcha_grid.webp" width="260"
26
+ alt="A live hCaptcha image select challenge being solved end to end">
27
+ <img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/demo/recaptcha_4x4.webp" width="260"
28
+ alt="A live reCAPTCHA 4×4 tile grid challenge being solved end to end">
29
+ <img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/demo/geetest_slide.webp" width="260"
30
+ alt="A live GeeTest slide jigsaw challenge being solved end to end">
31
+ </p>
32
+
33
+ hCaptcha image select 12/12 in 10.5s · reCAPTCHA 4×4 tile grid 9/10 in 8.7s · GeeTest slide jigsaw 10/10 in 7.6s — median of the solved attempts, measured 2026-08-19 on **captcha-v12**
34
+ against each vendor's own public demo page. Counts rather than percentages
35
+ because ten attempts is not a percentage. Idle time is cut from the clips, so
36
+ they run shorter than the solves they show.
37
+
38
+ **Ten more puzzle types**, as video and with the full method, at
39
+ [captchakraken.com](https://captchakraken.com) and in the
40
+ [main repo](https://github.com/JWriter20/CaptchaKraken#watch-it-work).
41
+
22
42
  ## Install
23
43
 
24
44
  ```bash
@@ -83,15 +103,78 @@ await browser.close();
83
103
 
84
104
  Puppeteer users wrap the page once with `fromPuppeteer(page)`.
85
105
 
86
- ## Configuration
106
+ ## How it moves — mouse, mobile, none, or yours
87
107
 
88
- | Variable | Meaning |
89
- |---|---|
90
- | `VLLM_BASE_URL` | Inference endpoint (local vLLM, a server you run, or `https://api.captchakraken.com/v1`) |
91
- | `CAPTCHA_KRAKEN_API_KEY` | Bearer token (`VLLM_API_KEY` also accepted) |
108
+ Solving a captcha is two jobs: reading the puzzle, and *performing* the answer.
109
+ The second is a choice of **input device**, not a realism dial.
110
+
111
+ ```typescript
112
+ new CaptchaKrakenSolver({ humanization: 'mouse' }) // default
113
+ new CaptchaKrakenSolver({ humanization: 'mobile' }) // real touch events
114
+ new CaptchaKrakenSolver({ humanization: 'none' }) // straight to the DOM effect
115
+ new CaptchaKrakenSolver({ humanizer: myOwn }) // yours; overrides the above
116
+ ```
117
+
118
+ Unset, it reads `CAPTCHA_HUMANIZATION` and then falls back to `mouse` — which is
119
+ byte-for-byte what the driver did before the option existed, so nothing moves for
120
+ an existing caller. Anything set in code **wins** over the env var: which mode is
121
+ right is a property of the page you are driving, not a deployment decision.
122
+
123
+ `mobile` never touches `page.mouse`. A mousemove at a touch-only widget is the
124
+ *wrong event*, not a weaker one — the page's touch handlers never fire and the
125
+ solve fails for a reason nothing reports. It needs a Chromium-family page with
126
+ touch turned on:
127
+
128
+ ```typescript
129
+ import { chromium, devices } from 'playwright';
130
+
131
+ const context = await browser.newContext({ ...devices['Pixel 7'], hasTouch: true });
132
+ await new CaptchaKrakenSolver({ humanization: 'mobile' }).solve(await context.newPage());
133
+ ```
134
+
135
+ Or drive a real handset over **Appium / WebdriverIO / Selenium** — gestures go
136
+ out as W3C pointer actions with `pointerType: "touch"`:
137
+
138
+ ```typescript
139
+ new CaptchaKrakenSolver({
140
+ humanization: 'mobile',
141
+ touchDriver: driver, // your Appium / WebdriverIO session
142
+ touchTransform: { scale: 3, origin: [0, 132] }, // CSS px -> screen px
143
+ });
144
+ ```
145
+
146
+ `touchTransform` is required rather than guessed: get it wrong and nothing
147
+ raises — the action chain is valid, the device performs it, and the finger lands
148
+ somewhere else. A `touchDriver` with no `scale` on a page reporting
149
+ `devicePixelRatio !== 1` refuses on the first gesture and names the value to pass.
150
+
151
+ Full details, including writing your own humanizer:
152
+ [docs/usage.md § How it moves](https://github.com/JWriter20/CaptchaKraken/blob/main/docs/usage.md#how-it-moves--mouse-mobile-none-or-yours).
153
+
154
+ ## Configuration
92
155
 
93
- Both fall back to `~/.captchakraken/credentials` when unset, so the hosted API
94
- needs no environment variables at all.
156
+ | Variable | Meaning | Default |
157
+ |---|---|---|
158
+ | `VLLM_BASE_URL` | Inference endpoint (local vLLM, a server you run, or `https://api.captchakraken.com/v1`) | credentials file, else `http://localhost:8000/v1` |
159
+ | `CAPTCHA_KRAKEN_API_KEY` | Bearer token (`VLLM_API_KEY` also accepted) | credentials file, else `EMPTY` |
160
+ | `CAPTCHA_HUMANIZATION` | How gestures are performed: `mouse`, `mobile` or `none`. Loses to anything set in code | `mouse` |
161
+ | `CAPTCHA_BASE_MODEL` | Base weights vLLM loads | `RedHatAI/Qwen3.5-9B-FP8-dynamic` |
162
+ | `CAPTCHA_LORA_ADAPTER` | Captcha adapter (HF id or path) | `CaptchaKraken/CaptchaKraken-Lora-v1.2` |
163
+ | `CAPTCHA_LORA_NAME` | Served adapter name sent as `model` | `captcha-v12` |
164
+ | `CAPTCHA_KRAKEN_AUTOSTART` | `0` never auto-starts a local server | `1` |
165
+ | `CAPTCHA_KRAKEN_PYTHON` | Interpreter used for the bundled engine | the package's venv, else `python3` |
166
+ | `CAPTCHA_KRAKEN_SKIP_PYTHON_SETUP` | `1` skips the `postinstall` bootstrap | unset |
167
+ | `CAPTCHA_DEBUG` | `1` prints solver diagnostics to stderr | `0` |
168
+
169
+ `VLLM_BASE_URL` and `CAPTCHA_KRAKEN_API_KEY` fall back to
170
+ `~/.captchakraken/credentials` when unset, so the hosted API needs no
171
+ environment variables at all.
172
+
173
+ **Why the model and server variables appear here.** This package bundles the
174
+ Python engine and spawns it, forwarding the environment wholesale — so every
175
+ variable the engine reads applies to a TypeScript solve too. The full list, and
176
+ the vLLM server knobs, are in
177
+ [AGENTS.md § Environment variables](https://github.com/JWriter20/CaptchaKraken/blob/main/AGENTS.md#environment-variables).
95
178
 
96
179
  ## License
97
180
 
@@ -1,26 +1,24 @@
1
1
  /**
2
2
  * How the JS client hands a solve request to the bundled Python CLI.
3
3
  *
4
- * The API key used to travel as a POSITIONAL ARGUMENT, and the whole command
5
- * line was then echoed:
4
+ * Two rules govern this module, and both exist because the alternative is
5
+ * unsafe rather than merely untidy:
6
6
  *
7
- * console.log(`Executing CaptchaKraken CLI: ${command}`);
8
- * // Executing CaptchaKraken CLI: python -m captchakraken.cli "shot.png" \
9
- * // captcha-v12 captchaKrakenApi b8978fa3392... --puzzle-source=hcaptcha
7
+ * 1. The API key travels in the ENVIRONMENT, never in argv and never to stdout.
8
+ * Argv is world-readable on Linux — any local user can read
9
+ * `/proc/<pid>/cmdline` while a solve runs, and `ps` shows it — and anything
10
+ * printed alongside the command lands in CI logs and terminal scrollback.
11
+ * An environment block is per-process and not world-readable.
10
12
  *
11
- * Two leaks from one mistake. Argv is world-readable on Linux — any local user
12
- * can read `/proc/<pid>/cmdline` while the solve runs, and `ps` shows it — and
13
- * the key was additionally written to stdout, so it landed in CI logs, terminal
14
- * scrollback, and anything scraping driver output.
15
- *
16
- * Secrets go through the environment instead, which is per-process and not
17
- * world-readable, and the command is built as an ARGV ARRAY for `execFile`
18
- * rather than a joined string for a shell, so a path with a space or a quote
19
- * cannot reshape the command.
13
+ * 2. The command is built as an ARGV ARRAY for `execFile`, not a joined string
14
+ * for a shell, so a screenshot path containing a space or a quote cannot
15
+ * reshape the command.
20
16
  */
21
17
  /** Env var the CLI reads the bearer token from. */
22
18
  export declare const API_KEY_ENV = "CAPTCHA_KRAKEN_API_KEY";
23
- /** Historical positional placeholder. The CLI still accepts the slot. */
19
+ /** Env var carrying the re-ask level for a board the vendor already refused. */
20
+ export declare const RESAMPLE_LEVEL_ENV = "CAPTCHA_RESAMPLE_LEVEL";
21
+ /** Positional placeholder the CLI still accepts in this slot. */
24
22
  export declare const API_PROVIDER = "captchaKrakenApi";
25
23
  export interface SolveInvocation {
26
24
  imagePath: string;
@@ -28,6 +26,8 @@ export interface SolveInvocation {
28
26
  puzzleSource: string;
29
27
  retryMode?: string | null;
30
28
  textMode?: boolean;
29
+ /** One expert of a routed model, or undefined to route by prompt family. */
30
+ expert?: string | null;
31
31
  }
32
32
  /**
33
33
  * The argv for one solve. No shell, no quoting, and NO CREDENTIAL — the key is
@@ -35,7 +35,7 @@ export interface SolveInvocation {
35
35
  */
36
36
  export declare function buildSolveArgs(invocation: SolveInvocation): string[];
37
37
  /** Base env plus the credential, when there is one. */
38
- export declare function solveEnv(base: NodeJS.ProcessEnv, apiKey?: string): NodeJS.ProcessEnv;
38
+ export declare function solveEnv(base: NodeJS.ProcessEnv, apiKey?: string, resampleLevel?: number): NodeJS.ProcessEnv;
39
39
  /**
40
40
  * A log line for a solve invocation, with any credential removed.
41
41
  *
@@ -2,38 +2,36 @@
2
2
  /**
3
3
  * How the JS client hands a solve request to the bundled Python CLI.
4
4
  *
5
- * The API key used to travel as a POSITIONAL ARGUMENT, and the whole command
6
- * line was then echoed:
5
+ * Two rules govern this module, and both exist because the alternative is
6
+ * unsafe rather than merely untidy:
7
7
  *
8
- * console.log(`Executing CaptchaKraken CLI: ${command}`);
9
- * // Executing CaptchaKraken CLI: python -m captchakraken.cli "shot.png" \
10
- * // captcha-v12 captchaKrakenApi b8978fa3392... --puzzle-source=hcaptcha
8
+ * 1. The API key travels in the ENVIRONMENT, never in argv and never to stdout.
9
+ * Argv is world-readable on Linux — any local user can read
10
+ * `/proc/<pid>/cmdline` while a solve runs, and `ps` shows it — and anything
11
+ * printed alongside the command lands in CI logs and terminal scrollback.
12
+ * An environment block is per-process and not world-readable.
11
13
  *
12
- * Two leaks from one mistake. Argv is world-readable on Linux — any local user
13
- * can read `/proc/<pid>/cmdline` while the solve runs, and `ps` shows it — and
14
- * the key was additionally written to stdout, so it landed in CI logs, terminal
15
- * scrollback, and anything scraping driver output.
16
- *
17
- * Secrets go through the environment instead, which is per-process and not
18
- * world-readable, and the command is built as an ARGV ARRAY for `execFile`
19
- * rather than a joined string for a shell, so a path with a space or a quote
20
- * cannot reshape the command.
14
+ * 2. The command is built as an ARGV ARRAY for `execFile`, not a joined string
15
+ * for a shell, so a screenshot path containing a space or a quote cannot
16
+ * reshape the command.
21
17
  */
22
18
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.API_PROVIDER = exports.API_KEY_ENV = void 0;
19
+ exports.API_PROVIDER = exports.RESAMPLE_LEVEL_ENV = exports.API_KEY_ENV = void 0;
24
20
  exports.buildSolveArgs = buildSolveArgs;
25
21
  exports.solveEnv = solveEnv;
26
22
  exports.redactCommand = redactCommand;
27
23
  /** Env var the CLI reads the bearer token from. */
28
24
  exports.API_KEY_ENV = 'CAPTCHA_KRAKEN_API_KEY';
29
- /** Historical positional placeholder. The CLI still accepts the slot. */
25
+ /** Env var carrying the re-ask level for a board the vendor already refused. */
26
+ exports.RESAMPLE_LEVEL_ENV = 'CAPTCHA_RESAMPLE_LEVEL';
27
+ /** Positional placeholder the CLI still accepts in this slot. */
30
28
  exports.API_PROVIDER = 'captchaKrakenApi';
31
29
  /**
32
30
  * The argv for one solve. No shell, no quoting, and NO CREDENTIAL — the key is
33
31
  * supplied by `solveEnv` instead.
34
32
  */
35
33
  function buildSolveArgs(invocation) {
36
- const { imagePath, model, puzzleSource, retryMode, textMode } = invocation;
34
+ const { imagePath, model, puzzleSource, retryMode, textMode, expert } = invocation;
37
35
  const args = [
38
36
  '-m',
39
37
  'captchakraken.cli',
@@ -46,13 +44,27 @@ function buildSolveArgs(invocation) {
46
44
  args.push(`--retry-mode=${retryMode}`);
47
45
  if (textMode)
48
46
  args.push('--text-mode');
47
+ // Only when set. An absent flag is what lets the Python side route by prompt
48
+ // family, and passing `--expert=` would be an empty choice rather than none.
49
+ if (expert)
50
+ args.push(`--expert=${expert}`);
49
51
  return args;
50
52
  }
51
53
  /** Base env plus the credential, when there is one. */
52
- function solveEnv(base, apiKey) {
53
- if (!apiKey)
54
- return base;
55
- return { ...base, [exports.API_KEY_ENV]: apiKey };
54
+ function solveEnv(base, apiKey, resampleLevel) {
55
+ const out = apiKey ? { ...base, [exports.API_KEY_ENV]: apiKey } : { ...base };
56
+ // How many times THIS board has already been read and refused. The CLI is a
57
+ // fresh process per round, so it cannot know; and without it a re-ask is the
58
+ // same greedy arithmetic on the same pixels and returns the same answer,
59
+ // which is the whole of the "same answer 3 times running" failure.
60
+ //
61
+ // The LEVEL travels, not the temperature: the schedule lives once, in
62
+ // `planner.RESAMPLE_TEMPERATURES`, so the two ports cannot drift apart on a
63
+ // number Tier 3 would then average.
64
+ if (resampleLevel && resampleLevel > 0) {
65
+ out[exports.RESAMPLE_LEVEL_ENV] = String(resampleLevel);
66
+ }
67
+ return out;
56
68
  }
57
69
  /**
58
70
  * A log line for a solve invocation, with any credential removed.
package/dist/errors.d.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * message drift, and the Python one is also what a direct CLI user sees.
15
15
  */
16
16
  /** The machine-readable half. Mirrors the gateway's `GatewayErrorCode`. */
17
- export type CaptchaKrakenErrorCode = 'missing_api_key' | 'invalid_api_key' | 'account_suspended' | 'insufficient_credits' | 'rate_limited' | 'solve_abandoned' | 'unrecognized_prompt' | 'invalid_request' | 'request_too_large' | 'upstream_unavailable' | (string & {});
17
+ export type CaptchaKrakenErrorCode = 'missing_api_key' | 'invalid_api_key' | 'account_suspended' | 'insufficient_credits' | 'rate_limited' | 'solve_abandoned' | 'unrecognized_prompt' | 'invalid_request' | 'request_too_large' | 'upstream_unavailable' | 'model_not_licensed' | 'model_not_serving' | (string & {});
18
18
  export declare class CaptchaKrakenAPIError extends Error {
19
19
  readonly status: number | undefined;
20
20
  readonly code: CaptchaKrakenErrorCode | undefined;
@@ -0,0 +1,377 @@
1
+ /**
2
+ * How the driver MOVES. One pluggable object per input device.
3
+ *
4
+ * The TypeScript side of `python/src/captchakraken/humanize.py`; the two are one
5
+ * design in two languages, and `the training repo's humanizer parity tests` in
6
+ * the training repo pins that the mode names and the pause vocabulary have
7
+ * not drifted.
8
+ *
9
+ * Humanisation used to be wired straight into `solver.ts`: every gesture was a
10
+ * `page.mouse.*` call with a Bezier trajectory in front of it and a `Math.
11
+ * random()` sleep behind it, and there was no way to ask for anything else. That
12
+ * is wrong in three directions at once —
13
+ *
14
+ * - a caller driving a MOBILE page has no cursor. Dispatching mousemove at a
15
+ * touch-only widget is not weak humanisation, it is the wrong event type:
16
+ * the page's touch handlers never fire, and a vendor that scores pointer
17
+ * telemetry sees a desktop mouse on a phone.
18
+ * - a caller who has their OWN humanisation (a hardware pointer, a proxied
19
+ * device farm, a model of their own users) was composing two of them. That
20
+ * is not hypothetical — camoufox's `humanize` juggler re-humanises every
21
+ * `mouse.move()` it is handed, and running both measured 82.1s against
22
+ * 13.4s on one a GeeTest v4 slider solve, because each of the 60 trajectory
23
+ * points became its own humanised sub-trajectory.
24
+ * - a caller on their own infrastructure, against their own fixtures, is
25
+ * paying for texture nobody is measuring.
26
+ *
27
+ * So the vocabulary of gestures is an INTERFACE and the humanisation is an
28
+ * implementation of it. The solver says "tap here", "drag this there", "type
29
+ * that"; a `Humanizer` decides what events that is and how long it takes.
30
+ *
31
+ * Four modes, selected by `CaptchaKrakenConfig.humanization`:
32
+ *
33
+ * 'mouse' MouseHumanizer — the default, and byte-for-byte what the
34
+ * driver did before this module existed.
35
+ * 'mobile' MobileHumanizer — touch events, finger kinematics. Dispatches
36
+ * through a `TouchBackend`, so it drives either
37
+ * a Chromium-family page (CDP) or an
38
+ * Appium/WebdriverIO driver on a real device.
39
+ * 'none' NullHumanizer — the shortest legal path to the same DOM
40
+ * effect.
41
+ * custom anything satisfying `Humanizer`, passed as
42
+ * `CaptchaKrakenConfig.humanizer`.
43
+ *
44
+ * THE POINTER POSITION LIVES HERE, not in the solver. A humanizer that
45
+ * dispatches no motion at all (mobile, between taps) still has to answer "where
46
+ * is the pointer", because that is what the next gesture starts from and what
47
+ * `SolveResult.finalMousePosition` reports.
48
+ */
49
+ import type { Page, PlaywrightElementHandle } from './playwright-types.js';
50
+ export type Point = [number, number];
51
+ /**
52
+ * Every inter-gesture wait the driver takes, named.
53
+ *
54
+ * A vocabulary rather than a number at each call site, because the whole point
55
+ * of the mode switch is that these differ per device — a finger dwells on a tap
56
+ * about four times as long as a mouse button is held, and neither number means
57
+ * anything to the caller who turned humanisation off. Each mode supplies its own
58
+ * table below; an unknown name yields no wait rather than throwing, so adding a
59
+ * pause site cannot break a custom humanizer written against an older release.
60
+ *
61
+ * tap inside a click/tap, between press and release
62
+ * between between two taps of one batch (grid tiles)
63
+ * grab after pressing, before a drag starts moving
64
+ * drop after a drag arrives, before releasing
65
+ * probe between the slider's measurement nudges
66
+ * settle before releasing a slider — the release IS the submit, and some
67
+ * vendors sample the final milliseconds of the gesture
68
+ * key between two typed characters
69
+ */
70
+ export type PauseKind = 'tap' | 'between' | 'grab' | 'drop' | 'probe' | 'settle' | 'key';
71
+ export declare const PAUSE_KINDS: readonly PauseKind[];
72
+ /**
73
+ * The gesture vocabulary the solver drives. Implement this to supply your own
74
+ * humanisation; only `move`, `press`, `release`, `typeText` and `pause` are
75
+ * required, and `BaseHumanizer` composes `click`/`drag` from them.
76
+ */
77
+ export interface Humanizer {
78
+ /** Mode name, for logs. */
79
+ readonly name: string;
80
+ /**
81
+ * Whether this device has a cursor that can rest somewhere without touching.
82
+ * False switches off every hover-for-realism behaviour — the reCAPTCHA tile
83
+ * hover and the idle drift during inference — because on a touchscreen those
84
+ * are not weak mimicry, they are impossible.
85
+ */
86
+ readonly hovers: boolean;
87
+ /** Where the pointer is now. */
88
+ at: Point;
89
+ /** Called once at the top of each `solve()`. Drop per-page caches. */
90
+ reset(page: Page): Promise<void>;
91
+ move(page: Page, to: Point): Promise<void>;
92
+ press(page: Page): Promise<void>;
93
+ release(page: Page): Promise<void>;
94
+ click(page: Page, to: Point): Promise<void>;
95
+ drag(page: Page, src: Point, dst: Point): Promise<void>;
96
+ typeText(page: Page, field: PlaywrightElementHandle, text: string): Promise<boolean>;
97
+ pause(kind: PauseKind): Promise<void>;
98
+ }
99
+ type PauseTable = Record<string, [number, number]>;
100
+ export declare abstract class BaseHumanizer implements Humanizer {
101
+ abstract readonly name: string;
102
+ abstract readonly hovers: boolean;
103
+ protected abstract readonly pauses: PauseTable;
104
+ at: Point;
105
+ constructor(start?: Point);
106
+ reset(_page: Page): Promise<void>;
107
+ abstract move(page: Page, to: Point): Promise<void>;
108
+ abstract press(page: Page): Promise<void>;
109
+ abstract release(page: Page): Promise<void>;
110
+ abstract typeText(page: Page, field: PlaywrightElementHandle, text: string): Promise<boolean>;
111
+ protected pauseMs(kind: PauseKind): number;
112
+ pause(kind: PauseKind): Promise<void>;
113
+ click(page: Page, to: Point): Promise<void>;
114
+ drag(page: Page, src: Point, dst: Point): Promise<void>;
115
+ }
116
+ /**
117
+ * A hand on a mouse. Bezier arcs, Fitts's-law durations, overshoot.
118
+ *
119
+ * Everything in here was `solver.performSmoothMove` / `tracePath` before this
120
+ * module existed, and is unchanged: the constants were measured and a refactor
121
+ * is not the place to revisit them.
122
+ */
123
+ export declare class MouseHumanizer extends BaseHumanizer {
124
+ readonly name = "mouse";
125
+ readonly hovers = true;
126
+ /** Inclusive ranges, in ms. See PauseKind. */
127
+ protected readonly pauses: PauseTable;
128
+ private frequency;
129
+ constructor(start?: Point, frequency?: number);
130
+ move(page: Page, to: Point): Promise<void>;
131
+ press(page: Page): Promise<void>;
132
+ release(page: Page): Promise<void>;
133
+ typeText(page: Page, _field: PlaywrightElementHandle, text: string): Promise<boolean>;
134
+ private trace;
135
+ }
136
+ /** `[x, y, dtMs]` — dt is the wait BEFORE that sample. */
137
+ export type TouchSample = [number, number, number];
138
+ /**
139
+ * Where touch events actually go.
140
+ *
141
+ * Three methods rather than one per sample, because the two backends want
142
+ * opposite things: CDP dispatches one event per call and we pace it locally,
143
+ * while a W3C/Appium driver takes a whole timed action chain in one round trip
144
+ * and paces it on the device. Handing `move` the entire leg lets each do what it
145
+ * is good at — and on a real device, pacing over the wire is not pacing at all.
146
+ */
147
+ export interface TouchBackend {
148
+ readonly name: string;
149
+ down(x: number, y: number): Promise<void>;
150
+ move(path: TouchSample[]): Promise<void>;
151
+ up(x: number, y: number): Promise<void>;
152
+ }
153
+ /**
154
+ * `Input.dispatchTouchEvent` over a CDP session on a Playwright page.
155
+ *
156
+ * Chromium-family only, and that is checked at construction rather than
157
+ * discovered per gesture: WebKit and Firefox (camoufox included) expose no touch
158
+ * dispatch through Playwright at all, so the alternatives there are to fail
159
+ * loudly or to quietly emit MOUSE events at a touch-only widget. The second is
160
+ * worse than not running — the page's touch handlers never fire, the solve fails
161
+ * for a reason nothing reports, and the report reads as a model that cannot
162
+ * solve mobile puzzles.
163
+ *
164
+ * The context must also have been created with `hasTouch: true`, or the page
165
+ * advertises no touch support to feature detection and a mobile widget renders
166
+ * its desktop branch.
167
+ */
168
+ export declare class CdpTouchBackend implements TouchBackend {
169
+ readonly name = "cdp";
170
+ private session;
171
+ private constructor();
172
+ static open(page: Page): Promise<CdpTouchBackend>;
173
+ private send;
174
+ down(x: number, y: number): Promise<void>;
175
+ move(path: TouchSample[]): Promise<void>;
176
+ up(_x: number, _y: number): Promise<void>;
177
+ }
178
+ /** CSS-pixel → device-pixel transform for a real handset. See AppiumTouchBackend. */
179
+ export interface TouchTransform {
180
+ scale?: number;
181
+ origin?: Point;
182
+ }
183
+ /**
184
+ * W3C pointer actions with `pointerType: 'touch'`, for Appium and WebdriverIO.
185
+ *
186
+ * Batches a whole leg into ONE action chain with per-sample durations, so the
187
+ * kinematics are reproduced BY THE DEVICE rather than by a JS loop round-tripping
188
+ * over the wire — over which a 90Hz path is not 90Hz at all.
189
+ *
190
+ * Press and release are separate `performActions()` calls on purpose. W3C input
191
+ * state is per SESSION, so a pointer put down in one chain stays down across
192
+ * later chains until it is lifted; that is what lets the puzzle-slider driver
193
+ * press, screenshot, steer, screenshot and only then release. (It is also why
194
+ * the spec has a "release actions" endpoint at all.)
195
+ *
196
+ * COORDINATES. Everything upstream of here is CSS pixels in the page's viewport,
197
+ * because that is what `boundingBox()` returns. A real device wants screen
198
+ * pixels, and the two differ by the device pixel ratio and by whatever chrome
199
+ * sits above the webview. Neither is guessable from here, so both are options:
200
+ *
201
+ * new AppiumTouchBackend(driver, { scale: 3, origin: [0, 132] })
202
+ *
203
+ * `scale` is usually `window.devicePixelRatio`; `origin` is the top-left of the
204
+ * webview in screen coordinates. Defaults are the identity transform, which is
205
+ * correct for browser mobile emulation and for any caller who has already mapped
206
+ * the coordinates themselves.
207
+ */
208
+ export declare class AppiumTouchBackend implements TouchBackend {
209
+ readonly name = "appium";
210
+ private driver;
211
+ private scale;
212
+ private origin;
213
+ /**
214
+ * UNSET is not the same fact as an explicit 1. The first is a caller who has
215
+ * not thought about the transform; the second is one who has, and says the
216
+ * coordinates are already mapped. Only the first is checked.
217
+ */
218
+ private scaleGiven;
219
+ private page;
220
+ private checked;
221
+ constructor(driver: any, transform?: TouchTransform, page?: Page);
222
+ /**
223
+ * Refuse an unset scale on a session that reports it is not 1.
224
+ *
225
+ * A wrong transform fails SILENTLY on both sides of the wire: the chain is
226
+ * valid W3C, the device performs it, and the finger lands somewhere else. The
227
+ * solve then fails looking exactly like a model that cannot read the puzzle.
228
+ *
229
+ * Read once, not per gesture — it is a round trip into the page and a solve
230
+ * makes hundreds of these. An unreadable ratio is absent evidence, not
231
+ * evidence of a mismatch, so it falls through to the identity. `origin`
232
+ * cannot be measured from here at all, so it is named in the refusal rather
233
+ * than guessed at.
234
+ */
235
+ private checkScale;
236
+ private map;
237
+ /**
238
+ * One W3C action chain on a single touch pointer.
239
+ *
240
+ * Sent as the raw protocol payload rather than through a client's action
241
+ * builder, so this imports nothing and works against any client that speaks
242
+ * WebDriver — WebdriverIO (`performActions`), selenium-webdriver, or a thin
243
+ * HTTP wrapper of someone's own.
244
+ */
245
+ private perform;
246
+ down(x: number, y: number): Promise<void>;
247
+ move(path: TouchSample[]): Promise<void>;
248
+ up(_x: number, _y: number): Promise<void>;
249
+ }
250
+ /**
251
+ * Playwright's `page.touchscreen.tap()` — taps only, no travel.
252
+ *
253
+ * The fallback for a browser with touch support but no CDP (WebKit). A tap still
254
+ * lands correctly; a DRAG cannot be expressed at all, so it throws rather than
255
+ * approximating one with a mouse, for the reason in `CdpTouchBackend`.
256
+ */
257
+ export declare class TouchscreenTouchBackend implements TouchBackend {
258
+ readonly name = "touchscreen";
259
+ private page;
260
+ private pending;
261
+ constructor(page: Page);
262
+ down(x: number, y: number): Promise<void>;
263
+ move(_path: TouchSample[]): Promise<void>;
264
+ up(x: number, y: number): Promise<void>;
265
+ }
266
+ /**
267
+ * Pick a backend. An explicit `driver` always wins.
268
+ *
269
+ * `driver` is what a caller passes when the thing under automation is not the
270
+ * thing being touched — an Appium session driving a real handset while the page
271
+ * object is a webview bridge over it.
272
+ */
273
+ export declare function touchBackendFor(page: Page, driver?: any, transform?: TouchTransform): Promise<TouchBackend>;
274
+ /**
275
+ * A finger on glass.
276
+ *
277
+ * What differs from `MouseHumanizer`, and none of it is cosmetic:
278
+ *
279
+ * - **A move that is not touching dispatches nothing.** There is no hover on a
280
+ * touchscreen. The position is still RECORDED, because the next gesture
281
+ * starts from it, but no event is emitted — emitting one would be the
282
+ * desktop tell this mode exists to remove.
283
+ * - **Taps carry a contact wobble.** A finger held on glass for 90ms does not
284
+ * report one unchanging coordinate; the centroid of the contact patch rolls
285
+ * a pixel or two under pressure. A tap with zero movement between touchstart
286
+ * and touchend is a synthetic tap.
287
+ * - **Touch kinematics**, via `generate_swipe` — see its docstring for why that
288
+ * is a different model rather than the mouse one retuned.
289
+ * - **Longer, more variable pauses.** Every measured touch interaction is
290
+ * slower than its mouse equivalent, and a phone's are more variable because
291
+ * the hand holding the device is also moving.
292
+ */
293
+ export declare class MobileHumanizer extends BaseHumanizer {
294
+ readonly name = "mobile";
295
+ readonly hovers = false;
296
+ protected readonly pauses: PauseTable;
297
+ private backend;
298
+ private driver;
299
+ private transform;
300
+ private frequency;
301
+ private down;
302
+ constructor(start?: Point, options?: {
303
+ backend?: TouchBackend;
304
+ driver?: any;
305
+ transform?: TouchTransform;
306
+ frequency?: number;
307
+ });
308
+ reset(page: Page): Promise<void>;
309
+ private touch;
310
+ move(page: Page, to: Point): Promise<void>;
311
+ press(page: Page): Promise<void>;
312
+ release(page: Page): Promise<void>;
313
+ /** Tap. The wobble is the point — see the class docstring. */
314
+ click(page: Page, to: Point): Promise<void>;
315
+ /**
316
+ * Into the field, at soft-keyboard pace.
317
+ *
318
+ * The mouse path clears with Control+A. There is no Control on a phone
319
+ * keyboard, so the box is cleared through the element instead — which also
320
+ * works on an Appium element, where `page.keyboard` does not exist.
321
+ */
322
+ typeText(page: Page, field: PlaywrightElementHandle, text: string): Promise<boolean>;
323
+ }
324
+ /**
325
+ * No humanisation: the shortest legal path to the same DOM effect.
326
+ *
327
+ * One mousemove per gesture instead of sixty, no dwell, no jitter, and text goes
328
+ * in with a single `fill()`. Roughly an order of magnitude faster on a slider
329
+ * puzzle, and it will be detected by anything that scores pointer telemetry —
330
+ * which is the whole trade, stated plainly. For fixtures, for self-hosted
331
+ * targets, and for callers whose stack humanises somewhere else.
332
+ *
333
+ * Still moves the mouse and still presses and releases: the events are what make
334
+ * the widget respond at all, and a click dispatched with no preceding move fails
335
+ * on the vendors that require a hover state first.
336
+ */
337
+ export declare class NullHumanizer extends BaseHumanizer {
338
+ readonly name = "none";
339
+ readonly hovers = false;
340
+ protected readonly pauses: PauseTable;
341
+ move(page: Page, to: Point): Promise<void>;
342
+ press(page: Page): Promise<void>;
343
+ release(page: Page): Promise<void>;
344
+ typeText(page: Page, field: PlaywrightElementHandle, text: string): Promise<boolean>;
345
+ }
346
+ export type HumanizationMode = 'mouse' | 'mobile' | 'none';
347
+ export declare const MODES: readonly HumanizationMode[];
348
+ /** The subset of `CaptchaKrakenConfig` this module reads. */
349
+ export interface HumanizerOptions {
350
+ humanization?: HumanizationMode;
351
+ humanizer?: Humanizer;
352
+ touchDriver?: any;
353
+ touchTransform?: TouchTransform;
354
+ startingMousePosition?: {
355
+ x: number;
356
+ y: number;
357
+ };
358
+ }
359
+ /**
360
+ * The humanizer one solver will use, from its config.
361
+ *
362
+ * Precedence, and the reasoning for it:
363
+ *
364
+ * 1. `config.humanizer` — a caller who handed us an OBJECT has already
365
+ * decided; there is nothing left to select.
366
+ * 2. `config.humanization` — an explicit mode set in code.
367
+ * 3. `CAPTCHA_HUMANIZATION` — for a caller who cannot edit the code.
368
+ * 4. 'mouse'.
369
+ *
370
+ * Note that the env var loses to code, which is the opposite of this package's
371
+ * model-identity settings. Deliberate: which mode is right is a property of the
372
+ * PAGE the caller is driving, and an env var flipping a desktop solve to touch
373
+ * dispatch would break every one of them silently. Pinning a model is a
374
+ * deployment decision; picking an input device is not.
375
+ */
376
+ export declare function resolveHumanizer(config?: HumanizerOptions): Humanizer;
377
+ export {};