captchakraken 2.6.0 → 2.8.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/README.md +126 -16
- package/dist/cli-invocation.d.ts +2 -0
- package/dist/cli-invocation.js +5 -1
- package/dist/errors.d.ts +1 -1
- package/dist/humanize.d.ts +377 -0
- package/dist/humanize.js +716 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +44 -1
- package/dist/playwright-types.d.ts +18 -0
- package/dist/solver.d.ts +115 -5
- package/dist/solver.js +583 -190
- package/dist/timing.d.ts +48 -0
- package/dist/timing.js +103 -0
- package/dist/trajectory.d.ts +27 -0
- package/dist/trajectory.js +104 -0
- package/dist/types.d.ts +129 -0
- package/package.json +11 -3
- package/python/README.md +76 -5
- package/python/pyproject.toml +21 -4
- package/python/src/captchakraken/__init__.py +36 -1
- package/python/src/captchakraken/cli.py +62 -4
- package/python/src/captchakraken/config.py +28 -2
- package/python/src/captchakraken/errors.py +24 -0
- package/python/src/captchakraken/humanize.py +929 -0
- package/python/src/captchakraken/keyframes.py +99 -0
- package/python/src/captchakraken/models.json +149 -13
- package/python/src/captchakraken/page_solver.py +588 -291
- package/python/src/captchakraken/pinned_model.json +3 -2
- package/python/src/captchakraken/planner.py +118 -9
- package/python/src/captchakraken/prompts.py +182 -0
- package/python/src/captchakraken/server_manager.py +9 -4
- package/python/src/captchakraken/solver.py +97 -57
- package/python/src/captchakraken/timing.py +8 -3
- package/python/src/captchakraken/tool_calls/find_grid.py +357 -20
- package/python/src/captchakraken/trajectory.py +117 -0
- package/python/src/captchakraken/updater.py +62 -1
package/README.md
CHANGED
|
@@ -1,13 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/logo-card.png" alt="CaptchaKraken" width="128" height="128">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
<h1 align="center">captchakraken</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>A captcha solver for browser automation.</b><br>
|
|
9
|
+
The TypeScript driver for <a href="https://github.com/JWriter20/CaptchaKraken">CaptchaKraken</a>.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
Hand it a Playwright or Puppeteer `Page`. It finds the captcha, reads the whole
|
|
13
|
+
puzzle with a fine-tuned **Qwen3.5-9B** vision model, and clicks, drags, slides
|
|
14
|
+
or types human-like through to a token.
|
|
15
|
+
|
|
16
|
+
Run the model on **your own hardware**, or point it at the **hosted API** and run
|
|
17
|
+
nothing at all.
|
|
7
18
|
|
|
8
19
|
> Full docs — demo videos, accuracy, self-hosting — live in the main repo
|
|
9
20
|
> **[CaptchaKraken](https://github.com/JWriter20/CaptchaKraken)**.
|
|
10
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
|
+
|
|
11
42
|
## Install
|
|
12
43
|
|
|
13
44
|
```bash
|
|
@@ -15,9 +46,9 @@ npm install captchakraken
|
|
|
15
46
|
```
|
|
16
47
|
|
|
17
48
|
The package bundles the Python engine (`captchakraken`) and, on `postinstall`,
|
|
18
|
-
creates a local venv with its lightweight core deps so
|
|
19
|
-
|
|
20
|
-
Playwright-compatible launcher.
|
|
49
|
+
creates a local venv with its lightweight core deps, so tile detection and the
|
|
50
|
+
inference client work out of the box. It ships **no browser** — bring your own
|
|
51
|
+
Playwright-compatible launcher (camoufox and patchright both work).
|
|
21
52
|
|
|
22
53
|
- Skip the Python bootstrap: `CAPTCHA_KRAKEN_SKIP_PYTHON_SETUP=1`
|
|
23
54
|
- To **self-host** the model, run the repo's `setup.sh` (installs vLLM + weights).
|
|
@@ -37,6 +68,22 @@ claude mcp add captchakraken -- npx -y captchakraken-mcp
|
|
|
37
68
|
`create_api_key` writes the key and the endpoint to `~/.captchakraken/credentials`,
|
|
38
69
|
which the client reads on its own — **no environment variables needed**.
|
|
39
70
|
|
|
71
|
+
## What it solves
|
|
72
|
+
|
|
73
|
+
| Vendor | Puzzles |
|
|
74
|
+
|---|---|
|
|
75
|
+
| **reCAPTCHA** | 3×3 and 4×4 image grids, including the dynamic re-deal |
|
|
76
|
+
| **hCaptcha** | Image grids, click, drag, connect-the-path, tetris-fit, animated |
|
|
77
|
+
| **GeeTest** v3 + v4 | Slide, icon, nine, svg, gobang, iconcrush |
|
|
78
|
+
| **NetEase Yidun** | Jigsaw, picture-click, icon-click |
|
|
79
|
+
| **Tencent, Lemin, Prosopo** | Slide, cropped-image and grid flows |
|
|
80
|
+
| **BotDetect, MTCaptcha, Yandex** | Distorted text — read and typed, not clicked |
|
|
81
|
+
| **Cloudflare Turnstile** | Via the checkbox flow (free on the hosted API) |
|
|
82
|
+
|
|
83
|
+
**44 puzzle types**, driven end to end in CI against generated fixtures on both
|
|
84
|
+
the TypeScript and Python ports. Animated challenges are recorded, sliced into
|
|
85
|
+
keyframes and answered with the frame the action belongs to.
|
|
86
|
+
|
|
40
87
|
## Usage
|
|
41
88
|
|
|
42
89
|
```typescript
|
|
@@ -49,22 +96,85 @@ await page.goto('https://www.google.com/recaptcha/api2/demo');
|
|
|
49
96
|
|
|
50
97
|
// Reads VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY from the environment.
|
|
51
98
|
const solver = new CaptchaKrakenSolver();
|
|
52
|
-
await solver.solve(page); // detect →
|
|
99
|
+
await solver.solve(page); // detect → read → act → verify
|
|
53
100
|
|
|
54
101
|
await browser.close();
|
|
55
102
|
```
|
|
56
103
|
|
|
57
104
|
Puppeteer users wrap the page once with `fromPuppeteer(page)`.
|
|
58
105
|
|
|
59
|
-
##
|
|
106
|
+
## How it moves — mouse, mobile, none, or yours
|
|
60
107
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
65
155
|
|
|
66
|
-
|
|
67
|
-
|
|
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).
|
|
68
178
|
|
|
69
179
|
## License
|
|
70
180
|
|
package/dist/cli-invocation.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface SolveInvocation {
|
|
|
28
28
|
puzzleSource: string;
|
|
29
29
|
retryMode?: string | null;
|
|
30
30
|
textMode?: boolean;
|
|
31
|
+
/** One expert of a routed model, or undefined to route by prompt family. */
|
|
32
|
+
expert?: string | null;
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
35
|
* The argv for one solve. No shell, no quoting, and NO CREDENTIAL — the key is
|
package/dist/cli-invocation.js
CHANGED
|
@@ -33,7 +33,7 @@ exports.API_PROVIDER = 'captchaKrakenApi';
|
|
|
33
33
|
* supplied by `solveEnv` instead.
|
|
34
34
|
*/
|
|
35
35
|
function buildSolveArgs(invocation) {
|
|
36
|
-
const { imagePath, model, puzzleSource, retryMode, textMode } = invocation;
|
|
36
|
+
const { imagePath, model, puzzleSource, retryMode, textMode, expert } = invocation;
|
|
37
37
|
const args = [
|
|
38
38
|
'-m',
|
|
39
39
|
'captchakraken.cli',
|
|
@@ -46,6 +46,10 @@ function buildSolveArgs(invocation) {
|
|
|
46
46
|
args.push(`--retry-mode=${retryMode}`);
|
|
47
47
|
if (textMode)
|
|
48
48
|
args.push('--text-mode');
|
|
49
|
+
// Only when set. An absent flag is what lets the Python side route by prompt
|
|
50
|
+
// family, and passing `--expert=` would be an empty choice rather than none.
|
|
51
|
+
if (expert)
|
|
52
|
+
args.push(`--expert=${expert}`);
|
|
49
53
|
return args;
|
|
50
54
|
}
|
|
51
55
|
/** Base env plus the credential, when there is one. */
|
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 `tests/test_humanizer_parity.py` in
|
|
6
|
+
* CaptchaKrakenFinetune 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 geetest_v4_slide 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 {};
|