capskip 1.1.0 → 1.2.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/CHANGELOG.md +29 -0
- package/README.md +161 -23
- package/docs/API_REFERENCE.md +125 -1
- package/examples/altcha.js +103 -0
- package/package.json +23 -2
- package/src/apiParams.js +63 -0
- package/src/solver.js +134 -1
- package/types/index.d.ts +39 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0] - 2026-09-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **ALTCHA support** via `altcha(url, options)`. Pass `challengeUrl` for CapSkip
|
|
13
|
+
to fetch the challenge, or `challengeJson` with the document itself (a JSON
|
|
14
|
+
string, or an object which is serialized for you). Sending both is allowed —
|
|
15
|
+
the inline document wins. The result exposes `token` (the value the site's
|
|
16
|
+
`altcha` form field expects) and `number`, the counter that solved it; `code`
|
|
17
|
+
keeps the same raw string.
|
|
18
|
+
- Parameter aliases `challengeUrl`/`challengeURL` for `challenge_url` and
|
|
19
|
+
`challengeJson`/`challengeJSON` for `challenge_json`.
|
|
20
|
+
- `AltchaOptions` and the `token`/`number` result fields in the TypeScript
|
|
21
|
+
definitions.
|
|
22
|
+
- Both ALTCHA generations are handled: the legacy scheme (SHA-1/256/384/512) and
|
|
23
|
+
proof-of-work v2 (PBKDF2 or SHA). Their tokens are shaped differently — a v2
|
|
24
|
+
payload carries no top-level `number`, its counter sitting at
|
|
25
|
+
`solution.counter` — so the counter is taken from the server's own `solution`
|
|
26
|
+
object, the one field both report the same way, and dug out of the token only
|
|
27
|
+
when a poll did not carry it.
|
|
28
|
+
|
|
29
|
+
### Notes
|
|
30
|
+
|
|
31
|
+
- ALTCHA is CPU proof-of-work rather than a browser solve, so it uses
|
|
32
|
+
`defaultTimeout` instead of the longer `recaptchaTimeout` that reCAPTCHA,
|
|
33
|
+
Turnstile and GeeTest use.
|
|
34
|
+
- A proxy passed to `altcha()` applies only to the `challengeUrl` fetch; a task
|
|
35
|
+
carrying its challenge inline never touches the network.
|
|
36
|
+
|
|
8
37
|
## [1.1.0] - 2026-07-26
|
|
9
38
|
|
|
10
39
|
### Added
|
package/README.md
CHANGED
|
@@ -1,20 +1,52 @@
|
|
|
1
|
-
# CapSkip Node.js SDK
|
|
1
|
+
# CapSkip Node.js SDK — Captcha Solver for Node.js
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/capskip)
|
|
3
4
|
[](https://nodejs.org/)
|
|
4
5
|
[](LICENSE)
|
|
5
6
|
[](https://github.com/capskip/capskip-node/actions/workflows/ci.yml)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
**Solve reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, GeeTest, ALTCHA and image captchas from Node.js.**
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Official Node.js client for [CapSkip](https://capskip.com), a **local captcha solver** that runs on your own machine. Licensed once, not billed per solve. TypeScript definitions included.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install capskip
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
CapSkip is a desktop app. It does the solving on your machine and exposes the standard captcha-solver HTTP API — the same `in.php` / `res.php` endpoints every 2captcha-compatible client already speaks — on `127.0.0.1:8080`.
|
|
21
|
+
|
|
22
|
+
This SDK is a thin wrapper over that API, with the method names you would expect: `normal()`, `recaptcha()`, `turnstile()`, `geetest()`, `altcha()`. Nothing leaves your network, and there is no credit balance to keep an eye on.
|
|
23
|
+
|
|
24
|
+
## Supported captcha types
|
|
25
|
+
|
|
26
|
+
| Captcha | Method |
|
|
27
|
+
|---|---|
|
|
28
|
+
| **Image captcha solver** (distorted text / OCR) | `solver.normal(file)` |
|
|
29
|
+
| **reCAPTCHA v2 solver** (checkbox) | `solver.recaptcha(sitekey, url)` |
|
|
30
|
+
| **reCAPTCHA v2 invisible solver** | `solver.recaptcha(sitekey, url, { invisible: 1 })` |
|
|
31
|
+
| **reCAPTCHA Enterprise solver** | `solver.recaptcha(sitekey, url, { enterprise: 1 })` |
|
|
32
|
+
| **reCAPTCHA v3 solver** | `solver.recaptcha(sitekey, url, { version: 'v3', action: 'submit' })` |
|
|
33
|
+
| reCAPTCHA v3 Enterprise | `solver.recaptcha(sitekey, url, { version: 'v3', enterprise: 1 })` |
|
|
34
|
+
| **Cloudflare Turnstile solver** (widget) | `solver.turnstile(sitekey, url)` |
|
|
35
|
+
| Cloudflare Turnstile (challenge page) | `solver.turnstile(sitekey, url, { data, pagedata })` |
|
|
36
|
+
| **GeeTest v3 solver** (slide puzzle) | `solver.geetest(gt, challenge, url)` |
|
|
37
|
+
| **ALTCHA solver** (proof-of-work) | `solver.altcha(url, { challengeUrl })` |
|
|
38
|
+
|
|
39
|
+
**hCaptcha and FunCaptcha/Arkose are not supported.** hCaptcha is the one people misidentify most often, since it also puts a `data-sitekey` on the widget — check for `class="h-captcha"` or a `js.hcaptcha.com` script before reaching for `recaptcha()`.
|
|
40
|
+
|
|
41
|
+
Point the SDK at the [live captcha demo pages](https://capskip.com/captcha-demo/) to sanity-check your setup against real widgets.
|
|
10
42
|
|
|
11
43
|
---
|
|
12
44
|
|
|
13
45
|
## Quick start (5 minutes)
|
|
14
46
|
|
|
15
|
-
### 1. Install CapSkip
|
|
47
|
+
### 1. Install the CapSkip captcha solver
|
|
16
48
|
|
|
17
|
-
Download and run the CapSkip desktop app
|
|
49
|
+
Download and run the CapSkip desktop app — [CapSkipInstaller.msi](https://capskip.com/download/CapSkipInstaller.msi). Leave it running in the background.
|
|
18
50
|
|
|
19
51
|
In CapSkip settings, note:
|
|
20
52
|
|
|
@@ -58,19 +90,20 @@ Every solve method returns a Promise — use `await` or `.then()`.
|
|
|
58
90
|
|
|
59
91
|
---
|
|
60
92
|
|
|
61
|
-
##
|
|
93
|
+
## Why solve captchas locally
|
|
62
94
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
95
|
+
Cloud captcha APIs charge per solve, which turns a retry loop into an expense and routes every page URL and sitekey you touch through someone else's queue.
|
|
96
|
+
|
|
97
|
+
CapSkip flips that around:
|
|
98
|
+
|
|
99
|
+
- **Unlimited solving** — one license, no per-captcha charge, no balance to top up
|
|
100
|
+
- **Runs on `127.0.0.1`** — the SDK never talks to a third-party server
|
|
101
|
+
- **No per-key rate limit** — throughput is whatever your machine can manage
|
|
102
|
+
- **Fast** — image captchas come back in well under a second; a typical reCAPTCHA v2 lands in 30–45 seconds
|
|
103
|
+
|
|
104
|
+
### Coming from 2captcha or Anti-Captcha
|
|
105
|
+
|
|
106
|
+
CapSkip answers on the same `in.php` / `res.php` endpoints, so it works as a **2captcha API alternative**: an existing integration usually needs nothing more than its host pointed at `127.0.0.1:8080`. The [migration notes](https://capskip.com/2captcha-api-alternative/) cover the details, if you would rather keep your current client library than switch to this one.
|
|
74
107
|
|
|
75
108
|
---
|
|
76
109
|
|
|
@@ -99,6 +132,7 @@ const solver = new CapSkip({
|
|
|
99
132
|
port: 8080, // CapSkip port from app settings
|
|
100
133
|
defaultTimeout: 120, // seconds — image captcha polling timeout
|
|
101
134
|
recaptchaTimeout: 300, // seconds — reCAPTCHA / Turnstile / GeeTest polling timeout
|
|
135
|
+
// (ALTCHA uses defaultTimeout — CPU work, not a browser solve)
|
|
102
136
|
pollingInterval: 5, // max seconds between res.php polls (starts at 0.25s, backs off to this)
|
|
103
137
|
});
|
|
104
138
|
```
|
|
@@ -178,7 +212,25 @@ const result = await solver.geetest(
|
|
|
178
212
|
result.challenge, result.validate, result.seccode;
|
|
179
213
|
```
|
|
180
214
|
|
|
181
|
-
###
|
|
215
|
+
### ALTCHA
|
|
216
|
+
|
|
217
|
+
ALTCHA is proof-of-work, not recognition — there is nothing to read, so a solve
|
|
218
|
+
is deterministic and takes milliseconds. Give CapSkip the endpoint that serves
|
|
219
|
+
the challenge, or the challenge document itself.
|
|
220
|
+
|
|
221
|
+
```js
|
|
222
|
+
const result = await solver.altcha('https://example.com/signup', {
|
|
223
|
+
challengeUrl: 'https://example.com/captcha/api/altcha/challenge',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Post this back in the form field the widget uses, named `altcha`
|
|
227
|
+
result.token;
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Challenges expire fast — some sites inside two minutes — so fetch one
|
|
231
|
+
immediately before solving and submit the token promptly.
|
|
232
|
+
|
|
233
|
+
### With a proxy (reCAPTCHA, Turnstile, GeeTest & ALTCHA only)
|
|
182
234
|
|
|
183
235
|
```js
|
|
184
236
|
// Proxy is not supported for image captcha
|
|
@@ -213,6 +265,53 @@ More examples: [`examples/`](examples/)
|
|
|
213
265
|
|
|
214
266
|
---
|
|
215
267
|
|
|
268
|
+
## Puppeteer, Playwright and Selenium
|
|
269
|
+
|
|
270
|
+
The SDK hands back a token; your existing browser tooling does the driving. The shape is the same whichever you use:
|
|
271
|
+
|
|
272
|
+
1. Read the sitekey off the page (`data-sitekey`, or the widget's config object).
|
|
273
|
+
2. Call the matching solve method with that sitekey and the page URL.
|
|
274
|
+
3. Write the token into the response field and submit.
|
|
275
|
+
|
|
276
|
+
### Puppeteer
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
const sitekey = await page.$eval('[data-sitekey]', el => el.dataset.sitekey);
|
|
280
|
+
const { code } = await solver.recaptcha(sitekey, page.url());
|
|
281
|
+
|
|
282
|
+
await page.evaluate(t => {
|
|
283
|
+
document.getElementById('g-recaptcha-response').value = t;
|
|
284
|
+
}, code);
|
|
285
|
+
await page.click('button[type=submit]');
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Playwright
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
const sitekey = await page.getAttribute('[data-sitekey]', 'data-sitekey');
|
|
292
|
+
const { code } = await solver.recaptcha(sitekey, page.url());
|
|
293
|
+
|
|
294
|
+
await page.evaluate(t => {
|
|
295
|
+
document.getElementById('g-recaptcha-response').value = t;
|
|
296
|
+
}, code);
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Selenium WebDriver
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
const el = await driver.findElement(By.css('[data-sitekey]'));
|
|
303
|
+
const sitekey = await el.getAttribute('data-sitekey');
|
|
304
|
+
const { code } = await solver.recaptcha(sitekey, await driver.getCurrentUrl());
|
|
305
|
+
|
|
306
|
+
await driver.executeScript(
|
|
307
|
+
'document.getElementById("g-recaptcha-response").value = arguments[0];', code,
|
|
308
|
+
);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Longer walkthroughs: [Puppeteer](https://capskip.com/puppeteer-captcha-solver/), [Playwright](https://capskip.com/playwright-captcha-solver/) and [Selenium](https://capskip.com/selenium-captcha-solver/).
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
216
315
|
## Return value
|
|
217
316
|
|
|
218
317
|
Every solve method resolves to:
|
|
@@ -226,7 +325,8 @@ Every solve method resolves to:
|
|
|
226
325
|
```
|
|
227
326
|
|
|
228
327
|
GeeTest additionally expands its answer into `challenge`, `validate`, and
|
|
229
|
-
`seccode`, while `code` keeps the raw JSON string.
|
|
328
|
+
`seccode`, while `code` keeps the raw JSON string. ALTCHA adds `token` (the same
|
|
329
|
+
string as `code`) and `number`, the counter that solved it.
|
|
230
330
|
|
|
231
331
|
---
|
|
232
332
|
|
|
@@ -267,6 +367,42 @@ const result: SolveResult = await solver.recaptcha('...', 'https://example.com')
|
|
|
267
367
|
|
|
268
368
|
---
|
|
269
369
|
|
|
370
|
+
## FAQ
|
|
371
|
+
|
|
372
|
+
### How do I solve a captcha in Node.js?
|
|
373
|
+
|
|
374
|
+
Install the CapSkip desktop app, `npm install capskip`, then call the method that matches the widget — `recaptcha()`, `turnstile()`, `geetest()`, `altcha()` or `normal()`. Each returns a Promise that resolves once CapSkip has an answer, giving you a token, or the recognized text in the case of an image captcha.
|
|
375
|
+
|
|
376
|
+
### Is this a free captcha solver?
|
|
377
|
+
|
|
378
|
+
The SDK itself is MIT-licensed and free. Solving needs the CapSkip app, which is bought once rather than metered per captcha, so your cost stops scaling with volume.
|
|
379
|
+
|
|
380
|
+
### Which captchas can it solve?
|
|
381
|
+
|
|
382
|
+
reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3, reCAPTCHA Enterprise, Cloudflare Turnstile, GeeTest v3, ALTCHA, and image/text captchas. Not hCaptcha, and not FunCaptcha/Arkose.
|
|
383
|
+
|
|
384
|
+
### Does it work with Puppeteer and Playwright?
|
|
385
|
+
|
|
386
|
+
Yes — see [above](#puppeteer-playwright-and-selenium). The SDK never touches a browser itself, so it drops into whatever stack you already have, Selenium and plain `fetch` included.
|
|
387
|
+
|
|
388
|
+
### Why is my reCAPTCHA v3 score low?
|
|
389
|
+
|
|
390
|
+
Google derives v3 scores from IP reputation, cookies and browsing history. A solver returns a valid token, but it cannot change how Google grades that token — `score` is forwarded as the target you want, not a guarantee. If a site enforces a high threshold, solve through a cleaner IP using the `proxy` option.
|
|
391
|
+
|
|
392
|
+
### Can I use it as a 2captcha alternative?
|
|
393
|
+
|
|
394
|
+
Yes. CapSkip serves the same endpoints, so you can either move to this SDK or repoint an existing 2captcha client at `127.0.0.1:8080`.
|
|
395
|
+
|
|
396
|
+
### Does the captcha have to be on a public page?
|
|
397
|
+
|
|
398
|
+
For widget captchas, yes — CapSkip loads the URL you pass it. Image captchas only need the image, and that can be a local file.
|
|
399
|
+
|
|
400
|
+
### Does it work with TypeScript and ESM?
|
|
401
|
+
|
|
402
|
+
Types ship in the package. The runtime is CommonJS, so `import { CapSkip } from 'capskip'` works from ESM through Node's interop.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
270
406
|
## Development
|
|
271
407
|
|
|
272
408
|
```bash
|
|
@@ -282,10 +418,12 @@ The test suite uses Node's built-in test runner — no extra dependencies. See [
|
|
|
282
418
|
|
|
283
419
|
## Links
|
|
284
420
|
|
|
285
|
-
- [CapSkip
|
|
286
|
-
- [
|
|
287
|
-
- [
|
|
288
|
-
- [
|
|
421
|
+
- [CapSkip — local captcha solver](https://capskip.com) · [download the app](https://capskip.com/download/CapSkipInstaller.msi)
|
|
422
|
+
- [Captcha demo pages](https://capskip.com/captcha-demo/) — live reCAPTCHA, Turnstile, GeeTest and image widgets
|
|
423
|
+
- [Node.js captcha solver guide](https://capskip.com/nodejs-captcha-solver/)
|
|
424
|
+
- [HTTP API docs](https://capskip.com/api-docs/)
|
|
425
|
+
- Other clients: [Python](https://github.com/capskip/capskip-python) · [PHP](https://github.com/capskip/capskip-php) · [.NET](https://github.com/capskip/capskip-dotnet) · [MCP server for AI agents](https://github.com/capskip/capskip-mcp)
|
|
426
|
+
- [npm package](https://www.npmjs.com/package/capskip) · [report an issue](https://github.com/capskip/capskip-node/issues)
|
|
289
427
|
|
|
290
428
|
---
|
|
291
429
|
|
package/docs/API_REFERENCE.md
CHANGED
|
@@ -23,8 +23,10 @@ method returns a `Promise`.
|
|
|
23
23
|
| reCAPTCHA v3 | `recaptcha(sitekey, url, { version: 'v3' })` | `userrecaptcha` + `version=v3` |
|
|
24
24
|
| Cloudflare Turnstile | `turnstile()` | `turnstile` |
|
|
25
25
|
| GeeTest v3 (slide) | `geetest()` | `geetest` |
|
|
26
|
+
| ALTCHA (proof-of-work) | `altcha()` | `altcha` |
|
|
26
27
|
|
|
27
|
-
**Proxy** is supported for reCAPTCHA, Turnstile, and
|
|
28
|
+
**Proxy** is supported for reCAPTCHA, Turnstile, GeeTest, and ALTCHA — not for image
|
|
29
|
+
captcha. For ALTCHA the proxy is used only for the `challengeUrl` fetch.
|
|
28
30
|
|
|
29
31
|
---
|
|
30
32
|
|
|
@@ -267,6 +269,114 @@ rather than `defaultTimeout`.
|
|
|
267
269
|
|
|
268
270
|
---
|
|
269
271
|
|
|
272
|
+
## 6. ALTCHA — `altcha(url, { ... })`
|
|
273
|
+
|
|
274
|
+
ALTCHA is not a recognition captcha. There is no image, audio or text to read:
|
|
275
|
+
the site issues a proof-of-work challenge and the client must brute-force a
|
|
276
|
+
number that satisfies it. A solve is therefore deterministic and cheap —
|
|
277
|
+
typically milliseconds.
|
|
278
|
+
|
|
279
|
+
### POST `/in.php`
|
|
280
|
+
|
|
281
|
+
| Parameter | Type | Required | Description |
|
|
282
|
+
|---|---|---|---|
|
|
283
|
+
| `key` | string | Yes | CapSkip API key |
|
|
284
|
+
| `method` | string | Yes | `altcha` |
|
|
285
|
+
| `pageurl` | string | Yes | Full URL of the page the challenge came from |
|
|
286
|
+
| `challenge_url` | string | One of the two | Endpoint CapSkip fetches the challenge from |
|
|
287
|
+
| `challenge_json` | string | One of the two | The challenge document itself, as a JSON string |
|
|
288
|
+
| `json` | int | No | `0` plain text, `1` JSON |
|
|
289
|
+
| `proxy` | string | No | Proxy address — used **only** for the `challenge_url` fetch |
|
|
290
|
+
| `proxytype` | string | No | Proxy type |
|
|
291
|
+
|
|
292
|
+
Sending both challenge parameters is allowed: the inline `challengeJson` wins,
|
|
293
|
+
because fetching would only re-obtain what you already supplied.
|
|
294
|
+
|
|
295
|
+
### Getting the challenge
|
|
296
|
+
|
|
297
|
+
Open DevTools → Network on the target page and look for the request the
|
|
298
|
+
`<altcha-widget>` makes for its challenge (often something like
|
|
299
|
+
`/altcha/challenge`). The request URL is your `challengeUrl`; its JSON response
|
|
300
|
+
is your `challengeJson`.
|
|
301
|
+
|
|
302
|
+
The widget attribute that names that endpoint depends on the widget version, so
|
|
303
|
+
read the page source rather than assuming:
|
|
304
|
+
|
|
305
|
+
| Widget | Attribute |
|
|
306
|
+
|---|---|
|
|
307
|
+
| v1 / v2 | `challengeurl="…"`, with a separate `challengejson="…"` for an inline challenge |
|
|
308
|
+
| v3+ | `challenge="…"` — the same attribute takes either a URL or the challenge data |
|
|
309
|
+
|
|
310
|
+
> **Challenges expire, and the window is short** — some sites inside two minutes.
|
|
311
|
+
> Once expired, the site refuses the solution with a bare "verification failed"
|
|
312
|
+
> that looks exactly like a wrong answer. Fetch the challenge immediately before
|
|
313
|
+
> solving and submit the token promptly; do not fetch a batch in advance, and do
|
|
314
|
+
> not hold a token while a user fills in a form.
|
|
315
|
+
>
|
|
316
|
+
> CapSkip refuses an already-expired inline challenge immediately rather than
|
|
317
|
+
> burning CPU on a token that cannot work. If you passed `challengeUrl` and the
|
|
318
|
+
> challenge expired while the job queued, it fetches a fresh one automatically.
|
|
319
|
+
|
|
320
|
+
### SDK usage
|
|
321
|
+
|
|
322
|
+
```js
|
|
323
|
+
// CapSkip fetches the challenge for you
|
|
324
|
+
const result = await solver.altcha('https://example.com/signup', {
|
|
325
|
+
challengeUrl: 'https://example.com/captcha/api/altcha/challenge',
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// …or hand it the document you already have. No network request at all.
|
|
329
|
+
const inline = await solver.altcha('https://example.com/signup', {
|
|
330
|
+
challengeJson: {
|
|
331
|
+
algorithm: 'SHA-256', challenge: '…', salt: '…',
|
|
332
|
+
signature: '…', maxnumber: 1000000,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
result.token; // the base64 payload to post back
|
|
337
|
+
result.number; // the counter that solved it
|
|
338
|
+
result.code; // the same string as token
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`challengeJson` accepts an object (serialized for you) or a JSON string.
|
|
342
|
+
|
|
343
|
+
`number` is reported for both ALTCHA generations. Their tokens differ — a legacy
|
|
344
|
+
payload carries it as a top-level `number`, while a proof-of-work v2 payload has
|
|
345
|
+
none, its counter sitting at `solution.counter` — so it is read from the server's
|
|
346
|
+
own `solution` object, which reports both the same way.
|
|
347
|
+
|
|
348
|
+
Post the token back in the form field the widget uses, named `altcha`:
|
|
349
|
+
|
|
350
|
+
```js
|
|
351
|
+
await fetch(SIGNUP_URL, {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
body: new URLSearchParams({
|
|
354
|
+
email: 'someone@example.com',
|
|
355
|
+
altcha: result.token,
|
|
356
|
+
}),
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Do not re-encode, trim or re-order the token: it is base64 of a JSON document
|
|
361
|
+
whose fields are covered by the server's HMAC signature, so any modification
|
|
362
|
+
invalidates it. Some integrations read the payload from a JSON body field
|
|
363
|
+
instead — check what the page's own submit sends and mirror it.
|
|
364
|
+
|
|
365
|
+
Unlike GeeTest and reCAPTCHA this is CPU proof-of-work rather than a browser
|
|
366
|
+
solve, so it uses `defaultTimeout`, not `recaptchaTimeout`.
|
|
367
|
+
|
|
368
|
+
### Algorithms
|
|
369
|
+
|
|
370
|
+
CapSkip supports the legacy scheme (SHA-1/256/384/512) and PoW v2 with PBKDF2 or
|
|
371
|
+
SHA. **Argon2id and scrypt are refused**, not attempted: a task using one returns
|
|
372
|
+
`ERROR_CAPTCHA_UNSOLVABLE` and is never retried. ALTCHA itself recommends PBKDF2
|
|
373
|
+
as the default, so this affects a minority of sites.
|
|
374
|
+
|
|
375
|
+
All three widget types (`native`, `checkbox`, `switch`) work — the distinction is
|
|
376
|
+
purely visual and never reaches CapSkip.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
270
380
|
## Return value
|
|
271
381
|
|
|
272
382
|
Every solve method resolves to:
|
|
@@ -279,6 +389,18 @@ Every solve method resolves to:
|
|
|
279
389
|
}
|
|
280
390
|
```
|
|
281
391
|
|
|
392
|
+
ALTCHA additionally exposes `token` (the same string as `code`, named for the
|
|
393
|
+
form field it goes in) and `number`, the counter that solved it:
|
|
394
|
+
|
|
395
|
+
```js
|
|
396
|
+
{
|
|
397
|
+
captchaId: '12345',
|
|
398
|
+
code: 'eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi…',
|
|
399
|
+
token: 'eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi…',
|
|
400
|
+
number: 9661,
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
282
404
|
GeeTest additionally expands its answer into `challenge`, `validate`, and
|
|
283
405
|
`seccode` (`code` keeps the raw JSON string):
|
|
284
406
|
|
|
@@ -307,6 +429,8 @@ Convenience aliases mapped before sending to CapSkip:
|
|
|
307
429
|
| `data_s` | `data-s` |
|
|
308
430
|
| `apiServer` | `api_server` |
|
|
309
431
|
| `api_subdomain` | `api_server` |
|
|
432
|
+
| `challengeUrl` / `challengeURL` | `challenge_url` |
|
|
433
|
+
| `challengeJson` / `challengeJSON` | `challenge_json` |
|
|
310
434
|
| `proxy` object | `proxy` + `proxytype` strings |
|
|
311
435
|
|
|
312
436
|
```js
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Solve an ALTCHA proof-of-work challenge.
|
|
5
|
+
*
|
|
6
|
+
* ALTCHA is not a recognition captcha -- there is nothing to read. The site
|
|
7
|
+
* issues a challenge and the browser must brute-force a number that satisfies
|
|
8
|
+
* it. CapSkip does that work for you, in milliseconds.
|
|
9
|
+
*
|
|
10
|
+
* You need the challenge, in one of two forms:
|
|
11
|
+
*
|
|
12
|
+
* * `challengeUrl` - the endpoint that serves it; CapSkip fetches it for you
|
|
13
|
+
* * `challengeJson` - the challenge document itself, if you already have it
|
|
14
|
+
*
|
|
15
|
+
* To find them, open DevTools -> Network on the target page and look for the
|
|
16
|
+
* request the `<altcha-widget>` makes for its challenge (often something like
|
|
17
|
+
* `/altcha/challenge`). The request URL is your `challengeUrl`; its JSON
|
|
18
|
+
* response is your `challengeJson`.
|
|
19
|
+
*
|
|
20
|
+
* Note the widget attribute that names the endpoint changed between versions:
|
|
21
|
+
* v1/v2 use `challengeurl="..."`, while v3+ uses `challenge="..."` for both a
|
|
22
|
+
* URL and inline data. Read the page source rather than assuming.
|
|
23
|
+
*
|
|
24
|
+
* Challenges expire fast -- some sites inside two minutes -- so fetch one
|
|
25
|
+
* immediately before solving and post the token promptly. An expired challenge
|
|
26
|
+
* is rejected with a bare "verification failed" that looks exactly like a wrong
|
|
27
|
+
* answer.
|
|
28
|
+
*
|
|
29
|
+
* This example issues its own challenge the way a site's server would, so it
|
|
30
|
+
* runs as-is with no third-party dependency. Swap in your target's endpoint to
|
|
31
|
+
* use it for real.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const crypto = require('crypto');
|
|
35
|
+
|
|
36
|
+
const { CapSkip } = require('../src');
|
|
37
|
+
|
|
38
|
+
const solver = new CapSkip({
|
|
39
|
+
apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
|
|
40
|
+
host: process.env.CAPSKIP_HOST || '127.0.0.1',
|
|
41
|
+
port: Number(process.env.CAPSKIP_PORT || 8080),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const PAGE_URL = 'https://example.com/signup';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Mint an ALTCHA challenge, exactly as a site's own server would.
|
|
48
|
+
*
|
|
49
|
+
* Replace this with a fetch of your target's challenge endpoint -- or skip it
|
|
50
|
+
* entirely and pass `challengeUrl` so CapSkip does the fetching.
|
|
51
|
+
*/
|
|
52
|
+
function issueChallenge(number = 54321) {
|
|
53
|
+
const salt = `${crypto.randomBytes(12).toString('hex')}?expires=${
|
|
54
|
+
Math.floor(Date.now() / 1000) + 600}`;
|
|
55
|
+
return {
|
|
56
|
+
algorithm: 'SHA-256',
|
|
57
|
+
challenge: crypto.createHash('sha256').update(`${salt}${number}`).digest('hex'),
|
|
58
|
+
salt,
|
|
59
|
+
signature: '0'.repeat(64),
|
|
60
|
+
maxnumber: 100000,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
(async () => {
|
|
65
|
+
// --- Option A: you already have the challenge document ---------------------
|
|
66
|
+
// No network request at all: CapSkip solves it locally.
|
|
67
|
+
const result = await solver.altcha(PAGE_URL, { challengeJson: issueChallenge() });
|
|
68
|
+
|
|
69
|
+
console.log('Captcha ID:', result.captchaId);
|
|
70
|
+
console.log('Number: ', result.number);
|
|
71
|
+
console.log('Token: ', `${result.token.slice(0, 60)}...`);
|
|
72
|
+
|
|
73
|
+
// --- Option B: let CapSkip fetch the challenge ------------------------------
|
|
74
|
+
// Point it at the endpoint the widget calls. Add `proxy` if the endpoint
|
|
75
|
+
// should be fetched from a particular IP -- the proxy is used only for that
|
|
76
|
+
// fetch, never for the solve itself.
|
|
77
|
+
//
|
|
78
|
+
// const result = await solver.altcha(PAGE_URL, {
|
|
79
|
+
// challengeUrl: 'https://example.com/captcha/api/altcha/challenge',
|
|
80
|
+
// proxy: { type: 'HTTP', uri: 'login:password@1.2.3.4:8080' },
|
|
81
|
+
// });
|
|
82
|
+
|
|
83
|
+
// Post the token back in the form field the widget uses, named `altcha`:
|
|
84
|
+
//
|
|
85
|
+
// await fetch(SIGNUP_URL, {
|
|
86
|
+
// method: 'POST',
|
|
87
|
+
// body: new URLSearchParams({
|
|
88
|
+
// email: 'someone@example.com',
|
|
89
|
+
// altcha: result.token,
|
|
90
|
+
// }),
|
|
91
|
+
// });
|
|
92
|
+
//
|
|
93
|
+
// Do not re-encode, trim or re-order it. The token is base64 of a JSON
|
|
94
|
+
// document whose fields are covered by the server's HMAC signature, so any
|
|
95
|
+
// modification invalidates it.
|
|
96
|
+
|
|
97
|
+
// `code` holds the same string as `token`, which is what you forward if you
|
|
98
|
+
// are porting code written against another solver's API.
|
|
99
|
+
console.assert(result.code === result.token);
|
|
100
|
+
})().catch((err) => {
|
|
101
|
+
console.error(err);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capskip",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Node.js
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Captcha solver for Node.js: solve reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest and image captchas with CapSkip, a local unlimited captcha solver with no per-solve fees.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -23,10 +23,31 @@
|
|
|
23
23
|
"keywords": [
|
|
24
24
|
"captcha",
|
|
25
25
|
"captcha-solver",
|
|
26
|
+
"captcha-solving",
|
|
27
|
+
"captcha-bypass",
|
|
28
|
+
"unlimited-captcha-solver",
|
|
29
|
+
"local-captcha-solver",
|
|
30
|
+
"free-captcha-solver",
|
|
26
31
|
"recaptcha",
|
|
32
|
+
"recaptcha-solver",
|
|
33
|
+
"recaptcha-v2",
|
|
34
|
+
"recaptcha-v3",
|
|
35
|
+
"recaptcha-enterprise",
|
|
27
36
|
"cloudflare",
|
|
28
37
|
"turnstile",
|
|
38
|
+
"turnstile-solver",
|
|
39
|
+
"cloudflare-turnstile",
|
|
29
40
|
"geetest",
|
|
41
|
+
"geetest-solver",
|
|
42
|
+
"image-captcha",
|
|
43
|
+
"ocr",
|
|
44
|
+
"2captcha-alternative",
|
|
45
|
+
"anti-captcha-alternative",
|
|
46
|
+
"puppeteer",
|
|
47
|
+
"playwright",
|
|
48
|
+
"selenium",
|
|
49
|
+
"browser-automation",
|
|
50
|
+
"web-scraping",
|
|
30
51
|
"capskip",
|
|
31
52
|
"automation"
|
|
32
53
|
],
|
package/src/apiParams.js
CHANGED
|
@@ -26,6 +26,11 @@ const GEETEST_SUBMIT = new Set([
|
|
|
26
26
|
'proxy', 'proxytype',
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
+
const ALTCHA_SUBMIT = new Set([
|
|
30
|
+
'method', 'pageurl', 'challenge_url', 'challenge_json', 'json',
|
|
31
|
+
'proxy', 'proxytype',
|
|
32
|
+
]);
|
|
33
|
+
|
|
29
34
|
// The only values CapSkip maps to a proxy scheme; it answers
|
|
30
35
|
// ERROR_BAD_PARAMETERS for anything else, SOCKS4 included. Matched
|
|
31
36
|
// case-insensitively, as the server does.
|
|
@@ -39,6 +44,10 @@ const PARAM_ALIASES = {
|
|
|
39
44
|
data_s: 'data-s',
|
|
40
45
|
apiServer: 'api_server',
|
|
41
46
|
api_subdomain: 'api_server',
|
|
47
|
+
challengeUrl: 'challenge_url',
|
|
48
|
+
challengeURL: 'challenge_url',
|
|
49
|
+
challengeJson: 'challenge_json',
|
|
50
|
+
challengeJSON: 'challenge_json',
|
|
42
51
|
};
|
|
43
52
|
|
|
44
53
|
function has(obj, key) {
|
|
@@ -176,6 +185,54 @@ function validateGeetestSubmit(params) {
|
|
|
176
185
|
}
|
|
177
186
|
}
|
|
178
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Drop unset challenge params and serialize an inline challenge document.
|
|
190
|
+
*
|
|
191
|
+
* `altcha(url, { challengeUrl, challengeJson })` is normally called with one of
|
|
192
|
+
* the two left out, and the form body can only carry a string -- so a document
|
|
193
|
+
* passed as an object is serialized rather than stringified into
|
|
194
|
+
* "[object Object]". Mirrors the server, which reads a JSON-body `null` as
|
|
195
|
+
* "not sent".
|
|
196
|
+
*/
|
|
197
|
+
function normalizeAltchaSubmit(params) {
|
|
198
|
+
const out = {};
|
|
199
|
+
for (const [key, value] of Object.entries(params)) {
|
|
200
|
+
if (value !== undefined && value !== null) {
|
|
201
|
+
out[key] = value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const challenge = out.challenge_json;
|
|
206
|
+
if (typeof challenge === 'object') {
|
|
207
|
+
out.challenge_json = JSON.stringify(challenge);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function validateAltchaSubmit(params) {
|
|
214
|
+
if (!params.pageurl) {
|
|
215
|
+
throw new ValidationException("'pageurl' is required for ALTCHA.");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// CapSkip answers ERROR_BAD_PARAMETERS when neither is sent. Sending both is
|
|
219
|
+
// deliberately allowed -- the inline document simply wins, because fetching
|
|
220
|
+
// would only re-obtain what the caller already supplied.
|
|
221
|
+
if (!params.challenge_url && !params.challenge_json) {
|
|
222
|
+
throw new ValidationException(
|
|
223
|
+
"ALTCHA needs a challenge: pass 'challenge_url' for CapSkip to fetch it, "
|
|
224
|
+
+ "or 'challenge_json' with the challenge document itself.",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const unknown = unknownKeys(params, ALTCHA_SUBMIT);
|
|
229
|
+
if (unknown.length > 0) {
|
|
230
|
+
throw new ValidationException(
|
|
231
|
+
`Unsupported parameters for ALTCHA: ${reprList(unknown)}.`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
179
236
|
function validateProxyType(params) {
|
|
180
237
|
const proxytype = params.proxytype;
|
|
181
238
|
if (proxytype === undefined || proxytype === null || proxytype === '') {
|
|
@@ -201,6 +258,9 @@ function prepareSubmitParams(params, captchaType, version = 'v2') {
|
|
|
201
258
|
validateTurnstileSubmit(prepared);
|
|
202
259
|
} else if (captchaType === 'geetest') {
|
|
203
260
|
validateGeetestSubmit(prepared);
|
|
261
|
+
} else if (captchaType === 'altcha') {
|
|
262
|
+
prepared = normalizeAltchaSubmit(prepared);
|
|
263
|
+
validateAltchaSubmit(prepared);
|
|
204
264
|
}
|
|
205
265
|
|
|
206
266
|
// Skipped for 'normal', which rejects proxy outright with a clearer message.
|
|
@@ -217,6 +277,7 @@ module.exports = {
|
|
|
217
277
|
RECAPTCHA_V3_SUBMIT,
|
|
218
278
|
TURNSTILE_SUBMIT,
|
|
219
279
|
GEETEST_SUBMIT,
|
|
280
|
+
ALTCHA_SUBMIT,
|
|
220
281
|
PROXY_TYPES,
|
|
221
282
|
PARAM_ALIASES,
|
|
222
283
|
applyParamAliases,
|
|
@@ -225,6 +286,8 @@ module.exports = {
|
|
|
225
286
|
validateRecaptchaSubmit,
|
|
226
287
|
validateTurnstileSubmit,
|
|
227
288
|
validateGeetestSubmit,
|
|
289
|
+
normalizeAltchaSubmit,
|
|
290
|
+
validateAltchaSubmit,
|
|
228
291
|
validateProxyType,
|
|
229
292
|
prepareSubmitParams,
|
|
230
293
|
};
|
package/src/solver.js
CHANGED
|
@@ -83,6 +83,13 @@ function applyPollResult(result, polled) {
|
|
|
83
83
|
if (userAgent) {
|
|
84
84
|
result.userAgent = userAgent;
|
|
85
85
|
}
|
|
86
|
+
// ALTCHA's createTask-shaped `solution` object. Carried through so
|
|
87
|
+
// applyAltchaSolution can read the counter the server already worked out,
|
|
88
|
+
// which is the only reliable source for a proof-of-work v2 answer; that
|
|
89
|
+
// function deletes it, so it never reaches the caller.
|
|
90
|
+
if (polled.solution !== null && typeof polled.solution === 'object') {
|
|
91
|
+
result.solution = polled.solution;
|
|
92
|
+
}
|
|
86
93
|
} else {
|
|
87
94
|
result.code = polled;
|
|
88
95
|
}
|
|
@@ -126,6 +133,90 @@ function applyGeetestSolution(result) {
|
|
|
126
133
|
return result;
|
|
127
134
|
}
|
|
128
135
|
|
|
136
|
+
// ALTCHA answers come back as a base64 payload: the challenge document with the
|
|
137
|
+
// winning counter added. That payload is what the site's own `altcha` form field
|
|
138
|
+
// carries, so it is posted back verbatim.
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Expose the answer as `token`, and the winning counter as `number`.
|
|
142
|
+
*
|
|
143
|
+
* `code` keeps the raw answer so callers that forward it verbatim (or that were
|
|
144
|
+
* written against another solver's API) keep working; `token` is the same string,
|
|
145
|
+
* named for the form field it goes into. If the payload does not decode, the
|
|
146
|
+
* result is returned untouched rather than masking the server's reply.
|
|
147
|
+
*/
|
|
148
|
+
/**
|
|
149
|
+
* Dig the winning counter out of a token, whichever scheme produced it.
|
|
150
|
+
*
|
|
151
|
+
* The two ALTCHA generations nest it differently: a legacy payload is the
|
|
152
|
+
* challenge document with a top-level `number` added, while a proof-of-work v2
|
|
153
|
+
* payload is `{ challenge: {...}, solution: { counter: N, ... } }` and has no
|
|
154
|
+
* `number` at all. Returns undefined if the payload does not decode.
|
|
155
|
+
*/
|
|
156
|
+
function tokenCounter(code) {
|
|
157
|
+
let payload;
|
|
158
|
+
try {
|
|
159
|
+
const decoded = Buffer.from(code, 'base64');
|
|
160
|
+
// Buffer.from is lenient: it drops invalid characters instead of throwing,
|
|
161
|
+
// so round-trip to confirm the input really was base64 before trusting it.
|
|
162
|
+
if (decoded.toString('base64').replace(/=+$/, '') !== code.replace(/=+$/, '')) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
payload = JSON.parse(decoded.toString('utf8'));
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (payload.number !== undefined) {
|
|
175
|
+
return payload.number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (payload.solution !== null && typeof payload.solution === 'object') {
|
|
179
|
+
return payload.solution.counter;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Expose the answer as `token`, and the winning counter as `number`.
|
|
187
|
+
*
|
|
188
|
+
* `code` keeps the raw answer so callers that forward it verbatim (or that were
|
|
189
|
+
* written against another solver's API) keep working; `token` is the same
|
|
190
|
+
* string, named for the form field it goes into.
|
|
191
|
+
*
|
|
192
|
+
* The counter comes from the server's own `solution` object when the poll
|
|
193
|
+
* carried one, because that is the single field both ALTCHA generations report
|
|
194
|
+
* the same way. Only if it is absent — a plain-text poll — is it dug out of the
|
|
195
|
+
* token, which is shaped differently per scheme. If neither yields one, the
|
|
196
|
+
* result keeps its token and simply has no `number`, rather than masking the
|
|
197
|
+
* server's reply.
|
|
198
|
+
*/
|
|
199
|
+
function applyAltchaSolution(result) {
|
|
200
|
+
const code = result.code || '';
|
|
201
|
+
result.token = code;
|
|
202
|
+
|
|
203
|
+
const { solution } = result;
|
|
204
|
+
delete result.solution;
|
|
205
|
+
|
|
206
|
+
let number = solution !== null && typeof solution === 'object'
|
|
207
|
+
? solution.number
|
|
208
|
+
: undefined;
|
|
209
|
+
if (number === undefined) {
|
|
210
|
+
number = tokenCounter(code);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (number !== undefined) {
|
|
214
|
+
result.number = number;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
129
220
|
// CapSkip's in.php returns OK|<id> by default, or {"status":1,"request":"<id>"}
|
|
130
221
|
// when the submit carried json=1. Accept both so submitting with json=1 works.
|
|
131
222
|
function parseSubmitResponse(response) {
|
|
@@ -149,7 +240,7 @@ function parseSubmitResponse(response) {
|
|
|
149
240
|
throw new ApiException(`cannot recognize response ${response}`);
|
|
150
241
|
}
|
|
151
242
|
|
|
152
|
-
/** Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile, GeeTest v3). */
|
|
243
|
+
/** Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile, GeeTest v3, ALTCHA). */
|
|
153
244
|
class CapSkip {
|
|
154
245
|
constructor({
|
|
155
246
|
apiKey = 'capskip',
|
|
@@ -230,6 +321,44 @@ class CapSkip {
|
|
|
230
321
|
return applyGeetestSolution(result);
|
|
231
322
|
}
|
|
232
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Solve an ALTCHA proof-of-work challenge.
|
|
326
|
+
*
|
|
327
|
+
* Pass `challengeUrl` for CapSkip to fetch the challenge itself, or
|
|
328
|
+
* `challengeJson` with the document you already have (a JSON string, or an
|
|
329
|
+
* object which is serialized for you). Sending both is allowed -- the inline
|
|
330
|
+
* document wins. A proxy applies only to the `challengeUrl` fetch.
|
|
331
|
+
*
|
|
332
|
+
* Challenges expire fast -- some sites inside two minutes -- and an expired one
|
|
333
|
+
* is refused with a bare "verification failed" that looks exactly like a wrong
|
|
334
|
+
* answer. Fetch the challenge immediately before calling, and post the token
|
|
335
|
+
* promptly.
|
|
336
|
+
*
|
|
337
|
+
* The result carries the raw answer as `code`, the same string as `token`
|
|
338
|
+
* (what the site's `altcha` form field expects, verbatim), and the counter
|
|
339
|
+
* that solved it as `number`.
|
|
340
|
+
*/
|
|
341
|
+
async altcha(url, options = {}) {
|
|
342
|
+
// An unset challenge param is dropped rather than sent as undefined, so
|
|
343
|
+
// `altcha(url, { challengeUrl, challengeJson })` works with either left out.
|
|
344
|
+
const given = {};
|
|
345
|
+
for (const [key, value] of Object.entries(options)) {
|
|
346
|
+
if (value !== undefined && value !== null) {
|
|
347
|
+
given[key] = value;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Unlike GeeTest and reCAPTCHA this is CPU proof-of-work measured in
|
|
352
|
+
// milliseconds, not a browser solve, so it keeps the default timeout.
|
|
353
|
+
const result = await this.solve({
|
|
354
|
+
url,
|
|
355
|
+
...given,
|
|
356
|
+
method: 'altcha',
|
|
357
|
+
poll_json: 1,
|
|
358
|
+
});
|
|
359
|
+
return applyAltchaSolution(result);
|
|
360
|
+
}
|
|
361
|
+
|
|
233
362
|
async solve(options = {}) {
|
|
234
363
|
const {
|
|
235
364
|
timeout = 0,
|
|
@@ -318,6 +447,9 @@ class CapSkip {
|
|
|
318
447
|
if (method === 'geetest') {
|
|
319
448
|
return prepareSubmitParams(params, 'geetest');
|
|
320
449
|
}
|
|
450
|
+
if (method === 'altcha') {
|
|
451
|
+
return prepareSubmitParams(params, 'altcha');
|
|
452
|
+
}
|
|
321
453
|
return applyProxy(applyParamAliases(params));
|
|
322
454
|
}
|
|
323
455
|
}
|
|
@@ -330,4 +462,5 @@ module.exports = {
|
|
|
330
462
|
parseSubmitResponse,
|
|
331
463
|
applyPollResult,
|
|
332
464
|
applyGeetestSolution,
|
|
465
|
+
applyAltchaSolution,
|
|
333
466
|
};
|
package/types/index.d.ts
CHANGED
|
@@ -31,7 +31,8 @@ export interface SolveResult {
|
|
|
31
31
|
/**
|
|
32
32
|
* The solution: recognized text for images, a token otherwise. For GeeTest
|
|
33
33
|
* this is the raw JSON string CapSkip returns — prefer the parsed
|
|
34
|
-
* `challenge` / `validate` / `seccode` fields below.
|
|
34
|
+
* `challenge` / `validate` / `seccode` fields below. For ALTCHA it is the
|
|
35
|
+
* base64 token, also exposed as `token`.
|
|
35
36
|
*/
|
|
36
37
|
code: string;
|
|
37
38
|
/** Turnstile only — the User-Agent to use when submitting the token. */
|
|
@@ -42,6 +43,13 @@ export interface SolveResult {
|
|
|
42
43
|
validate?: string;
|
|
43
44
|
/** GeeTest only — the `geetest_seccode` value to post back. */
|
|
44
45
|
seccode?: string;
|
|
46
|
+
/**
|
|
47
|
+
* ALTCHA only — the base64 payload to post back in the site's `altcha` form
|
|
48
|
+
* field. The same string as `code`, named for where it goes.
|
|
49
|
+
*/
|
|
50
|
+
token?: string;
|
|
51
|
+
/** ALTCHA only — the counter that solved the challenge. */
|
|
52
|
+
number?: number;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/** Extra options for {@link CapSkip.normal}. */
|
|
@@ -111,6 +119,28 @@ export interface GeetestOptions {
|
|
|
111
119
|
[key: string]: unknown;
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
/** Extra options for {@link CapSkip.altcha}. */
|
|
123
|
+
export interface AltchaOptions {
|
|
124
|
+
/** Endpoint CapSkip fetches the challenge from. */
|
|
125
|
+
challengeUrl?: string;
|
|
126
|
+
/** Endpoint CapSkip fetches the challenge from. */
|
|
127
|
+
challenge_url?: string;
|
|
128
|
+
/**
|
|
129
|
+
* The challenge document itself. An object is serialized for you; a string is
|
|
130
|
+
* sent as-is.
|
|
131
|
+
*/
|
|
132
|
+
challengeJson?: string | Record<string, unknown>;
|
|
133
|
+
/** The challenge document itself. */
|
|
134
|
+
challenge_json?: string | Record<string, unknown>;
|
|
135
|
+
/** `1` to request the raw JSON response from CapSkip. */
|
|
136
|
+
json?: number;
|
|
137
|
+
/** Proxy — used only for the `challengeUrl` fetch, never for the solve. */
|
|
138
|
+
proxy?: Proxy | string;
|
|
139
|
+
/** Proxy type when `proxy` is a bare string. */
|
|
140
|
+
proxytype?: string;
|
|
141
|
+
[key: string]: unknown;
|
|
142
|
+
}
|
|
143
|
+
|
|
114
144
|
/** Options for the {@link CapSkip.solve} manual workflow. */
|
|
115
145
|
export interface SolveOptions {
|
|
116
146
|
/** Poll timeout in seconds (falls back to the configured default). */
|
|
@@ -150,6 +180,14 @@ export class CapSkip {
|
|
|
150
180
|
url: string,
|
|
151
181
|
options?: GeetestOptions,
|
|
152
182
|
): Promise<SolveResult>;
|
|
183
|
+
/**
|
|
184
|
+
* Solve an ALTCHA proof-of-work challenge.
|
|
185
|
+
*
|
|
186
|
+
* Pass `challengeUrl` for CapSkip to fetch the challenge, or `challengeJson`
|
|
187
|
+
* with the document itself. Sending both is allowed — the inline document
|
|
188
|
+
* wins. Challenges expire fast, so fetch one immediately before calling.
|
|
189
|
+
*/
|
|
190
|
+
altcha(url: string, options?: AltchaOptions): Promise<SolveResult>;
|
|
153
191
|
/** Submit then poll to completion. Used by the higher-level solve methods. */
|
|
154
192
|
solve(options?: SolveOptions): Promise<SolveResult>;
|
|
155
193
|
/** Submit a captcha without polling; resolves to the captcha id. */
|