fiptcha 0.1.0 → 0.1.2

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 CHANGED
@@ -1,139 +1,203 @@
1
1
  # Fiptcha
2
2
 
3
- Local, self-hosted CAPTCHA solving primitives for browser automation. Built for Figranium, designed to be usable by other Node.js browser automation projects.
3
+ [![npm version](https://img.shields.io/npm/v/fiptcha.svg)](https://www.npmjs.com/package/fiptcha)
4
4
 
5
- Fiptcha is the standalone CAPTCHA runtime extracted from Figranium. It contains the local model/runtime layer that Figranium uses to detect resources, manage vision models, solve image grids, interact with browser challenges, and optionally use an Apple Silicon MLX companion.
5
+ Local CAPTCHA solving for browser automation.
6
6
 
7
- Fiptcha does **not** require the Figranium application or task format. The package exposes its solver modules directly, so another application can integrate the pieces it needs around its own browser lifecycle.
7
+ Fiptcha is the lightweight CAPTCHA runtime used by Figranium, packaged as a standalone Apache-2.0 Node.js library so it can be embedded in other browser automation projects.
8
8
 
9
- ## Why Fiptcha?
9
+ ## Install
10
10
 
11
- CAPTCHA solving inside a browser automation project tends to grow into its own subsystem: model selection, model downloads, resource checks, image-grid interpretation, challenge interception, browser interaction, platform-specific acceleration, and lifecycle management all need to work together. Fiptcha keeps that subsystem separate from the automation framework using it.
11
+ ```bash
12
+ npm install fiptcha
13
+ ```
14
+
15
+ CommonJS:
16
+
17
+ ```js
18
+ const { solveLocalCaptcha } = require('fiptcha');
19
+ ```
12
20
 
13
- The goals are:
21
+ Fiptcha does not launch a browser for you. Your application owns the browser lifecycle and passes an active Playwright-compatible `page` into the solver.
14
22
 
15
- - keep local CAPTCHA solving self-hostable;
16
- - avoid coupling the solver to Figranium's UI, API, task schema, or storage layer;
17
- - expose reusable Node.js modules for other browser automation projects;
18
- - download model assets only when they are needed rather than bundling large weights;
19
- - select an appropriate local backend based on available resources;
20
- - support optional companion runtimes without making them mandatory for every installation.
23
+ ## Quick start
24
+
25
+ ```js
26
+ const { chromium } = require('playwright');
27
+ const { solveLocalCaptcha } = require('fiptcha');
21
28
 
22
- ## Supported challenge families
29
+ const browser = await chromium.launch({ headless: false });
30
+ const page = await browser.newPage();
23
31
 
24
- Fiptcha contains the local runtime used by Figranium for:
32
+ await page.goto('https://example.com');
25
33
 
26
- - reCAPTCHA v2 image challenges;
27
- - hCaptcha image challenges;
28
- - Cloudflare Turnstile/browser challenge helpers.
34
+ const logs = [];
35
+ const result = await solveLocalCaptcha(page, {
36
+ captchaType: 'recaptcha_v2',
37
+ timeout: 60_000,
38
+ logs
39
+ });
29
40
 
30
- Support is intentionally described as challenge/runtime support rather than a guarantee that every CAPTCHA can be solved. Providers change their challenge implementations and may use additional risk signals beyond the visible challenge.
41
+ console.log(result.token);
42
+ console.log(result.provider); // "local"
43
+ console.log(result.model); // present when a vision model was used
44
+ console.log(result.device); // "browser", "cpu", or the active inference device
31
45
 
32
- ## Features
46
+ await browser.close();
47
+ ```
33
48
 
34
- - Local CAPTCHA solving from a browser automation session.
35
- - Resource-aware model selection.
36
- - Verified, on-demand model downloads and model lifecycle management.
37
- - Image-grid classification and challenge handling.
38
- - Browser challenge interception helpers.
39
- - Companion-process client and runtime support.
40
- - Optional MLX acceleration for compatible Apple Silicon environments.
41
- - Container/resource probing utilities.
42
- - CAPTCHA benchmarking helpers.
43
- - Environment-variable configuration through the `CAPTCHA_*` settings used by the runtime.
44
- - Apache-2.0 licensing for reuse in other applications.
49
+ ## Supported challenge types
45
50
 
46
- ## Requirements
51
+ Pass one of these values as `captchaType`:
47
52
 
48
- Fiptcha is a Node.js package and expects the host application to provide the browser/page object used for challenge interaction. It does not launch or manage your entire automation application for you.
53
+ | Value | Behavior |
54
+ | --- | --- |
55
+ | `recaptcha_v2` | Interacts with the checkbox and solves supported image-grid challenges locally when required. |
56
+ | `hcaptcha` | Interacts with the widget and solves supported image-grid challenges locally when required. |
57
+ | `turnstile` | Performs active-browser interaction and waits for a token. It does not use the image-grid vision path. |
49
58
 
50
- Model inference can require substantial memory. Fiptcha performs resource detection before selecting a local model. A provider-advertised 2 GB host is treated using decimal gigabyte semantics rather than incorrectly requiring a full 2 GiB/2048 MiB allocation.
59
+ Fiptcha cannot guarantee that every challenge will be solvable. CAPTCHA providers can change their UI, challenge format, and risk checks independently of the visible widget.
51
60
 
52
- Large model files are not intended to be embedded into the npm package. They are fetched by the model-management layer when required.
61
+ ## `solveLocalCaptcha(page, options)`
53
62
 
54
- ## Installation
63
+ This is the main integration API.
55
64
 
56
- ```bash
57
- npm install fiptcha
65
+ ```js
66
+ const result = await solveLocalCaptcha(page, {
67
+ captchaType: 'hcaptcha',
68
+ timeout: 60_000,
69
+ logs: []
70
+ });
58
71
  ```
59
72
 
60
- The package entry point is CommonJS, so it can be loaded with `require()` directly.
73
+ ### Arguments
74
+
75
+ #### `page`
76
+
77
+ An active Playwright-compatible page. Fiptcha uses page/frame APIs, locators, mouse interaction, evaluation, and timeouts against this object.
78
+
79
+ #### `options.captchaType`
80
+
81
+ Required. One of:
82
+
83
+ ```text
84
+ recaptcha_v2
85
+ hcaptcha
86
+ turnstile
87
+ ```
88
+
89
+ #### `options.timeout`
90
+
91
+ Optional. Maximum solve time in milliseconds.
92
+
93
+ Default:
61
94
 
62
95
  ```js
63
- const fiptcha = require('fiptcha');
96
+ 60_000
64
97
  ```
65
98
 
66
- ## Basic usage
99
+ #### `options.logs`
67
100
 
68
- The highest-level local solver can be used with a compatible browser page:
101
+ Optional array. Fiptcha appends diagnostic messages to it while solving.
69
102
 
70
103
  ```js
71
- const { solveLocalCaptcha } = require('fiptcha');
104
+ const logs = [];
72
105
 
73
- const result = await solveLocalCaptcha(page, {
106
+ await solveLocalCaptcha(page, {
74
107
  captchaType: 'recaptcha_v2',
75
- timeout: 60_000
108
+ logs
76
109
  });
77
110
 
78
- console.log(result);
111
+ console.log(logs);
79
112
  ```
80
113
 
81
- Your application remains responsible for creating the browser, navigating to the target page, deciding when a solve should occur, and handling the solve result in its own workflow.
114
+ ### Return value
82
115
 
83
- ## Integration model
116
+ A successful solve returns an object similar to:
84
117
 
85
- Fiptcha is designed as a runtime library rather than a complete automation framework. A typical integration looks like this:
118
+ ```js
119
+ {
120
+ token: '...',
121
+ provider: 'local',
122
+ model: 'owlvit', // only when a vision backend was used
123
+ device: 'cpu'
124
+ }
125
+ ```
86
126
 
87
- ```text
88
- Your application
89
- |
90
- +-- browser lifecycle (Playwright/compatible host)
91
- |
92
- +-- challenge detection / solve trigger
93
- | |
94
- | +-- Fiptcha
95
- | +-- resource detection
96
- | +-- model manager
97
- | +-- model downloader
98
- | +-- local solver
99
- | +-- grid solver
100
- | +-- optional companion / MLX runtime
101
- |
102
- +-- application-specific result handling
103
- ```
104
-
105
- This separation is deliberate. Fiptcha does not know about Figranium Tasks, blocks, Cabinets, executions, authentication, scheduling, or its UI.
106
-
107
- ## Public API
108
-
109
- The root package currently re-exports the public functions from the following modules:
110
-
111
- - `captcha-local-solver` — high-level local solving logic;
112
- - `captcha-grid-solver` — image-grid solving primitives;
113
- - `captcha-interceptor` — browser/challenge interception helpers;
114
- - `captcha-resources` — host/container resource detection;
115
- - `captcha-model-manager` — model selection and lifecycle management;
116
- - `captcha-model-downloader` — model acquisition and verification;
117
- - `captcha-model-manifest` — model metadata;
118
- - `captcha-companion-client` — communication with the optional companion process;
119
- - `captcha-mlx-runtime` — MLX runtime integration;
120
- - `captcha-benchmark` — benchmark helpers.
121
-
122
- For example:
127
+ `model` is omitted when no vision model was needed.
128
+
129
+ ### Errors
130
+
131
+ `solveLocalCaptcha` throws when:
132
+
133
+ - the requested CAPTCHA type is unsupported;
134
+ - a challenge never exposes a usable token or image challenge;
135
+ - a required vision backend cannot be prepared;
136
+ - the challenge ends without producing a token;
137
+ - Turnstile does not issue a token after browser interaction.
138
+
139
+ Wrap solves in your application's normal error handling:
123
140
 
124
141
  ```js
125
- const {
126
- solveLocalCaptcha,
127
- // Other model, resource, interception, and grid helpers are
128
- // exported from the same package entry point.
129
- } = require('fiptcha');
142
+ try {
143
+ const result = await solveLocalCaptcha(page, {
144
+ captchaType: 'recaptcha_v2',
145
+ timeout: 60_000
146
+ });
147
+
148
+ console.log(result.token);
149
+ } catch (error) {
150
+ console.error('CAPTCHA solve failed:', error.message);
151
+ }
130
152
  ```
131
153
 
132
- Fiptcha is still young, so applications that depend on lower-level helpers should pin the package version while the standalone API is being stabilized.
154
+ ## Browser integration pattern
155
+
156
+ A typical integration should keep orchestration outside Fiptcha:
157
+
158
+ ```js
159
+ const { solveLocalCaptcha } = require('fiptcha');
160
+
161
+ async function handleCaptcha(page, captchaType) {
162
+ const logs = [];
163
+
164
+ const result = await solveLocalCaptcha(page, {
165
+ captchaType,
166
+ timeout: 60_000,
167
+ logs
168
+ });
169
+
170
+ return {
171
+ token: result.token,
172
+ diagnostics: logs
173
+ };
174
+ }
175
+ ```
176
+
177
+ Your application should remain responsible for:
178
+
179
+ - launching and closing browsers;
180
+ - choosing browser contexts and sessions;
181
+ - navigation;
182
+ - deciding when CAPTCHA handling should run;
183
+ - retries at the workflow level;
184
+ - proxies and network policy;
185
+ - persistence;
186
+ - translating Fiptcha errors/results into your own execution model.
187
+
188
+ Fiptcha is responsible for the CAPTCHA-specific runtime: token detection, widget interaction, grid solving, model selection, model downloads, resource checks, and optional companion runtimes.
189
+
190
+ ## Models and memory requirements
191
+
192
+ Fiptcha does not bundle large model weights into the npm package. Required assets are acquired when needed.
193
+
194
+ The runtime chooses an appropriate backend based on available resources. A provider-advertised **2 GB** machine is interpreted using decimal sizing (2,000,000,000 bytes, about 1907 MiB) rather than requiring 2048 MiB.
195
+
196
+ If your application runs in an ephemeral container, persist the model/cache directory so models do not need to be downloaded again on every container recreation.
133
197
 
134
198
  ## Companion runtime
135
199
 
136
- Fiptcha includes the companion tooling that was previously embedded in Figranium.
200
+ Fiptcha includes an optional companion process for environments that need it.
137
201
 
138
202
  Install companion requirements:
139
203
 
@@ -141,103 +205,85 @@ Install companion requirements:
141
205
  npm run companion:install
142
206
  ```
143
207
 
144
- Start the companion:
208
+ Start it:
145
209
 
146
210
  ```bash
147
211
  npm run companion:start
148
212
  ```
149
213
 
150
- For the Docker-oriented path:
214
+ Start the Docker-oriented path:
151
215
 
152
216
  ```bash
153
217
  npm run companion:start:docker
154
218
  ```
155
219
 
156
- The companion is optional. Applications that do not need that backend can use the ordinary Node.js runtime without making the companion part of their own product architecture.
220
+ The companion is optional. Ordinary Node.js integrations do not need it unless they use that runtime path.
157
221
 
158
- ## Resource probing
222
+ ## Apple Silicon / MLX
159
223
 
160
- The package includes a probe command:
224
+ Fiptcha supports an optional MLX runtime for compatible Apple Silicon environments. Consumers that do not use MLX can ignore it.
225
+
226
+ ## Inspect the runtime environment
227
+
228
+ Run the included resource probe:
161
229
 
162
230
  ```bash
163
231
  npm run probe
164
232
  ```
165
233
 
166
- This is useful when validating the environment Fiptcha will run in, especially inside containers where reported memory limits may differ from the physical host.
167
-
168
- Resource detection is kept inside Fiptcha so applications embedding it do not need to reproduce Figranium-specific hardware checks.
169
-
170
- ## Model management
171
-
172
- Fiptcha separates model metadata, downloading, resource selection, and inference/runtime behavior. This allows the package to choose a suitable backend without forcing every application to ship every supported model.
234
+ This is useful in containers and VMs where cgroup/container limits may differ from the physical host's reported memory.
173
235
 
174
- The model downloader verifies model assets against the package's model manifest. Applications embedding Fiptcha should persist the configured model/cache directory if they do not want model assets to be downloaded again when an ephemeral container is recreated.
236
+ ## Using Fiptcha from another project
175
237
 
176
- ## Configuration
238
+ Install it normally:
177
239
 
178
- Fiptcha uses the `CAPTCHA_*` environment-variable configuration inherited from the standalone runtime extracted from Figranium. Configuration belongs to the process embedding Fiptcha; the package does not require a Figranium settings database.
179
-
180
- When integrating Fiptcha into another application, keep CAPTCHA/model configuration at the application boundary and pass browser state through the solver APIs rather than importing Figranium-specific configuration code.
240
+ ```bash
241
+ npm install fiptcha
242
+ ```
181
243
 
182
- ## Using Fiptcha outside Figranium
244
+ Then keep the integration at your browser/runtime boundary:
183
245
 
184
- Fiptcha is intentionally licensed and packaged for this use case. A third-party automation project can install `fiptcha`, provide its own browser page, call the high-level solver or lower-level exported helpers, and keep its own orchestration around the package.
246
+ ```js
247
+ const { solveLocalCaptcha } = require('fiptcha');
185
248
 
186
- A good integration should generally:
249
+ async function solve(page) {
250
+ return solveLocalCaptcha(page, {
251
+ captchaType: 'recaptcha_v2',
252
+ timeout: 45_000
253
+ });
254
+ }
255
+ ```
187
256
 
188
- 1. let the host application own browser creation and navigation;
189
- 2. detect or decide when a CAPTCHA needs to be handled;
190
- 3. invoke Fiptcha with the active page/challenge context;
191
- 4. allow Fiptcha to manage its local model/runtime concerns;
192
- 5. translate the returned result into the host application's own success/error model.
257
+ No Figranium task schema, server, UI, database, scheduler, authentication system, or storage layer is required.
193
258
 
194
- Avoid copying Fiptcha source into the consuming project. Depending on the package keeps fixes to model handling, resource detection, and challenge logic independently upgradeable.
259
+ ## Troubleshooting
195
260
 
196
- ## Figranium integration
261
+ ### `Local solver does not support ...`
197
262
 
198
- Figranium consumes Fiptcha as a dependency. Compatibility shims in Figranium preserve its previous internal CAPTCHA import paths while the implementation itself lives here.
263
+ Check that `captchaType` is exactly one of:
199
264
 
200
- That means Fiptcha is not merely a separately published copy of the solver: this repository is intended to be the canonical implementation of the local CAPTCHA runtime used by Figranium.
265
+ ```text
266
+ recaptcha_v2
267
+ hcaptcha
268
+ turnstile
269
+ ```
201
270
 
202
- ## Testing
271
+ ### `vision backend unavailable`
203
272
 
204
- Run the package smoke tests with:
273
+ The challenge requires the image-solving path, but Fiptcha could not prepare a supported vision backend. Check available memory, model availability/downloads, and the output of:
205
274
 
206
275
  ```bash
207
- npm test
276
+ npm run probe
208
277
  ```
209
278
 
210
- The test suite includes regression coverage for resource detection, including the 2 GB host threshold that previously rejected machines advertised as having 2 GB RAM because the check effectively required 2 GiB.
211
-
212
- ## Current maturity
213
-
214
- Fiptcha began as an extraction of a production subsystem rather than a greenfield SDK. The underlying solver has already been exercised through Figranium, but the standalone package API is new.
215
-
216
- The high-level integration boundary is usable today. The main area that should be considered pre-stable is the exact shape and naming of lower-level exported helpers. Until a stable standalone API is declared, pinning an exact Fiptcha version is recommended for third-party applications.
217
-
218
- ## Security and responsible use
219
-
220
- CAPTCHA systems are one part of a site's abuse-prevention and access-control strategy. Integrators are responsible for ensuring their automation is authorized and complies with applicable terms, policies, and law.
221
-
222
- Do not treat successful CAPTCHA handling as permission to access data or perform actions that the application is otherwise not authorized to perform.
223
-
224
- ## Relationship to Figranium
225
-
226
- Fiptcha is part of the Figranium ecosystem but is independently reusable. Figranium provides the visual browser-automation product and orchestration layer; Fiptcha focuses on the CAPTCHA-solving runtime.
227
-
228
- ## Contributing
279
+ ### Challenge appears but no token is returned
229
280
 
230
- Bug reports and focused improvements are welcome. For solver bugs, include enough environment information to reproduce the issue, particularly:
281
+ Increase the solve timeout and collect the `logs` array. Also verify that the challenge family still matches the provider structure expected by the current Fiptcha version.
231
282
 
232
- - operating system and architecture;
233
- - Node.js version;
234
- - container/runtime details if applicable;
235
- - available memory;
236
- - CAPTCHA provider/challenge family;
237
- - whether the ordinary runtime or companion/MLX path was used.
283
+ ### Models download repeatedly in containers
238
284
 
239
- Please avoid attaching sensitive cookies, credentials, or private browsing data to public issues.
285
+ Persist the model/cache storage used by the runtime instead of recreating it with every ephemeral container.
240
286
 
241
287
  ## License
242
288
 
243
- Fiptcha is licensed under the Apache License 2.0. See `LICENSE` for the full terms.
289
+ Apache-2.0.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fiptcha",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Local CAPTCHA solving for browser automation. Built for Figranium, usable anywhere.",
5
5
  "main": "index.js",
6
6
  "license": "Apache-2.0",
@@ -42,6 +42,17 @@ function normalizePrompt(adapter, text) {
42
42
  return adapter.cleanPrompt(String(text || '')).replace(/\s+/g, ' ').replace(/[.!]+$/, '').trim();
43
43
  }
44
44
 
45
+ function normalizeModelLabel(label) {
46
+ return String(label || '').replace(/\b[a-z]+\b/gi, (word) => {
47
+ const lower = word.toLowerCase();
48
+ if (lower === 'buses') return 'bus';
49
+ if (lower.endsWith('ies')) return `${lower.slice(0, -3)}y`;
50
+ if (/(?:ches|shes|xes|zes)$/.test(lower)) return lower.slice(0, -2);
51
+ if (lower.endsWith('s') && !lower.endsWith('ss') && !lower.endsWith('us')) return lower.slice(0, -1);
52
+ return lower;
53
+ });
54
+ }
55
+
45
56
  function normalizeBox(detection) {
46
57
  const box = detection?.box || detection?.bbox;
47
58
  if (Array.isArray(box) && box.length >= 4) {
@@ -81,16 +92,16 @@ function mapDetectionsToCells(detections, gridBox, cellBoxes, imageSize = null)
81
92
  }
82
93
 
83
94
  async function classifyGrid(frame, adapter, cells, label, seenTiles) {
95
+ const modelLabel = normalizeModelLabel(label);
84
96
  const grid = frame.locator(adapter.grid).first();
85
97
  const gridBox = await grid.boundingBox().catch(() => null);
86
98
  if (gridBox) {
87
99
  const image = await grid.screenshot({ type: 'png' });
88
- const detections = await captchaModelManager.detect(image, label);
89
- if (detections.length === 0) return [];
100
+ const detections = await captchaModelManager.detect(image, modelLabel);
90
101
  const cellBoxes = [];
91
102
  for (let index = 0; index < await cells.count(); index += 1) cellBoxes.push(await cells.nth(index).boundingBox().catch(() => null));
92
103
  const mapped = mapDetectionsToCells(detections, gridBox, cellBoxes, pngDimensions(image));
93
- if (mapped.length || detections.every((item) => normalizeBox(item))) {
104
+ if (mapped.length) {
94
105
  const changed = [];
95
106
  for (const index of mapped) {
96
107
  const image = await cells.nth(index).screenshot({ type: 'png' });
@@ -110,13 +121,19 @@ async function classifyGrid(frame, adapter, cells, label, seenTiles) {
110
121
  const fingerprint = checksumBuffer(image);
111
122
  if (seenTiles.get(index) === fingerprint) continue;
112
123
  seenTiles.set(index, fingerprint);
113
- if ((await captchaModelManager.detect(image, label)).length) selected.push(index);
124
+ if ((await captchaModelManager.detect(image, modelLabel)).length) selected.push(index);
114
125
  }
115
126
  return selected;
116
127
  }
117
128
 
118
129
  async function visibleText(frame, selector) {
119
- return frame.locator(selector).first().innerText({ timeout: 1500 }).catch(() => '');
130
+ const matches = frame.locator(selector);
131
+ for (let index = 0; index < await matches.count().catch(() => 0); index += 1) {
132
+ const locator = matches.nth(index);
133
+ if (!await locator.isVisible({ timeout: 250 }).catch(() => false)) continue;
134
+ return locator.innerText({ timeout: 1500 }).catch(() => '');
135
+ }
136
+ return '';
120
137
  }
121
138
 
122
139
  async function solveImageGrid(page, { captchaType, deadline, waitForToken, logs = [] }) {
@@ -128,7 +145,9 @@ async function solveImageGrid(page, { captchaType, deadline, waitForToken, logs
128
145
  const frame = await findChallengeFrame(page, adapter);
129
146
  if (!frame) throw new Error(`${captchaType} image challenge frame disappeared`);
130
147
  const rejection = await visibleText(frame, adapter.error);
131
- if (rejection) throw new Error(`${captchaType} rejected the previous selection: ${rejection.replace(/\s+/g, ' ').trim()}`);
148
+ if (rejection && round > 0) {
149
+ throw new Error(`${captchaType} rejected the previous selection: ${rejection.replace(/\s+/g, ' ').trim()}`);
150
+ }
132
151
  const prompt = normalizePrompt(adapter, await visibleText(frame, adapter.instruction));
133
152
  if (!prompt) throw new Error(`${captchaType} solver could not read the image challenge instruction`);
134
153
  if (prompt !== previousPrompt) {
@@ -155,4 +174,4 @@ async function solveImageGrid(page, { captchaType, deadline, waitForToken, logs
155
174
  return null;
156
175
  }
157
176
 
158
- module.exports = { PROVIDERS, normalizePrompt, normalizeBox, pngDimensions, mapDetectionsToCells, solveImageGrid };
177
+ module.exports = { PROVIDERS, normalizePrompt, normalizeModelLabel, normalizeBox, pngDimensions, mapDetectionsToCells, classifyGrid, visibleText, solveImageGrid };
@@ -7,6 +7,21 @@ const TOKEN_SELECTORS = Object.freeze({
7
7
  turnstile: ['input[name="cf-turnstile-response"]', 'textarea[name="cf-turnstile-response"]']
8
8
  });
9
9
 
10
+ const WIDGET_INTERACTIONS = Object.freeze({
11
+ recaptcha_v2: Object.freeze({
12
+ framePatterns: ['/recaptcha/api2/anchor', '/recaptcha/enterprise/anchor'],
13
+ selectors: ['#recaptcha-anchor', '[role="checkbox"]']
14
+ }),
15
+ hcaptcha: Object.freeze({
16
+ framePatterns: ['hcaptcha.com/captcha', 'newassets.hcaptcha.com/captcha'],
17
+ selectors: ['#checkbox', '[role="checkbox"]', 'input[type="checkbox"]', 'label']
18
+ }),
19
+ turnstile: Object.freeze({
20
+ framePatterns: ['challenges.cloudflare.com'],
21
+ selectors: ['[role="checkbox"]', 'input[type="checkbox"]', 'label', 'button']
22
+ })
23
+ });
24
+
10
25
  async function readToken(page, captchaType) {
11
26
  return page.evaluate(({ type, selectors }) => {
12
27
  for (const selector of selectors[type] || []) {
@@ -57,43 +72,40 @@ async function waitForTokenOrChallenge(page, captchaType, timeout) {
57
72
  return null;
58
73
  }
59
74
 
60
- async function clickCheckbox(page, captchaType) {
61
- const patterns = captchaType === 'hcaptcha'
62
- ? ['hcaptcha.com/captcha', 'newassets.hcaptcha.com/captcha']
63
- : captchaType === 'turnstile'
64
- ? ['challenges.cloudflare.com']
65
- : ['/recaptcha/api2/anchor', '/recaptcha/enterprise/anchor'];
66
- const selector = captchaType === 'hcaptcha'
67
- ? '#checkbox, [role="checkbox"], input[type="checkbox"], label'
68
- : captchaType === 'turnstile'
69
- ? 'input[type="checkbox"], [role="checkbox"], button, label'
70
- : '#recaptcha-anchor, [role="checkbox"]';
71
- for (const frame of page.frames?.() || []) {
72
- if (!patterns.some((pattern) => frame.url().includes(pattern))) continue;
73
- const checkbox = frame.locator(selector).first();
74
- if (!await checkbox.isVisible({ timeout: 1000 }).catch(() => false)) continue;
75
- if (!await checkbox.isEnabled({ timeout: 250 }).catch(() => true)) continue;
76
- const pointerReady = await checkbox.evaluate((element) => {
77
- const style = getComputedStyle(element);
78
- return style.pointerEvents !== 'none' && element.getAttribute('aria-disabled') !== 'true';
79
- }).catch(() => true);
80
- if (!pointerReady) continue;
81
- const firstBox = await checkbox.boundingBox().catch(() => null);
82
- await page.waitForTimeout(75);
83
- const box = await checkbox.boundingBox().catch(() => null);
84
- if (firstBox && box && (Math.abs(firstBox.x - box.x) >= 1 || Math.abs(firstBox.y - box.y) >= 1)) continue;
85
- if (box && page.mouse) {
86
- const x = box.x + box.width / 2;
87
- const y = box.y + box.height / 2;
88
- await page.mouse.move(x, y, { steps: 8 });
89
- await page.mouse.down();
90
- await page.waitForTimeout(45);
91
- await page.mouse.up();
92
- } else {
93
- await checkbox.click({ timeout: 2000 });
75
+ async function clickCheckbox(page, captchaType, timeout = 10_000) {
76
+ const interaction = WIDGET_INTERACTIONS[captchaType];
77
+ if (!interaction) return false;
78
+ const deadline = Date.now() + Math.max(0, timeout);
79
+
80
+ do {
81
+ for (const frame of page.frames?.() || []) {
82
+ const frameUrl = String(frame.url?.() || '');
83
+ if (!interaction.framePatterns.some((pattern) => frameUrl.includes(pattern))) continue;
84
+
85
+ for (const selector of interaction.selectors) {
86
+ const matches = frame.locator(selector);
87
+ const count = await matches.count().catch(() => 0);
88
+ for (let index = 0; index < count; index += 1) {
89
+ const checkbox = matches.nth(index);
90
+ if (!await checkbox.isVisible({ timeout: 100 }).catch(() => false)) continue;
91
+ if (!await checkbox.isEnabled({ timeout: 100 }).catch(() => true)) continue;
92
+ const pointerReady = await checkbox.evaluate((element) => {
93
+ const style = getComputedStyle(element);
94
+ return style.pointerEvents !== 'none' && element.getAttribute('aria-disabled') !== 'true';
95
+ }).catch(() => true);
96
+ if (!pointerReady) continue;
97
+
98
+ const clickTimeout = Math.max(1, Math.min(2000, deadline - Date.now()));
99
+ const clicked = await checkbox.click({ timeout: clickTimeout }).then(() => true, () => false);
100
+ if (clicked) return true;
101
+ }
102
+ }
94
103
  }
95
- return true;
96
- }
104
+
105
+ if (Date.now() >= deadline) break;
106
+ await page.waitForTimeout(Math.min(100, Math.max(1, deadline - Date.now())));
107
+ } while (Date.now() <= deadline);
108
+
97
109
  return false;
98
110
  }
99
111
 
@@ -121,7 +133,15 @@ async function solveLocalCaptcha(page, { captchaType, timeout = 60_000, logs = [
121
133
  : null;
122
134
  let token = await readToken(page, captchaType);
123
135
  if (!token) {
124
- await clickCheckbox(page, captchaType).catch((error) => logs.push(`Local ${captchaType} checkbox click failed: ${error.message}`));
136
+ const clickTimeout = Math.min(10_000, Math.max(0, deadline - Date.now()));
137
+ const clicked = await clickCheckbox(page, captchaType, clickTimeout)
138
+ .catch((error) => {
139
+ logs.push(`Local ${captchaType} checkbox click failed: ${error.message}`);
140
+ return false;
141
+ });
142
+ logs.push(clicked
143
+ ? `Local ${captchaType} checkbox clicked`
144
+ : `Local ${captchaType} checkbox was not found before the interaction timeout`);
125
145
  const initialWait = captchaType === 'hcaptcha' ? 1500 : 7000;
126
146
  token = await waitForTokenOrChallenge(page, captchaType, Math.min(initialWait, Math.max(0, deadline - Date.now())));
127
147
  if (!token && captchaType === 'hcaptcha' && Date.now() < deadline) {
@@ -165,4 +185,4 @@ async function solveLocalCaptcha(page, { captchaType, timeout = 60_000, logs = [
165
185
  };
166
186
  }
167
187
 
168
- module.exports = { TOKEN_SELECTORS, readToken, waitForToken, waitForTokenOrChallenge, clickCheckbox, executeHcaptchaWidget, solveLocalCaptcha };
188
+ module.exports = { TOKEN_SELECTORS, WIDGET_INTERACTIONS, readToken, waitForToken, waitForTokenOrChallenge, clickCheckbox, executeHcaptchaWidget, solveLocalCaptcha };