captchakraken 2.8.0 → 3.0.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 (77) hide show
  1. package/AGENTS.md +112 -0
  2. package/NOTICE +36 -0
  3. package/dist/cli-invocation.d.ts +9 -40
  4. package/dist/cli-invocation.js +16 -49
  5. package/dist/errors.d.ts +7 -27
  6. package/dist/errors.js +6 -25
  7. package/dist/humanize.d.ts +56 -240
  8. package/dist/humanize.js +159 -415
  9. package/dist/index.d.ts +5 -129
  10. package/dist/index.js +4 -98
  11. package/dist/kinds.d.ts +135 -0
  12. package/dist/kinds.js +70 -0
  13. package/dist/limits.d.ts +0 -48
  14. package/dist/limits.js +2 -48
  15. package/dist/model-name.d.ts +4 -19
  16. package/dist/model-name.js +32 -40
  17. package/dist/playwright-types.d.ts +25 -95
  18. package/dist/playwright-types.js +0 -23
  19. package/dist/puppeteer-adapter.d.ts +4 -43
  20. package/dist/puppeteer-adapter.js +22 -58
  21. package/dist/python-command.d.ts +0 -41
  22. package/dist/python-command.js +0 -37
  23. package/dist/selectors.d.ts +54 -0
  24. package/dist/selectors.js +117 -0
  25. package/dist/slide-geometry.d.ts +2 -29
  26. package/dist/slide-geometry.js +18 -44
  27. package/dist/solver.d.ts +112 -539
  28. package/dist/solver.js +1088 -2950
  29. package/dist/timing.d.ts +7 -40
  30. package/dist/timing.js +4 -36
  31. package/dist/token-usage.d.ts +1 -7
  32. package/dist/token-usage.js +10 -95
  33. package/dist/types.d.ts +77 -483
  34. package/dist/watcher.d.ts +5 -78
  35. package/dist/watcher.js +0 -56
  36. package/package.json +12 -4
  37. package/python/AGENTS.md +122 -0
  38. package/python/NOTICE +36 -0
  39. package/python/README.md +1 -2
  40. package/python/examples/README.md +10 -0
  41. package/python/examples/_harness.py +4 -73
  42. package/python/examples/demoHcaptcha.py +0 -13
  43. package/python/examples/demoRecaptcha.py +0 -10
  44. package/python/examples/watch_playwright.py +1 -26
  45. package/python/examples/with_local_gguf.py +28 -0
  46. package/python/examples/with_playwright.py +0 -19
  47. package/python/pyproject.toml +68 -19
  48. package/python/src/captchakraken/__init__.py +37 -45
  49. package/python/src/captchakraken/action_types.py +14 -36
  50. package/python/src/captchakraken/cli.py +262 -879
  51. package/python/src/captchakraken/config.py +60 -146
  52. package/python/src/captchakraken/errors.py +17 -95
  53. package/python/src/captchakraken/humanize.py +174 -597
  54. package/python/src/captchakraken/image_processor.py +16 -235
  55. package/python/src/captchakraken/keyframes.py +69 -434
  56. package/python/src/captchakraken/kinds.py +152 -0
  57. package/python/src/captchakraken/models.json +79 -21
  58. package/python/src/captchakraken/overlay.py +65 -498
  59. package/python/src/captchakraken/page_solver.py +1123 -2564
  60. package/python/src/captchakraken/planner.py +70 -428
  61. package/python/src/captchakraken/prompts.py +45 -261
  62. package/python/src/captchakraken/selectors.py +184 -0
  63. package/python/src/captchakraken/server_manager.py +7 -58
  64. package/python/src/captchakraken/solver.py +128 -522
  65. package/python/src/captchakraken/timing.py +20 -74
  66. package/python/src/captchakraken/tool_calls/board_painted.py +47 -0
  67. package/python/src/captchakraken/tool_calls/find_checkbox.py +20 -30
  68. package/python/src/captchakraken/tool_calls/find_grid.py +186 -1339
  69. package/python/src/captchakraken/tool_calls/track_piece.py +81 -57
  70. package/python/src/captchakraken/updater.py +7 -76
  71. package/python/src/captchakraken/watcher.py +12 -101
  72. package/scripts/copy-python.mjs +10 -10
  73. package/scripts/setup-python.js +1 -21
  74. package/dist/trajectory.d.ts +0 -58
  75. package/dist/trajectory.js +0 -266
  76. package/python/src/captchakraken/tool_calls/move_indicator.py +0 -431
  77. package/python/src/captchakraken/trajectory.py +0 -284
@@ -4,41 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getBundledCliRoot = getBundledCliRoot;
7
+ exports.isHostedEndpoint = isHostedEndpoint;
7
8
  exports.resolveLoraName = resolveLoraName;
8
- /**
9
- * Which served LoRA name this client asks for — the SAME answer the Python
10
- * client's `config.lora_name()` gives, and for the same reasons.
11
- *
12
- * This has to be shared, not re-decided per port. The name is not just routing:
13
- * the Python CLI feeds it to `prompts.resolve()`, which maps it through
14
- * models.json to a PROMPT GENERATION. A port that picks a different name sends
15
- * a different generation of prompts to the same weights, and nothing errors for
16
- * any family both generations have a prompt for — it just answers worse. That
17
- * is the failure models.json exists to prevent, and the JS port shipped it:
18
- * a hardcoded `'captcha'` (CaptchaKraken_v1.1, generation 1) against the
19
- * generation-2 adapter `latest` has named since v1.2.
20
- *
21
- * Precedence mirrors config.py exactly:
22
- * 1. CAPTCHA_LORA_NAME — an explicit pin always wins, and pinning is opt-in.
23
- * 2. models.json's `latest` entry, so the default model and the default
24
- * prompt generation move forward together and cannot get out of step.
25
- * 3. pinned_model.json, so a hand-edited or older manifest still resolves.
26
- */
27
9
  const node_fs_1 = __importDefault(require("node:fs"));
28
10
  const node_path_1 = __importDefault(require("node:path"));
29
- /**
30
- * Where the bundled Python engine lives — the copy of `models.json` this client
31
- * actually answers from.
32
- *
33
- * Lives HERE rather than in solver.ts because the registry lookup below is its
34
- * only remaining caller-visible use, and a second copy of this path logic is
35
- * how the two ports drifted apart in the first place. solver.ts imports it.
36
- *
37
- * When installed from npm this file is in `<pkgRoot>/dist` (compiled) or
38
- * `<pkgRoot>/src` (dev), and published packages bundle the engine at
39
- * `<pkgRoot>/python` (copied in by scripts/copy-python.mjs at build time). In
40
- * the source monorepo it instead lives at the sibling `../python`.
41
- */
42
11
  function getBundledCliRoot() {
43
12
  const bundled = node_path_1.default.resolve(__dirname, '..', 'python');
44
13
  if (node_fs_1.default.existsSync(bundled))
@@ -48,23 +17,46 @@ function getBundledCliRoot() {
48
17
  function readJson(cliRoot, name) {
49
18
  return JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(cliRoot, 'src', 'captchakraken', name), 'utf-8'));
50
19
  }
51
- /**
52
- * `cliRoot` is optional so a caller that only wants to REPORT the name — a test
53
- * harness recording which adapter it drove — can ask without reconstructing the
54
- * engine's layout. Re-deriving it is the same mistake one level down.
55
- */
56
- function resolveLoraName({ cliRoot = getBundledCliRoot(), env = process.env } = {}) {
20
+ // An exact host list, not "is it remote": a self-hoster's vLLM across the network is remote too, and the hosted-only model 404s there.
21
+ const HOSTED_HOSTS = ['api.captchakraken.com'];
22
+ function isHostedEndpoint(baseUrl, env = process.env) {
23
+ if (!baseUrl)
24
+ return false;
25
+ const extra = (env.CAPTCHA_HOSTED_HOSTS ?? '')
26
+ .split(',').map((h) => h.trim().toLowerCase()).filter(Boolean);
27
+ try {
28
+ return [...HOSTED_HOSTS, ...extra].includes(new URL(baseUrl).hostname.toLowerCase());
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ /** Hosted: the routing alias, not an arm's `lora_name`, because a routed mixture is several names and only the alias routes. A missing or broken registry falls through to the pin, never throws. */
35
+ function resolveLoraName({ cliRoot = getBundledCliRoot(), env = process.env, baseUrl } = {}) {
57
36
  if (env.CAPTCHA_LORA_NAME)
58
37
  return env.CAPTCHA_LORA_NAME;
59
38
  try {
60
39
  const reg = readJson(cliRoot, 'models.json');
40
+ if (isHostedEndpoint(baseUrl, env)) {
41
+ const repo = reg?.hosted_default;
42
+ if (typeof repo === 'string' && repo) {
43
+ const aliases = Object.entries(reg?.served_aliases ?? {})
44
+ .filter(([a, target]) => target === repo && !a.startsWith('_'))
45
+ .map(([a]) => a);
46
+ const routed = aliases.find((a) => {
47
+ const id = (reg?.served_aliases ?? {})[a];
48
+ return Object.keys(reg?.models?.[id]?.experts ?? {}).length > 0;
49
+ });
50
+ const picked = routed ?? aliases[0] ?? reg?.models?.[repo]?.lora_name;
51
+ if (typeof picked === 'string' && picked)
52
+ return picked;
53
+ }
54
+ }
61
55
  const name = reg?.models?.[reg?.latest]?.lora_name;
62
56
  if (typeof name === 'string' && name)
63
57
  return name;
64
58
  }
65
59
  catch {
66
- // A missing or broken registry falls through to the pin, never throws —
67
- // same contract as config.py's `_registry_default`.
68
60
  }
69
61
  return readJson(cliRoot, 'pinned_model.json').lora_name;
70
62
  }
@@ -1,95 +1,61 @@
1
- /**
2
- * Minimal, implementation-neutral structural types for the slice of the
3
- * Playwright API the solver actually uses.
4
- *
5
- * **Why these exist.** CaptchaKraken never launches a browser — you bring your
6
- * own Playwright-compatible launcher and hand `solve()` a `Page`. To stay truly
7
- * browser-agnostic the package depends on NO concrete browser implementation
8
- * (not `playwright`, not `patchright`, not `camoufox-js`). Typing the public API
9
- * against any one of those packages' `Page` would (a) force that package into
10
- * consumers' trees and (b) break across version skew — e.g. `patchright` pins
11
- * its own forked, older core whose `Page` is missing the newest `playwright-core`
12
- * methods, so a `patchright` page won't structurally match a `playwright-core`
13
- * page and vice-versa.
14
- *
15
- * Instead we declare a duck-typed surface covering only what the solver calls.
16
- * Every real Playwright `Page` / `Frame` / `ElementHandle` — from vanilla
17
- * `playwright`, `patchright`, `camoufox-js`, or anything else — structurally
18
- * satisfies these, regardless of which Playwright version it was built against.
19
- *
20
- * Keep these in sync with `solver.ts`: if the solver starts calling a new
21
- * Playwright method, add it here. The set is intentionally small and stable —
22
- * these are long-standing Playwright primitives, not bleeding-edge additions.
23
- */
24
- /** `{ x, y, width, height }` in CSS pixels — the shape `boundingBox()` returns. */
25
1
  export interface BoundingBoxRect {
26
2
  x: number;
27
3
  y: number;
28
4
  width: number;
29
5
  height: number;
30
6
  }
31
- /** `{ width, height }` — the shape `viewportSize()` returns. */
32
7
  export interface ViewportSize {
33
8
  width: number;
34
9
  height: number;
35
10
  }
36
- /**
37
- * Structural subset of Playwright's `ElementHandle`. Returned by `Page.$` /
38
- * `Frame.$` / `Page.waitForSelector` and accepted by the solver as the captcha
39
- * element / verify button / iframe handle.
40
- */
41
11
  export interface PlaywrightElementHandle {
42
- /** Screenshot just this element to a PNG file (the only option the solver passes). */
43
12
  screenshot(options?: {
44
13
  path?: string;
45
14
  timeout?: number;
46
15
  animations?: 'disabled' | 'allow';
47
16
  }): Promise<Buffer>;
48
- /** The content document of an `<iframe>` element handle, or null if not a frame. */
49
17
  contentFrame(): Promise<PlaywrightFrame | null>;
50
- /** Element box in page CSS pixels, or null if not rendered. */
51
18
  boundingBox(): Promise<BoundingBoxRect | null>;
52
- /** Scroll the element into view if it isn't already. */
53
19
  scrollIntoViewIfNeeded(options?: {
54
20
  timeout?: number;
55
21
  }): Promise<void>;
56
- /** Read an attribute, or null if absent. */
57
22
  getAttribute(name: string): Promise<string | null>;
58
- /** Whether the element is visible. */
59
23
  isVisible(): Promise<boolean>;
60
- /** The element's text content, or null. */
61
24
  textContent(): Promise<string | null>;
62
- /**
63
- * First matching element WITHIN this element's subtree, or null. The vendors
64
- * that render into the host page rather than an iframe (GeeTest, Yidun,
65
- * BotDetect, …) have no Frame to scope against, so the widget element is the
66
- * boundary that keeps a generic selector off the rest of the document.
67
- */
68
- $(selector: string): Promise<PlaywrightElementHandle | null>;
25
+ inputValue(): Promise<string>;
26
+ evaluate<R>(pageFunction: (element: Element) => R): Promise<R>;
69
27
  }
70
- /**
71
- * Structural subset of Playwright's `Frame` (an iframe's content document).
72
- */
73
- export interface PlaywrightFrame {
74
- /** First matching element handle, or null. */
75
- $(selector: string): Promise<PlaywrightElementHandle | null>;
76
- /** Wait for a selector to reach the given state; resolves to the handle (or null). */
28
+ /** The subset of Playwright's Locator the driver needs. `filter({ visible })` is Playwright 1.51+. */
29
+ export interface PlaywrightLocator {
30
+ locator(selector: string): PlaywrightLocator;
31
+ filter(options: {
32
+ visible?: boolean;
33
+ }): PlaywrightLocator;
34
+ all(): Promise<PlaywrightLocator[]>;
35
+ count(): Promise<number>;
36
+ elementHandle(options?: {
37
+ timeout?: number;
38
+ }): Promise<PlaywrightElementHandle | null>;
39
+ }
40
+ /** Anything selectors can be run inside: a page, a frame, or the locator of an inline widget. */
41
+ export interface PlaywrightScope {
42
+ locator(selector: string): PlaywrightLocator;
43
+ }
44
+ export interface PlaywrightFrame extends PlaywrightScope {
77
45
  waitForSelector(selector: string, options?: {
78
46
  state?: 'attached' | 'detached' | 'visible' | 'hidden';
79
47
  timeout?: number;
80
48
  }): Promise<PlaywrightElementHandle | null>;
81
- /** Poll a predicate evaluated in the page until it returns truthy. */
82
49
  waitForFunction(pageFunction: Function | string, arg?: any, options?: {
83
50
  timeout?: number;
84
51
  polling?: number | 'raf';
85
52
  }): Promise<unknown>;
86
53
  }
87
54
  /**
88
- * Structural subset of Playwright's `Page` — exactly the members `solve()` and
89
- * its helpers call. Any real Playwright `Page` satisfies this.
55
+ * Structural on purpose: the package depends on no browser library, and the version skew between
56
+ * playwright, patchright and camoufox-js makes a nominal import the wrong one for someone.
90
57
  */
91
- export interface PlaywrightPage {
92
- /** Low-level mouse control used to drive human-like trajectories and clicks. */
58
+ export interface PlaywrightPage extends PlaywrightScope {
93
59
  mouse: {
94
60
  move(x: number, y: number, options?: {
95
61
  steps?: number;
@@ -103,11 +69,7 @@ export interface PlaywrightPage {
103
69
  clickCount?: number;
104
70
  }): Promise<void>;
105
71
  };
106
- /**
107
- * Key input. Used to type a distorted-text captcha's answer character by
108
- * character — `fill()` would set the value with no keystrokes at all, and
109
- * these are the vendors that score typing cadence.
110
- */
72
+ /** `type` is called per character by the humanizer; a constant `delay` would itself be a signal. */
111
73
  keyboard: {
112
74
  type(text: string, options?: {
113
75
  delay?: number;
@@ -116,52 +78,20 @@ export interface PlaywrightPage {
116
78
  delay?: number;
117
79
  }): Promise<void>;
118
80
  };
119
- /** Sleep `timeout` ms (Playwright's own timer). */
120
81
  waitForTimeout(timeout: number): Promise<void>;
121
- /** Wait for a selector to reach the given state; resolves to the handle (or null). */
122
82
  waitForSelector(selector: string, options?: {
123
83
  state?: 'attached' | 'detached' | 'visible' | 'hidden';
124
84
  timeout?: number;
125
85
  }): Promise<PlaywrightElementHandle | null>;
126
- /** Current viewport size, or null if not set. */
127
86
  viewportSize(): ViewportSize | null;
128
- /**
129
- * The browser context this page belongs to. OPTIONAL because only
130
- * `humanization: 'mobile'` needs it, and then only to open a CDP session for
131
- * `Input.dispatchTouchEvent` — the mouse and none modes never touch it. A
132
- * launcher that does not expose it simply cannot serve mobile mode, and says
133
- * so at construction rather than mid-solve.
134
- */
87
+ /** Optional: the mouse humanizer asks the window for its size when `viewportSize()` is null (camoufox). */
88
+ evaluate?<R>(pageFunction: () => R): Promise<R>;
135
89
  context?(): {
136
90
  newCDPSession(page: PlaywrightPage): Promise<any>;
137
91
  };
138
- /**
139
- * Playwright's touchscreen, present when the context was created with
140
- * `hasTouch: true`. OPTIONAL, and tap-only: it is the fallback for a browser
141
- * with touch support but no CDP, and cannot express a drag.
142
- */
143
92
  touchscreen?: {
144
93
  tap(x: number, y: number): Promise<void>;
145
94
  };
146
- /** First matching element handle, or null. */
147
- $(selector: string): Promise<PlaywrightElementHandle | null>;
148
- /** All matching element handles. */
149
- $$(selector: string): Promise<PlaywrightElementHandle[]>;
150
- /**
151
- * Whether the page has been closed. OPTIONAL because it is the one member
152
- * here the solver itself never needs — the auto-solve watcher uses it to end
153
- * its loop when the caller closes the browser out from under it, and falls
154
- * back to treating the page as open when a launcher does not expose it.
155
- * Playwright and Puppeteer both do.
156
- */
157
95
  isClosed?(): boolean;
158
- /** Run `pageFunction` against the first matching element, in the page context. */
159
- $eval<R>(selector: string, pageFunction: (element: Element) => R, arg?: any): Promise<R>;
160
96
  }
161
- /**
162
- * Public alias. This is the type the solver's `solve(page)` accepts — kept under
163
- * a friendly name so consumers can annotate against it. It is the
164
- * implementation-neutral Playwright `Page`; pass a page from whichever
165
- * Playwright-compatible launcher you prefer.
166
- */
167
97
  export type Page = PlaywrightPage;
@@ -1,25 +1,2 @@
1
1
  "use strict";
2
- /**
3
- * Minimal, implementation-neutral structural types for the slice of the
4
- * Playwright API the solver actually uses.
5
- *
6
- * **Why these exist.** CaptchaKraken never launches a browser — you bring your
7
- * own Playwright-compatible launcher and hand `solve()` a `Page`. To stay truly
8
- * browser-agnostic the package depends on NO concrete browser implementation
9
- * (not `playwright`, not `patchright`, not `camoufox-js`). Typing the public API
10
- * against any one of those packages' `Page` would (a) force that package into
11
- * consumers' trees and (b) break across version skew — e.g. `patchright` pins
12
- * its own forked, older core whose `Page` is missing the newest `playwright-core`
13
- * methods, so a `patchright` page won't structurally match a `playwright-core`
14
- * page and vice-versa.
15
- *
16
- * Instead we declare a duck-typed surface covering only what the solver calls.
17
- * Every real Playwright `Page` / `Frame` / `ElementHandle` — from vanilla
18
- * `playwright`, `patchright`, `camoufox-js`, or anything else — structurally
19
- * satisfies these, regardless of which Playwright version it was built against.
20
- *
21
- * Keep these in sync with `solver.ts`: if the solver starts calling a new
22
- * Playwright method, add it here. The set is intentionally small and stable —
23
- * these are long-standing Playwright primitives, not bleeding-edge additions.
24
- */
25
2
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,38 +1,3 @@
1
- /**
2
- * Puppeteer → CaptchaKraken adapter.
3
- *
4
- * The solver speaks the Playwright API surface (see playwright-types.ts). Puppeteer
5
- * is ~95% the same shape but differs in a handful of method names/options. Rather
6
- * than couple the solver to either library, this thin adapter wraps a Puppeteer
7
- * `Page` (and, lazily, the `Frame`/`ElementHandle` objects it hands back) so it
8
- * satisfies the structural `PlaywrightPage` interface the solver consumes.
9
- *
10
- * The deltas it bridges (verified against Puppeteer 24.x):
11
- * - `page.viewportSize()` → `page.viewport()` (same `{width,height}` shape)
12
- * - `page.waitForTimeout(ms)` → removed in modern Puppeteer; use a timer
13
- * - `*.waitForSelector(sel, {state}) → Puppeteer uses `{visible}/{hidden}`
14
- * - `handle.getAttribute(name)` → `handle.evaluate((el,n)=>el.getAttribute(n), name)`
15
- * - `handle.textContent()` → `handle.evaluate(el=>el.textContent)`
16
- * - `handle.scrollIntoViewIfNeeded()`→ `handle.scrollIntoView()`
17
- * - `page.isClosed()` → same, but must be forwarded explicitly
18
- * Everything else (`$`, `$$`, `$eval`, `waitForFunction`, `mouse.*`,
19
- * `screenshot({path})`, `boundingBox`, `contentFrame`, `isVisible`) is
20
- * call-compatible and passed straight through.
21
- *
22
- * Usage:
23
- * ```typescript
24
- * import puppeteer from 'puppeteer';
25
- * import { CaptchaKrakenSolver, fromPuppeteer } from 'captchakraken';
26
- *
27
- * const browser = await puppeteer.launch({ headless: false });
28
- * const page = await browser.newPage();
29
- * await page.goto('https://www.google.com/recaptcha/api2/demo');
30
- *
31
- * const solver = new CaptchaKrakenSolver();
32
- * await solver.solve(fromPuppeteer(page));
33
- * await browser.close();
34
- * ```
35
- */
36
1
  import { PlaywrightPage, BoundingBoxRect, ViewportSize } from './playwright-types';
37
2
  interface PuppeteerElementHandle {
38
3
  screenshot(options?: {
@@ -43,10 +8,10 @@ interface PuppeteerElementHandle {
43
8
  scrollIntoView(): Promise<void>;
44
9
  isVisible(): Promise<boolean>;
45
10
  evaluate(pageFunction: (el: any, ...args: any[]) => any, ...args: any[]): Promise<any>;
46
- $(selector: string): Promise<PuppeteerElementHandle | null>;
11
+ $$(selector: string): Promise<PuppeteerElementHandle[]>;
47
12
  }
48
13
  interface PuppeteerFrame {
49
- $(selector: string): Promise<PuppeteerElementHandle | null>;
14
+ $$(selector: string): Promise<PuppeteerElementHandle[]>;
50
15
  waitForSelector(selector: string, options?: any): Promise<PuppeteerElementHandle | null>;
51
16
  waitForFunction(pageFunction: Function | string, options?: any, ...args: any[]): Promise<unknown>;
52
17
  }
@@ -68,14 +33,10 @@ interface PuppeteerPage {
68
33
  };
69
34
  waitForSelector(selector: string, options?: any): Promise<PuppeteerElementHandle | null>;
70
35
  viewport(): ViewportSize | null;
71
- $(selector: string): Promise<PuppeteerElementHandle | null>;
36
+ evaluate<R>(pageFunction: () => R): Promise<R>;
72
37
  $$(selector: string): Promise<PuppeteerElementHandle[]>;
73
- $eval(selector: string, pageFunction: (element: Element) => any, ...args: any[]): Promise<any>;
74
38
  isClosed(): boolean;
39
+ evaluate(pageFunction: () => any): Promise<any>;
75
40
  }
76
- /**
77
- * Wrap a Puppeteer `Page` so it satisfies the solver's structural Playwright
78
- * `Page`. Pass the result to `solver.solve(...)`.
79
- */
80
41
  export declare function fromPuppeteer(page: PuppeteerPage): PlaywrightPage;
81
42
  export {};
@@ -1,42 +1,6 @@
1
1
  "use strict";
2
- /**
3
- * Puppeteer → CaptchaKraken adapter.
4
- *
5
- * The solver speaks the Playwright API surface (see playwright-types.ts). Puppeteer
6
- * is ~95% the same shape but differs in a handful of method names/options. Rather
7
- * than couple the solver to either library, this thin adapter wraps a Puppeteer
8
- * `Page` (and, lazily, the `Frame`/`ElementHandle` objects it hands back) so it
9
- * satisfies the structural `PlaywrightPage` interface the solver consumes.
10
- *
11
- * The deltas it bridges (verified against Puppeteer 24.x):
12
- * - `page.viewportSize()` → `page.viewport()` (same `{width,height}` shape)
13
- * - `page.waitForTimeout(ms)` → removed in modern Puppeteer; use a timer
14
- * - `*.waitForSelector(sel, {state}) → Puppeteer uses `{visible}/{hidden}`
15
- * - `handle.getAttribute(name)` → `handle.evaluate((el,n)=>el.getAttribute(n), name)`
16
- * - `handle.textContent()` → `handle.evaluate(el=>el.textContent)`
17
- * - `handle.scrollIntoViewIfNeeded()`→ `handle.scrollIntoView()`
18
- * - `page.isClosed()` → same, but must be forwarded explicitly
19
- * Everything else (`$`, `$$`, `$eval`, `waitForFunction`, `mouse.*`,
20
- * `screenshot({path})`, `boundingBox`, `contentFrame`, `isVisible`) is
21
- * call-compatible and passed straight through.
22
- *
23
- * Usage:
24
- * ```typescript
25
- * import puppeteer from 'puppeteer';
26
- * import { CaptchaKrakenSolver, fromPuppeteer } from 'captchakraken';
27
- *
28
- * const browser = await puppeteer.launch({ headless: false });
29
- * const page = await browser.newPage();
30
- * await page.goto('https://www.google.com/recaptcha/api2/demo');
31
- *
32
- * const solver = new CaptchaKrakenSolver();
33
- * await solver.solve(fromPuppeteer(page));
34
- * await browser.close();
35
- * ```
36
- */
37
2
  Object.defineProperty(exports, "__esModule", { value: true });
38
3
  exports.fromPuppeteer = fromPuppeteer;
39
- /** Translate Playwright `{state}` selector options to Puppeteer `{visible}/{hidden}`. */
40
4
  function toPuppeteerSelectorOptions(options) {
41
5
  if (!options)
42
6
  return undefined;
@@ -48,10 +12,22 @@ function toPuppeteerSelectorOptions(options) {
48
12
  out.visible = true;
49
13
  else if (state === 'hidden')
50
14
  out.hidden = true;
51
- // 'attached'/'detached' have no direct Puppeteer flag — default wait (attached)
52
- // is the closest match, so we pass no visibility flag.
53
15
  return out;
54
16
  }
17
+ /** Puppeteer's own Locator has no `all()`, `count()` or visibility filter, so a Playwright-shaped one is built over `$$`. */
18
+ function locatorOver(resolve) {
19
+ return {
20
+ locator: (selector) => locatorOver(async () => (await Promise.all((await resolve()).map((h) => h.$$(selector)))).flat()),
21
+ filter: ({ visible }) => locatorOver(async () => {
22
+ const found = await resolve();
23
+ const shown = await Promise.all(found.map((h) => (visible === undefined ? true : h.isVisible().then((v) => v === visible))));
24
+ return found.filter((_, i) => shown[i]);
25
+ }),
26
+ all: async () => (await resolve()).map((h) => locatorOver(async () => [h])),
27
+ count: async () => (await resolve()).length,
28
+ elementHandle: async () => wrapHandle((await resolve())[0] ?? null),
29
+ };
30
+ }
55
31
  function wrapHandle(h) {
56
32
  if (!h)
57
33
  return null;
@@ -63,24 +39,19 @@ function wrapHandle(h) {
63
39
  getAttribute: (name) => h.evaluate((el, n) => el.getAttribute(n), name),
64
40
  isVisible: () => h.isVisible(),
65
41
  textContent: () => h.evaluate((el) => el.textContent),
66
- $: async (selector) => wrapHandle(await h.$(selector)),
42
+ inputValue: () => h.evaluate((el) => (typeof el.value === 'string' ? el.value : '')),
43
+ evaluate: (pageFunction) => h.evaluate(pageFunction),
67
44
  };
68
45
  }
69
46
  function wrapFrame(f) {
70
47
  if (!f)
71
48
  return null;
72
49
  return {
73
- $: async (selector) => wrapHandle(await f.$(selector)),
50
+ locator: (selector) => locatorOver(() => f.$$(selector)),
74
51
  waitForSelector: async (selector, options) => wrapHandle(await f.waitForSelector(selector, toPuppeteerSelectorOptions(options))),
75
- waitForFunction: (pageFunction, arg, options) =>
76
- // Playwright: (fn, arg, {timeout,polling}); Puppeteer: (fn, {timeout,polling}, ...args).
77
- f.waitForFunction(pageFunction, options, arg),
52
+ waitForFunction: (pageFunction, arg, options) => f.waitForFunction(pageFunction, options, arg),
78
53
  };
79
54
  }
80
- /**
81
- * Wrap a Puppeteer `Page` so it satisfies the solver's structural Playwright
82
- * `Page`. Pass the result to `solver.solve(...)`.
83
- */
84
55
  function fromPuppeteer(page) {
85
56
  return {
86
57
  mouse: {
@@ -90,9 +61,7 @@ function fromPuppeteer(page) {
90
61
  },
91
62
  keyboard: {
92
63
  type: (text, options) => page.keyboard.type(text, options),
93
- // Puppeteer has no combo syntax: 'Control+A' must be held, pressed,
94
- // released. Playwright accepts the combo string directly, which is why
95
- // the solver speaks Playwright and this adapter translates.
64
+ // Puppeteer has no 'Control+A' combo syntax; the solver speaks Playwright and this side translates.
96
65
  press: async (key) => {
97
66
  const parts = key.split('+');
98
67
  const target = parts.pop();
@@ -106,14 +75,9 @@ function fromPuppeteer(page) {
106
75
  waitForTimeout: (timeout) => new Promise((resolve) => setTimeout(resolve, timeout)),
107
76
  waitForSelector: async (selector, options) => wrapHandle(await page.waitForSelector(selector, toPuppeteerSelectorOptions(options))),
108
77
  viewportSize: () => page.viewport(),
109
- $: async (selector) => wrapHandle(await page.$(selector)),
110
- $$: async (selector) => (await page.$$(selector)).map((h) => wrapHandle(h)).filter(Boolean),
111
- $eval: (selector, pageFunction, arg) => page.$eval(selector, pageFunction, arg),
112
- // Same name and shape on both libraries, but it still has to be forwarded:
113
- // the wrapper is a fresh object literal, so anything not listed here is
114
- // simply absent. The auto-solve watcher reads it to end its loop when the
115
- // caller closes the browser, and an undefined `isClosed` reads as "still
116
- // open" — a watcher that polls a dead page forever.
78
+ evaluate: (pageFunction) => page.evaluate(pageFunction),
79
+ locator: (selector) => locatorOver(() => page.$$(selector)),
80
+ // Forwarded explicitly: without it the watcher polls a dead page forever.
117
81
  isClosed: () => page.isClosed(),
118
82
  };
119
83
  }
@@ -1,50 +1,9 @@
1
- /**
2
- * Which interpreter runs the bundled CaptchaKraken CLI.
3
- *
4
- * This used to be `pythonCommand = 'python'`, inline in solver.ts. On
5
- * Debian/Ubuntu — the most common Linux deployment target, and what CI runs —
6
- * there is no `python` on PATH at all, only `python3`. So every JS solve died
7
- * with `/bin/sh: 1: python: not found` before it ever reached the model, unless
8
- * a `.venv` happened to exist next to the package. The type doc claimed the
9
- * default was "'python' or 'python3'"; the code never tried `python3`.
10
- *
11
- * That is a straight violation of the rule that the Python and JS clients
12
- * behave identically: the Python client obviously ran, so the divergence looked
13
- * like a model or endpoint problem rather than a missing binary.
14
- *
15
- * Kept in its own module, with the probe injected, so the resolution order is
16
- * testable without spawning anything.
17
- */
18
- /** Env var that overrides everything below it. */
19
1
  export declare const PYTHON_ENV_VAR = "CAPTCHA_KRAKEN_PYTHON";
20
- /**
21
- * Candidates tried in order when nothing is configured.
22
- *
23
- * `python3` FIRST. On Debian-family systems `python` is absent; where both
24
- * exist they are the same interpreter in practice, and PEP 394 makes `python3`
25
- * the portable spelling. `python` stays as the fallback for Windows and for
26
- * venvs that only expose the unsuffixed name.
27
- */
28
2
  export declare const PYTHON_CANDIDATES: readonly ["python3", "python"];
29
3
  export interface ResolveOptions {
30
- /** `config.pythonCommand`, if the caller set one. Wins outright. */
31
4
  configured?: string;
32
- /** The venv interpreter, when one was found next to the CLI. Wins over probing. */
33
5
  venvPython?: string | null;
34
- /** Process env, injected for testability. */
35
6
  env?: NodeJS.ProcessEnv;
36
- /** Returns true if the command is runnable. Injected so tests never spawn. */
37
7
  exists?: (command: string) => boolean;
38
8
  }
39
- /**
40
- * Resolve the interpreter, most specific first:
41
- *
42
- * 1. an explicitly configured `pythonCommand`
43
- * 2. `CAPTCHAKRAKEN_PYTHON`
44
- * 3. the venv interpreter beside the bundled CLI
45
- * 4. the first of `python3`, `python` that actually exists
46
- *
47
- * Falls back to `python3` rather than `python` when nothing can be probed: on
48
- * the platform where the two differ, `python3` is the one that is present.
49
- */
50
9
  export declare function resolvePythonCommand(options?: ResolveOptions): string;
@@ -1,46 +1,9 @@
1
1
  "use strict";
2
- /**
3
- * Which interpreter runs the bundled CaptchaKraken CLI.
4
- *
5
- * This used to be `pythonCommand = 'python'`, inline in solver.ts. On
6
- * Debian/Ubuntu — the most common Linux deployment target, and what CI runs —
7
- * there is no `python` on PATH at all, only `python3`. So every JS solve died
8
- * with `/bin/sh: 1: python: not found` before it ever reached the model, unless
9
- * a `.venv` happened to exist next to the package. The type doc claimed the
10
- * default was "'python' or 'python3'"; the code never tried `python3`.
11
- *
12
- * That is a straight violation of the rule that the Python and JS clients
13
- * behave identically: the Python client obviously ran, so the divergence looked
14
- * like a model or endpoint problem rather than a missing binary.
15
- *
16
- * Kept in its own module, with the probe injected, so the resolution order is
17
- * testable without spawning anything.
18
- */
19
2
  Object.defineProperty(exports, "__esModule", { value: true });
20
3
  exports.PYTHON_CANDIDATES = exports.PYTHON_ENV_VAR = void 0;
21
4
  exports.resolvePythonCommand = resolvePythonCommand;
22
- /** Env var that overrides everything below it. */
23
5
  exports.PYTHON_ENV_VAR = 'CAPTCHA_KRAKEN_PYTHON';
24
- /**
25
- * Candidates tried in order when nothing is configured.
26
- *
27
- * `python3` FIRST. On Debian-family systems `python` is absent; where both
28
- * exist they are the same interpreter in practice, and PEP 394 makes `python3`
29
- * the portable spelling. `python` stays as the fallback for Windows and for
30
- * venvs that only expose the unsuffixed name.
31
- */
32
6
  exports.PYTHON_CANDIDATES = ['python3', 'python'];
33
- /**
34
- * Resolve the interpreter, most specific first:
35
- *
36
- * 1. an explicitly configured `pythonCommand`
37
- * 2. `CAPTCHAKRAKEN_PYTHON`
38
- * 3. the venv interpreter beside the bundled CLI
39
- * 4. the first of `python3`, `python` that actually exists
40
- *
41
- * Falls back to `python3` rather than `python` when nothing can be probed: on
42
- * the platform where the two differ, `python3` is the one that is present.
43
- */
44
7
  function resolvePythonCommand(options = {}) {
45
8
  const { configured, venvPython, env = process.env, exists } = options;
46
9
  if (configured)
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Every selector the driver knows, keyed by vendor. Mirrored in python/src/captchakraken/selectors.py; keep both in the
3
+ * same order, because detection takes the first visible match in table order.
4
+ */
5
+ import { FrameRole, RecaptchaBanner, Vendor } from './kinds';
6
+ export interface VendorSelectors {
7
+ /** URL substrings of the vendor's code on the wire. A tripwire, not a detector: the host stays on the wire when a vendor renames its markup (see TRIBAL_KNOWLEDGE.md). BotDetect is self-hosted, so it has none. */
8
+ readonly hosts: readonly string[];
9
+ /** On the host page: the open challenge. Detected first. */
10
+ readonly challenge?: readonly string[];
11
+ /** On the host page: the checkbox. Detected while `response` is empty and `checked` is not showing. */
12
+ readonly checkbox?: readonly string[];
13
+ /** On the host page: inline widget shapes, detected last. Iframe selectors go first; a class that lives inside the frame document (.CheckboxCaptcha, .mtcap) only ever matches an inline embed. */
14
+ readonly widget?: readonly string[];
15
+ /** On the host page: the field the vendor fills on success. Read first and unconditionally, because hCaptcha's overlay hides the anchor. */
16
+ readonly response?: string;
17
+ /** Inside the checkbox frame: the box shows accepted. Demo pages do not always fill `response`. */
18
+ readonly checked?: string;
19
+ /** On the host page: the vendor painted its success state. Visibility is part of the test (see TRIBAL_KNOWLEDGE.md). */
20
+ readonly accepted?: string;
21
+ /** Inside the challenge frame: a new round has painted, with text. */
22
+ readonly fresh?: string;
23
+ /** Inside the challenge frame: the pictures a board is made of; the driver waits for each to have painted. */
24
+ readonly images?: readonly string[];
25
+ /** Inside the widget: the vendor's own submit control, tried after the generic text probes. */
26
+ readonly submit?: readonly string[];
27
+ /** Inside the challenge frame: verdict banners and what each means. */
28
+ readonly banners?: ReadonlyArray<readonly [string, RecaptchaBanner]>;
29
+ /** Inside the widget, and its enclosing fieldset/form: the answer box of a typed captcha. */
30
+ readonly textInput?: readonly string[];
31
+ /** Inside the widget: the knob a slide must start on; a drag from the piece moves nothing. */
32
+ readonly sliderHandle?: readonly string[];
33
+ /** Inside the widget: the piece, measured to steer the slide and dragged directly when there is no track. */
34
+ readonly piece?: readonly string[];
35
+ }
36
+ export declare const SELECTORS: Readonly<Record<Vendor, VendorSelectors>>;
37
+ export declare const VENDORS: ReadonlyArray<readonly [Vendor, VendorSelectors]>;
38
+ export interface WidgetProbe {
39
+ readonly vendor: Vendor;
40
+ readonly role: FrameRole;
41
+ readonly selector: string;
42
+ }
43
+ /** Every host-page selector with what it names, in detection order: open challenges, then checkboxes, then inline widgets. */
44
+ export declare const WIDGET_PROBES: ReadonlyArray<WidgetProbe>;
45
+ export declare const RESPONSE_SELECTORS: readonly string[];
46
+ export declare const ACCEPTED_SELECTORS: readonly string[];
47
+ /** `.//` keeps the xpath relative so a host-page widget cannot reach the form's own submit. */
48
+ export declare const submitByText: (text: string) => string;
49
+ /** A real Verify/Next/Submit/Skip first; the vendor-named controls are fallbacks. */
50
+ export declare const SUBMIT_SELECTORS: readonly string[];
51
+ export declare const TEXT_INPUT_VENDOR_SELECTORS: readonly string[];
52
+ export declare const TEXT_INPUT_SELECTORS: readonly string[];
53
+ export declare const SLIDER_HANDLE_SELECTORS: readonly string[];
54
+ export declare const PIECE_SELECTORS: readonly string[];