fiptcha 0.1.1 → 0.1.3
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 +5 -100
- package/package.json +1 -1
- package/src/captcha-grid-solver.js +33 -9
- package/src/captcha-local-solver.js +2 -2
- package/src/captcha-pointer.js +31 -0
package/README.md
CHANGED
|
@@ -187,43 +187,13 @@ Your application should remain responsible for:
|
|
|
187
187
|
|
|
188
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
189
|
|
|
190
|
-
## Exported helpers
|
|
191
|
-
|
|
192
|
-
The package root re-exports the public exports from these modules:
|
|
193
|
-
|
|
194
|
-
| Module | Purpose |
|
|
195
|
-
| --- | --- |
|
|
196
|
-
| `captcha-local-solver` | High-level widget/token handling and `solveLocalCaptcha`. |
|
|
197
|
-
| `captcha-grid-solver` | Image-grid solving and provider adapters. |
|
|
198
|
-
| `captcha-interceptor` | Challenge interception helpers. |
|
|
199
|
-
| `captcha-resources` | Host/container memory and resource detection. |
|
|
200
|
-
| `captcha-model-manager` | Model selection, readiness, and lifecycle state. |
|
|
201
|
-
| `captcha-model-downloader` | Model download and verification. |
|
|
202
|
-
| `captcha-model-manifest` | Model metadata and requirements. |
|
|
203
|
-
| `captcha-companion-client` | Client for the optional companion process. |
|
|
204
|
-
| `captcha-mlx-runtime` | Apple Silicon MLX runtime support. |
|
|
205
|
-
| `captcha-benchmark` | Benchmarking helpers. |
|
|
206
|
-
|
|
207
|
-
For example, lower-level token helpers are also available:
|
|
208
|
-
|
|
209
|
-
```js
|
|
210
|
-
const {
|
|
211
|
-
readToken,
|
|
212
|
-
waitForToken,
|
|
213
|
-
clickCheckbox,
|
|
214
|
-
solveLocalCaptcha
|
|
215
|
-
} = require('fiptcha');
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
The high-level solver is the recommended integration boundary. Lower-level exports are useful for custom integrations, but their API should be treated as pre-stable until Fiptcha reaches a stable standalone API release.
|
|
219
|
-
|
|
220
190
|
## Models and memory requirements
|
|
221
191
|
|
|
222
|
-
Fiptcha does not bundle large model weights into the npm
|
|
192
|
+
Fiptcha does not bundle large model weights into the npm package. Required assets are acquired when needed.
|
|
223
193
|
|
|
224
|
-
The runtime chooses an appropriate backend based on available resources.
|
|
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.
|
|
225
195
|
|
|
226
|
-
If your application runs in an ephemeral container, persist the model/cache directory
|
|
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.
|
|
227
197
|
|
|
228
198
|
## Companion runtime
|
|
229
199
|
|
|
@@ -247,13 +217,11 @@ Start the Docker-oriented path:
|
|
|
247
217
|
npm run companion:start:docker
|
|
248
218
|
```
|
|
249
219
|
|
|
250
|
-
The companion is optional. Ordinary Node.js integrations do not need
|
|
220
|
+
The companion is optional. Ordinary Node.js integrations do not need it unless they use that runtime path.
|
|
251
221
|
|
|
252
222
|
## Apple Silicon / MLX
|
|
253
223
|
|
|
254
|
-
Fiptcha
|
|
255
|
-
|
|
256
|
-
The MLX path is optional. Consumers that do not use MLX can ignore the bundled worker and companion requirements.
|
|
224
|
+
Fiptcha supports an optional MLX runtime for compatible Apple Silicon environments. Consumers that do not use MLX can ignore it.
|
|
257
225
|
|
|
258
226
|
## Inspect the runtime environment
|
|
259
227
|
|
|
@@ -265,43 +233,6 @@ npm run probe
|
|
|
265
233
|
|
|
266
234
|
This is useful in containers and VMs where cgroup/container limits may differ from the physical host's reported memory.
|
|
267
235
|
|
|
268
|
-
## Test Fiptcha
|
|
269
|
-
|
|
270
|
-
```bash
|
|
271
|
-
npm test
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
The smoke suite checks the extracted runtime and includes regression coverage for resource selection, including the 2 GB memory threshold.
|
|
275
|
-
|
|
276
|
-
## Check what npm will publish
|
|
277
|
-
|
|
278
|
-
Before publishing a release:
|
|
279
|
-
|
|
280
|
-
```bash
|
|
281
|
-
npm pack --dry-run
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
The package intentionally publishes the runtime source, entry point, companion scripts, README, and license—not downloaded model weights.
|
|
285
|
-
|
|
286
|
-
## Release process
|
|
287
|
-
|
|
288
|
-
Fiptcha publishes to npm as:
|
|
289
|
-
|
|
290
|
-
```text
|
|
291
|
-
fiptcha
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
The repository uses npm Trusted Publishing through GitHub Actions. Releases are published without a long-lived npm token.
|
|
295
|
-
|
|
296
|
-
To release a new version:
|
|
297
|
-
|
|
298
|
-
1. Update `version` in `package.json`.
|
|
299
|
-
2. Merge the change to `main`.
|
|
300
|
-
3. Create and publish a GitHub Release whose tag matches the package version, for example `v0.2.0`.
|
|
301
|
-
4. The release workflow runs the tests and publishes the package to npm through OIDC Trusted Publishing.
|
|
302
|
-
|
|
303
|
-
Do not manually add an `NPM_TOKEN` secret for the normal release path.
|
|
304
|
-
|
|
305
236
|
## Using Fiptcha from another project
|
|
306
237
|
|
|
307
238
|
Install it normally:
|
|
@@ -325,12 +256,6 @@ async function solve(page) {
|
|
|
325
256
|
|
|
326
257
|
No Figranium task schema, server, UI, database, scheduler, authentication system, or storage layer is required.
|
|
327
258
|
|
|
328
|
-
## Using Fiptcha in Figranium
|
|
329
|
-
|
|
330
|
-
Figranium consumes the published `fiptcha` package and keeps only Figranium-specific integration code in the main application.
|
|
331
|
-
|
|
332
|
-
The standalone Fiptcha repository is the canonical implementation of the local CAPTCHA runtime. Fixes to model handling, resource detection, challenge interaction, or companion behavior should be made here rather than copied back into Figranium.
|
|
333
|
-
|
|
334
259
|
## Troubleshooting
|
|
335
260
|
|
|
336
261
|
### `Local solver does not support ...`
|
|
@@ -359,26 +284,6 @@ Increase the solve timeout and collect the `logs` array. Also verify that the ch
|
|
|
359
284
|
|
|
360
285
|
Persist the model/cache storage used by the runtime instead of recreating it with every ephemeral container.
|
|
361
286
|
|
|
362
|
-
### A lower-level helper changed between releases
|
|
363
|
-
|
|
364
|
-
Prefer `solveLocalCaptcha` for application integrations. Until the low-level API is declared stable, pin an exact Fiptcha version if you depend directly on internal/model/grid helpers.
|
|
365
|
-
|
|
366
|
-
## Package contents
|
|
367
|
-
|
|
368
|
-
The npm package contains:
|
|
369
|
-
|
|
370
|
-
```text
|
|
371
|
-
index.js
|
|
372
|
-
src/
|
|
373
|
-
scripts/captcha-mlx-worker.py
|
|
374
|
-
scripts/captcha-companion-requirements.txt
|
|
375
|
-
scripts/captcha-container-probe.js
|
|
376
|
-
scripts/install-captcha-companion.js
|
|
377
|
-
scripts/captcha-companion.js
|
|
378
|
-
README.md
|
|
379
|
-
LICENSE
|
|
380
|
-
```
|
|
381
|
-
|
|
382
287
|
## License
|
|
383
288
|
|
|
384
289
|
Apache-2.0.
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { captchaModelManager, checksumBuffer } = require('./captcha-model-manager');
|
|
2
|
+
const { clickVisibleTarget } = require('./captcha-pointer');
|
|
2
3
|
|
|
3
4
|
const PROVIDERS = Object.freeze({
|
|
4
5
|
recaptcha_v2: Object.freeze({
|
|
@@ -42,6 +43,17 @@ function normalizePrompt(adapter, text) {
|
|
|
42
43
|
return adapter.cleanPrompt(String(text || '')).replace(/\s+/g, ' ').replace(/[.!]+$/, '').trim();
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function normalizeModelLabel(label) {
|
|
47
|
+
return String(label || '').replace(/\b[a-z]+\b/gi, (word) => {
|
|
48
|
+
const lower = word.toLowerCase();
|
|
49
|
+
if (lower === 'buses') return 'bus';
|
|
50
|
+
if (lower.endsWith('ies')) return `${lower.slice(0, -3)}y`;
|
|
51
|
+
if (/(?:ches|shes|xes|zes)$/.test(lower)) return lower.slice(0, -2);
|
|
52
|
+
if (lower.endsWith('s') && !lower.endsWith('ss') && !lower.endsWith('us')) return lower.slice(0, -1);
|
|
53
|
+
return lower;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
45
57
|
function normalizeBox(detection) {
|
|
46
58
|
const box = detection?.box || detection?.bbox;
|
|
47
59
|
if (Array.isArray(box) && box.length >= 4) {
|
|
@@ -81,16 +93,16 @@ function mapDetectionsToCells(detections, gridBox, cellBoxes, imageSize = null)
|
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
async function classifyGrid(frame, adapter, cells, label, seenTiles) {
|
|
96
|
+
const modelLabel = normalizeModelLabel(label);
|
|
84
97
|
const grid = frame.locator(adapter.grid).first();
|
|
85
98
|
const gridBox = await grid.boundingBox().catch(() => null);
|
|
86
99
|
if (gridBox) {
|
|
87
100
|
const image = await grid.screenshot({ type: 'png' });
|
|
88
|
-
const detections = await captchaModelManager.detect(image,
|
|
89
|
-
if (detections.length === 0) return [];
|
|
101
|
+
const detections = await captchaModelManager.detect(image, modelLabel);
|
|
90
102
|
const cellBoxes = [];
|
|
91
103
|
for (let index = 0; index < await cells.count(); index += 1) cellBoxes.push(await cells.nth(index).boundingBox().catch(() => null));
|
|
92
104
|
const mapped = mapDetectionsToCells(detections, gridBox, cellBoxes, pngDimensions(image));
|
|
93
|
-
if (mapped.length
|
|
105
|
+
if (mapped.length) {
|
|
94
106
|
const changed = [];
|
|
95
107
|
for (const index of mapped) {
|
|
96
108
|
const image = await cells.nth(index).screenshot({ type: 'png' });
|
|
@@ -110,13 +122,19 @@ async function classifyGrid(frame, adapter, cells, label, seenTiles) {
|
|
|
110
122
|
const fingerprint = checksumBuffer(image);
|
|
111
123
|
if (seenTiles.get(index) === fingerprint) continue;
|
|
112
124
|
seenTiles.set(index, fingerprint);
|
|
113
|
-
if ((await captchaModelManager.detect(image,
|
|
125
|
+
if ((await captchaModelManager.detect(image, modelLabel)).length) selected.push(index);
|
|
114
126
|
}
|
|
115
127
|
return selected;
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
async function visibleText(frame, selector) {
|
|
119
|
-
|
|
131
|
+
const matches = frame.locator(selector);
|
|
132
|
+
for (let index = 0; index < await matches.count().catch(() => 0); index += 1) {
|
|
133
|
+
const locator = matches.nth(index);
|
|
134
|
+
if (!await locator.isVisible({ timeout: 250 }).catch(() => false)) continue;
|
|
135
|
+
return locator.innerText({ timeout: 1500 }).catch(() => '');
|
|
136
|
+
}
|
|
137
|
+
return '';
|
|
120
138
|
}
|
|
121
139
|
|
|
122
140
|
async function solveImageGrid(page, { captchaType, deadline, waitForToken, logs = [] }) {
|
|
@@ -128,7 +146,9 @@ async function solveImageGrid(page, { captchaType, deadline, waitForToken, logs
|
|
|
128
146
|
const frame = await findChallengeFrame(page, adapter);
|
|
129
147
|
if (!frame) throw new Error(`${captchaType} image challenge frame disappeared`);
|
|
130
148
|
const rejection = await visibleText(frame, adapter.error);
|
|
131
|
-
if (rejection
|
|
149
|
+
if (rejection && round > 0) {
|
|
150
|
+
throw new Error(`${captchaType} rejected the previous selection: ${rejection.replace(/\s+/g, ' ').trim()}`);
|
|
151
|
+
}
|
|
132
152
|
const prompt = normalizePrompt(adapter, await visibleText(frame, adapter.instruction));
|
|
133
153
|
if (!prompt) throw new Error(`${captchaType} solver could not read the image challenge instruction`);
|
|
134
154
|
if (prompt !== previousPrompt) {
|
|
@@ -141,13 +161,17 @@ async function solveImageGrid(page, { captchaType, deadline, waitForToken, logs
|
|
|
141
161
|
const count = await cells.count();
|
|
142
162
|
if (![9, 16].includes(count)) throw new Error(`Unsupported ${captchaType} grid size (${count} cells)`);
|
|
143
163
|
const selected = await classifyGrid(frame, adapter, cells, prompt, seenTiles);
|
|
144
|
-
for (const index of selected)
|
|
164
|
+
for (const index of selected) {
|
|
165
|
+
const clicked = await clickVisibleTarget(page, cells.nth(index));
|
|
166
|
+
if (!clicked) throw new Error(`${captchaType} grid tile was outside the viewport`);
|
|
167
|
+
}
|
|
145
168
|
totalSelections += selected.length;
|
|
146
169
|
if (!selected.length) break;
|
|
147
170
|
await page.waitForTimeout(600);
|
|
148
171
|
}
|
|
149
172
|
logs.push(`Local ${captchaType} grid round ${round + 1}: selected ${totalSelections} cells for "${prompt}"`);
|
|
150
|
-
await frame.locator(totalSelections ? adapter.submit : adapter.noMatch).first()
|
|
173
|
+
const submitted = await clickVisibleTarget(page, frame.locator(totalSelections ? adapter.submit : adapter.noMatch).first());
|
|
174
|
+
if (!submitted) throw new Error(`${captchaType} verification button was outside the viewport`);
|
|
151
175
|
const token = await waitForToken(page, captchaType, Math.min(2500, Math.max(0, deadline - Date.now())));
|
|
152
176
|
if (token) return token;
|
|
153
177
|
await page.waitForTimeout(350);
|
|
@@ -155,4 +179,4 @@ async function solveImageGrid(page, { captchaType, deadline, waitForToken, logs
|
|
|
155
179
|
return null;
|
|
156
180
|
}
|
|
157
181
|
|
|
158
|
-
module.exports = { PROVIDERS, normalizePrompt, normalizeBox, pngDimensions, mapDetectionsToCells, solveImageGrid };
|
|
182
|
+
module.exports = { PROVIDERS, normalizePrompt, normalizeModelLabel, normalizeBox, pngDimensions, mapDetectionsToCells, classifyGrid, visibleText, solveImageGrid };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { captchaModelManager } = require('./captcha-model-manager');
|
|
2
2
|
const { solveImageGrid, PROVIDERS } = require('./captcha-grid-solver');
|
|
3
|
+
const { clickVisibleTarget } = require('./captcha-pointer');
|
|
3
4
|
|
|
4
5
|
const TOKEN_SELECTORS = Object.freeze({
|
|
5
6
|
recaptcha_v2: ['#g-recaptcha-response', 'textarea[name="g-recaptcha-response"]'],
|
|
@@ -95,8 +96,7 @@ async function clickCheckbox(page, captchaType, timeout = 10_000) {
|
|
|
95
96
|
}).catch(() => true);
|
|
96
97
|
if (!pointerReady) continue;
|
|
97
98
|
|
|
98
|
-
const
|
|
99
|
-
const clicked = await checkbox.click({ timeout: clickTimeout }).then(() => true, () => false);
|
|
99
|
+
const clicked = await clickVisibleTarget(page, checkbox).catch(() => false);
|
|
100
100
|
if (clicked) return true;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
async function viewportSize(page) {
|
|
2
|
+
const configured = page.viewportSize?.();
|
|
3
|
+
if (configured?.width && configured?.height) return configured;
|
|
4
|
+
return page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight })).catch(() => null);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isInsideViewport(box, viewport) {
|
|
8
|
+
if (!box || !viewport) return false;
|
|
9
|
+
const x = box.x + (box.width / 2);
|
|
10
|
+
const y = box.y + (box.height / 2);
|
|
11
|
+
return Number.isFinite(x) && Number.isFinite(y)
|
|
12
|
+
&& x >= 0 && y >= 0 && x <= viewport.width && y <= viewport.height;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Dispatch a trusted pointer click without Playwright's scroll-into-view step.
|
|
17
|
+
* CAPTCHA widgets should already be visible to a human; scrolling the page to
|
|
18
|
+
* satisfy a locator can move or invalidate the challenge between tile clicks.
|
|
19
|
+
*/
|
|
20
|
+
async function clickVisibleTarget(page, locator) {
|
|
21
|
+
if (!page?.mouse?.click) return false;
|
|
22
|
+
const [box, viewport] = await Promise.all([
|
|
23
|
+
locator.boundingBox().catch(() => null),
|
|
24
|
+
viewportSize(page)
|
|
25
|
+
]);
|
|
26
|
+
if (!isInsideViewport(box, viewport)) return false;
|
|
27
|
+
await page.mouse.click(box.x + (box.width / 2), box.y + (box.height / 2));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { clickVisibleTarget, isInsideViewport };
|