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 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
+ [![npm](https://img.shields.io/npm/v/capskip.svg)](https://www.npmjs.com/package/capskip)
3
4
  [![Node.js 18+](https://img.shields.io/badge/node-18%2B-brightgreen.svg)](https://nodejs.org/)
4
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
5
6
  [![Tests](https://github.com/capskip/capskip-node/actions/workflows/ci.yml/badge.svg)](https://github.com/capskip/capskip-node/actions/workflows/ci.yml)
6
7
 
7
- Official Node.js client for the [CapSkip](https://capskip.com) **local** captcha solver.
8
+ **Solve reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, GeeTest, ALTCHA and image captchas from Node.js.**
8
9
 
9
- CapSkip runs on your machine and exposes a standard captcha-solver HTTP API (the familiar `in.php` / `res.php` endpoints). This SDK wraps that API with clean, familiar method names, so you can solve captchas locally — no per-solve API fees beyond your CapSkip license.
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 from [capskip.com](https://capskip.com). Leave it running in the background.
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
- ## Supported captcha types
93
+ ## Why solve captchas locally
62
94
 
63
- | Type | SDK method |
64
- |---|---|
65
- | Image CAPTCHA (distorted text) | `solver.normal(file)` |
66
- | reCAPTCHA v2 (checkbox) | `solver.recaptcha(sitekey, url)` |
67
- | reCAPTCHA v2 Invisible | `solver.recaptcha(sitekey, url, { invisible: 1 })` |
68
- | reCAPTCHA v2 Enterprise | `solver.recaptcha(sitekey, url, { enterprise: 1 })` |
69
- | reCAPTCHA v3 | `solver.recaptcha(sitekey, url, { version: 'v3' })` |
70
- | reCAPTCHA v3 Enterprise | `solver.recaptcha(sitekey, url, { version: 'v3', enterprise: 1 })` |
71
- | Cloudflare Turnstile (widget) | `solver.turnstile(sitekey, url)` |
72
- | Cloudflare Turnstile (challenge page) | `solver.turnstile(sitekey, url, { data, pagedata })` |
73
- | GeeTest v3 (slide) | `solver.geetest(gt, challenge, url)` |
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
- ### With a proxy (reCAPTCHA, Turnstile & GeeTest only)
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 website](https://capskip.com)
286
- - [CapSkip API docs](https://capskip.com/api-docs/)
287
- - [Report an issue](https://github.com/capskip/capskip-node/issues)
288
- - [npm package](https://www.npmjs.com/package/capskip)
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
 
@@ -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 GeeTest — not for image captcha.
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.1.0",
4
- "description": "Node.js SDK for the CapSkip local captcha solver",
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. */