capskip 1.5.0 → 1.6.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,36 @@ 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.6.0] - 2026-11-05
9
+
10
+ ### Added
11
+
12
+ - **hCaptcha support** via `hcaptcha(sitekey, url, options)`, sent as
13
+ `method=hcaptcha`. The arguments follow 2Captcha's `hcaptcha({ sitekey,
14
+ pageurl, ... })` in this SDK's usual shape; `pageurl` in `options` is accepted
15
+ in place of `url`. Checkbox, invisible and hCaptcha Enterprise widgets all use
16
+ this one method.
17
+ - Optional parameters: `invisible` (`1` or `true` for an invisible widget),
18
+ `data` (the Enterprise `rqdata`), `userAgent` / `useragent`, and `proxy`.
19
+ - The result carries the token as both `code` and `token`, `respKey` (always
20
+ present, empty when the widget handed out none) and `userAgent`, the
21
+ User-Agent the token was earned with. Polled results now carry a `respKey`
22
+ field whenever the server sends one.
23
+ - TypeScript definitions for `HCaptchaOptions` and the `hcaptcha()` method;
24
+ `SolveResult.respKey` now documents hCaptcha.
25
+ - `examples/hcaptcha.js`.
26
+
27
+ ### Notes
28
+
29
+ - Refused locally, before a round-trip: a missing or blank sitekey, a page URL
30
+ that is missing or not an absolute http(s) address, an `invisible` value other
31
+ than 0/1/true/false, and any parameter the CapSkip hCaptcha API does not take
32
+ (2Captcha's `domain` among them).
33
+ - `invisible` is sent as `1`, or left out when off, so a boolean never reaches
34
+ the server as the string `true` or `false`. An optional passed as `''` is
35
+ treated as not sent.
36
+ - hCaptcha uses `recaptchaTimeout`: the widget is passed in a real browser.
37
+
8
38
  ## [1.5.0] - 2026-11-02
9
39
 
10
40
  ### Added
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
6
  [![Tests](https://github.com/capskip/capskip-node/actions/workflows/ci.yml/badge.svg)](https://github.com/capskip/capskip-node/actions/workflows/ci.yml)
7
7
 
8
- **Solve reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, GeeTest, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF, DataDome and image captchas from Node.js.**
8
+ **Solve reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, GeeTest, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF, DataDome, hCaptcha and image captchas from Node.js.**
9
9
 
10
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
11
 
@@ -19,7 +19,7 @@ npm install capskip
19
19
 
20
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
21
 
22
- This SDK is a thin wrapper over that API, with the method names you would expect: `normal()`, `recaptcha()`, `turnstile()`, `geetest()`, `altcha()`, `capy()`, `captchafox()`, `friendlyCaptcha()`, `amazonWaf()`, `dataDome()`. Nothing leaves your network, and there is no credit balance to keep an eye on.
22
+ This SDK is a thin wrapper over that API, with the method names you would expect: `normal()`, `recaptcha()`, `turnstile()`, `geetest()`, `altcha()`, `capy()`, `captchafox()`, `friendlyCaptcha()`, `amazonWaf()`, `dataDome()`, `hcaptcha()`. Nothing leaves your network, and there is no credit balance to keep an eye on.
23
23
 
24
24
  ## Supported captcha types
25
25
 
@@ -41,8 +41,9 @@ This SDK is a thin wrapper over that API, with the method names you would expect
41
41
  | **AWS WAF solver** (challenge page) | `solver.amazonWaf(sitekey, url, { iv, context })` |
42
42
  | AWS WAF (widget) | `solver.amazonWaf(sitekey, url, { jsapiScript })` |
43
43
  | **DataDome solver** (slider and device check) | `solver.dataDome(captchaUrl, url)` |
44
+ | **hCaptcha solver** (checkbox, invisible and Enterprise) | `solver.hcaptcha(sitekey, url)` |
44
45
 
45
- **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()`.
46
+ **FunCaptcha/Arkose is not supported.** hCaptcha and reCAPTCHA both put a `data-sitekey` on the widget, so tell them apart before choosing a method: `class="h-captcha"` or a `js.hcaptcha.com` script means `hcaptcha()`, `class="g-recaptcha"` means `recaptcha()`.
46
47
 
47
48
  Point the SDK at the [live captcha demo pages](https://capskip.com/captcha-demo/) to sanity-check your setup against real widgets.
48
49
 
@@ -138,7 +139,7 @@ const solver = new CapSkip({
138
139
  port: 8080, // CapSkip port from app settings
139
140
  defaultTimeout: 120, // seconds — image captcha polling timeout
140
141
  recaptchaTimeout: 300, // seconds — reCAPTCHA / Turnstile / GeeTest / CaptchaFox /
141
- // Friendly Captcha / AWS WAF / DataDome polling timeout
142
+ // Friendly Captcha / AWS WAF / DataDome / hCaptcha polling timeout
142
143
  // (ALTCHA and Capy use defaultTimeout — neither is a browser solve)
143
144
  pollingInterval: 5, // max seconds between res.php polls (starts at 0.25s, backs off to this)
144
145
  });
@@ -420,6 +421,56 @@ A block page whose `dd` object carries `'t':'bv'` means DataDome has banned the
420
421
  address that fetched it; no solve from there can pass, so it is refused before
421
422
  the request is sent. Change the address and request the page again.
422
423
 
424
+ ### hCaptcha
425
+
426
+ hCaptcha is the checkbox widget behind many sign-up, login and checkout forms.
427
+ CapSkip passes the widget for the sitekey and page you send and returns the
428
+ token. The arguments follow 2Captcha's `hcaptcha({ sitekey, pageurl, ... })` in
429
+ this SDK's usual shape, `hcaptcha(sitekey, url, options)`, and invisible and
430
+ Enterprise widgets use the same method.
431
+
432
+ ```js
433
+ const result = await solver.hcaptcha(
434
+ 'a5f74b19-9e45-40e0-b45d-47ff91b7a6c2', // data-sitekey of the widget
435
+ 'https://accounts.hcaptcha.com/demo', // the page the widget is on
436
+ {
437
+ // invisible: 1, // for a widget with data-size="invisible"
438
+ // data: '...', // hCaptcha Enterprise: the rqdata the page passes to hcaptcha.setData()
439
+ // userAgent: 'Mozilla/5.0 ...',// the User-Agent you will submit the form with
440
+ // proxy: { type: 'HTTP', uri: 'login:password@203.0.113.10:8080' },
441
+ },
442
+ );
443
+
444
+ result.token; // put it in h-captcha-response (and g-recaptcha-response, if present)
445
+ result.respKey; // the second value hCaptcha hands the widget; usually empty or unused
446
+ result.userAgent; // the User-Agent the token was earned with
447
+ ```
448
+
449
+ The sitekey is on the widget container as `data-sitekey`, in the `sitekey`
450
+ option of `hcaptcha.render()`, or in the `sitekey=` parameter of the hCaptcha
451
+ iframe's address. An Enterprise `rqdata` value is issued for one challenge and
452
+ expires quickly, so read a fresh one for every task.
453
+
454
+ Put the token in the form's `h-captcha-response` field (and `g-recaptcha-response`,
455
+ if the form has one), or pass it to the widget's `data-callback`, and submit with
456
+ the User-Agent from the result. Tokens are single use and expire about two
457
+ minutes after they are issued, so submit each one soon.
458
+
459
+ ```js
460
+ await page.evaluate((token) => {
461
+ document.querySelector('[name="h-captcha-response"]').value = token;
462
+ const g = document.querySelector('[name="g-recaptcha-response"]');
463
+ if (g) g.value = token;
464
+ }, result.token);
465
+ ```
466
+
467
+ A `userAgent` you send is used only when it matches the browser CapSkip solves
468
+ with; the result always names the one the token was actually earned with. At
469
+ volume, hCaptcha scores the network a token comes from as well as the browser,
470
+ so send a proxy with each task or configure a pool in the CapSkip app (Settings,
471
+ Captcha types, hCaptcha, Configure Proxies). If a strict site still refuses the
472
+ tokens, turn on High score mode for hCaptcha in the same settings page.
473
+
423
474
  ### With a proxy (every method except image captcha)
424
475
 
425
476
  ```js
@@ -523,7 +574,8 @@ expanded into `captcha_voucher` and `existing_token`. ALTCHA adds `token` (the s
523
574
  string as `code`) and `number`, the counter that solved it. DataDome adds
524
575
  `cookie`, the `datadome=...` pair alone, while `code` keeps the cookie with its
525
576
  attributes; it also carries `userAgent`, and `proxy` when CapSkip chose the
526
- proxy.
577
+ proxy. hCaptcha adds `token`, the same string as `code`, `respKey` (always
578
+ present, empty when the widget handed out none) and `userAgent`.
527
579
 
528
580
  ---
529
581
 
@@ -568,7 +620,7 @@ const result: SolveResult = await solver.recaptcha('...', 'https://example.com')
568
620
 
569
621
  ### How do I solve a captcha in Node.js?
570
622
 
571
- Install the CapSkip desktop app, `npm install capskip`, then call the method that matches the widget — `recaptcha()`, `turnstile()`, `geetest()`, `altcha()`, `capy()`, `captchafox()`, `friendlyCaptcha()`, `amazonWaf()`, `dataDome()` or `normal()`. Each returns a Promise that resolves once CapSkip has an answer, giving you a token (a cookie for DataDome), or the recognized text in the case of an image captcha.
623
+ Install the CapSkip desktop app, `npm install capskip`, then call the method that matches the widget — `recaptcha()`, `turnstile()`, `geetest()`, `altcha()`, `capy()`, `captchafox()`, `friendlyCaptcha()`, `amazonWaf()`, `dataDome()`, `hcaptcha()` or `normal()`. Each returns a Promise that resolves once CapSkip has an answer, giving you a token (a cookie for DataDome), or the recognized text in the case of an image captcha.
572
624
 
573
625
  ### Is this a free captcha solver?
574
626
 
@@ -576,7 +628,7 @@ The SDK itself is MIT-licensed and free. Solving needs the CapSkip app, which is
576
628
 
577
629
  ### Which captchas can it solve?
578
630
 
579
- reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3, reCAPTCHA Enterprise, Cloudflare Turnstile, GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF, DataDome, and image/text captchas. Not hCaptcha, and not FunCaptcha/Arkose.
631
+ reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3, reCAPTCHA Enterprise, Cloudflare Turnstile, GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF, DataDome, hCaptcha (including invisible and Enterprise), and image/text captchas. Not FunCaptcha/Arkose.
580
632
 
581
633
  ### Does it work with Puppeteer and Playwright?
582
634
 
@@ -29,6 +29,7 @@ method returns a `Promise`.
29
29
  | Friendly Captcha (proof-of-work) | `friendlyCaptcha()` | `friendly_captcha` |
30
30
  | AWS WAF (challenge page or widget) | `amazonWaf()` | `amazon_waf` |
31
31
  | DataDome (slider and device check) | `dataDome()` | `datadome` |
32
+ | hCaptcha (checkbox, invisible, Enterprise) | `hcaptcha()` | `hcaptcha` |
32
33
 
33
34
  **Proxy** is supported for every method except image captcha. For ALTCHA the
34
35
  proxy is used only for the `challengeUrl` fetch; for Capy, only for the
@@ -846,6 +847,85 @@ Uses `recaptchaTimeout`: the challenge is passed in a real browser.
846
847
 
847
848
  ---
848
849
 
850
+ ## 12. hCaptcha — `hcaptcha(sitekey, url, { ... })`
851
+
852
+ hCaptcha is the checkbox widget behind many sign-up, login and checkout forms.
853
+ CapSkip passes the widget for the sitekey and page you send and returns the
854
+ token, ready to submit with the form. Invisible widgets and hCaptcha Enterprise
855
+ use the same method. Public docs: <https://capskip.com/api-docs/#hcaptcha>.
856
+
857
+ The arguments follow 2Captcha's `hcaptcha({ sitekey, pageurl, ... })` in this
858
+ SDK's usual shape, so code written against it ports directly. `pageurl` in the
859
+ options object is accepted in place of `url`.
860
+
861
+ ### POST `/in.php`
862
+
863
+ | Parameter | Type | Required | Description |
864
+ |---|---|---|---|
865
+ | `key` | string | Yes | CapSkip API key |
866
+ | `method` | string | Yes | `hcaptcha` |
867
+ | `sitekey` | string | Yes | The widget's sitekey (`data-sitekey`) |
868
+ | `pageurl` | string | Yes | Full http or https URL of the page the widget is on |
869
+ | `invisible` | int | No | `1` for an invisible widget (`data-size="invisible"`). The SDK takes `1`/`0`, `true`/`false` or `'1'`/`'0'`/`'true'`/`'false'`, sends `1`, and leaves it out when off |
870
+ | `data` | string | No | hCaptcha Enterprise: the `rqdata` value the page passes to `hcaptcha.setData()`. Single use; fetch a fresh one per task |
871
+ | `userAgent` | string | No | The User-Agent you will submit the form with. Used only when it matches CapSkip's browser. Sent as `useragent` |
872
+ | `json` | int | No | `0` plain text, `1` JSON |
873
+ | `proxy` | string | No | Proxy address |
874
+ | `proxytype` | string | No | `HTTP`, `HTTPS`, `SOCKS5` or `SOCKS5H` |
875
+
876
+ An optional left `undefined`, `null` or `''` is not sent.
877
+
878
+ Refused locally with `ValidationException`, as CapSkip would refuse them: a
879
+ missing or blank sitekey (`ERROR_RECAPTCHA_INVALID_SITEKEY`), a `pageurl` that
880
+ is missing or not an absolute http(s) address (`ERROR_PAGEURL`), an `invisible`
881
+ value other than 0/1/true/false, and any parameter not in the table, 2Captcha's
882
+ `domain` among them.
883
+
884
+ ### GET `/res.php`
885
+
886
+ The SDK polls with `json=1`, where CapSkip returns the token in `request` with
887
+ `respKey` and the User-Agent the token was earned with:
888
+
889
+ ```json
890
+ {
891
+ "status": 1,
892
+ "request": "P1_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.hadwYXNza2V5xQZ2...f3Kq8nV2",
893
+ "respKey": "E0_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjA0...9wB1",
894
+ "useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36"
895
+ }
896
+ ```
897
+
898
+ (In plain-text mode the User-Agent arrives in the `X-HCaptcha-User-Agent`
899
+ response header.) A task that hCaptcha refused the sitekey for fails with
900
+ `ERROR_RECAPTCHA_INVALID_SITEKEY`, one whose proxy could not be reached with
901
+ `ERROR_PROXY_CONNECT_REFUSED`; resubmitting the same task would fail again.
902
+
903
+ ### SDK usage
904
+
905
+ ```js
906
+ const result = await solver.hcaptcha(
907
+ 'a5f74b19-9e45-40e0-b45d-47ff91b7a6c2',
908
+ 'https://accounts.hcaptcha.com/demo',
909
+ );
910
+
911
+ // Invisible Enterprise widget, through a proxy
912
+ const result = await solver.hcaptcha(sitekey, pageUrl, {
913
+ invisible: 1,
914
+ data: rqdata,
915
+ proxy: { type: 'HTTP', uri: 'login:password@203.0.113.10:8080' },
916
+ });
917
+
918
+ form['h-captcha-response'] = result.token;
919
+ form['g-recaptcha-response'] = result.token; // if the form has one
920
+ headers['User-Agent'] = result.userAgent;
921
+ ```
922
+
923
+ Tokens are single use and expire about two minutes after they are issued.
924
+
925
+ Uses `recaptchaTimeout`: the widget is passed in a real browser.
926
+
927
+ ---
928
+
849
929
  ## Return value
850
930
 
851
931
  Every solve method resolves to:
@@ -925,6 +1005,20 @@ only when the task sent no proxy and CapSkip used its own DataDome proxy list:
925
1005
  }
926
1006
  ```
927
1007
 
1008
+ hCaptcha exposes `token`, the same string as `code`, plus `respKey` and the
1009
+ User-Agent the token was earned with. `respKey` is always present, and empty
1010
+ when the widget handed out none:
1011
+
1012
+ ```js
1013
+ {
1014
+ captchaId: '12345',
1015
+ code: 'P1_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…',
1016
+ token: 'P1_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…',
1017
+ respKey: 'E0_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…',
1018
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) …',
1019
+ }
1020
+ ```
1021
+
928
1022
  GeeTest additionally expands its answer into `challenge`, `validate`, and
929
1023
  `seccode` (`code` keeps the raw JSON string):
930
1024
 
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Solve an hCaptcha widget.
5
+ *
6
+ * hCaptcha is the checkbox widget behind many sign-up, login and checkout
7
+ * forms. CapSkip passes the widget in a real browser for the sitekey and page
8
+ * you send and returns the token the widget would have put in the form.
9
+ * Invisible widgets and hCaptcha Enterprise use the same method.
10
+ *
11
+ * You need two values, both read off the target page:
12
+ *
13
+ * * sitekey - the public identifier of the widget, the same for every
14
+ * visitor. Most pages carry it on the container as `data-sitekey`:
15
+ * `<div class="h-captcha" data-sitekey="...">`. When a page builds the
16
+ * widget with a script, look for the `sitekey` option of
17
+ * `hcaptcha.render()`, or the `sitekey=` parameter in the address of the
18
+ * hCaptcha iframe.
19
+ * * url - the full URL of the page the widget is on.
20
+ *
21
+ * A widget with `data-size="invisible"` is invisible: pass `invisible: 1`.
22
+ * Some hCaptcha Enterprise sites pass an `rqdata` value to the widget through
23
+ * `hcaptcha.setData()` before it runs; send it as `data`. It is issued for one
24
+ * challenge and expires quickly, so read a fresh one for every task.
25
+ */
26
+
27
+ const assert = require('node:assert');
28
+ const { CapSkip } = require('capskip');
29
+
30
+ const solver = new CapSkip({
31
+ apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
32
+ host: process.env.CAPSKIP_HOST || '127.0.0.1',
33
+ port: Number(process.env.CAPSKIP_PORT || 8080),
34
+ });
35
+
36
+ // hCaptcha's own public demo page and its sitekey.
37
+ const SITEKEY = 'a5f74b19-9e45-40e0-b45d-47ff91b7a6c2';
38
+ const PAGE_URL = 'https://accounts.hcaptcha.com/demo';
39
+
40
+ async function main() {
41
+ const result = await solver.hcaptcha(SITEKEY, PAGE_URL);
42
+
43
+ console.log('Captcha ID:', result.captchaId);
44
+ console.log('Token: ', `${result.token.slice(0, 60)}...`);
45
+ console.log('respKey: ', result.respKey ? `${result.respKey.slice(0, 40)}...` : '(empty)');
46
+ console.log('User-Agent:', result.userAgent || '(none reported)');
47
+
48
+ // --- Invisible and Enterprise widgets ------------------------------------
49
+ //
50
+ // const result = await solver.hcaptcha(SITEKEY, PAGE_URL, {
51
+ // invisible: 1, // data-size="invisible"
52
+ // data: rqdata, // the rqdata the page passed to hcaptcha.setData()
53
+ // });
54
+ //
55
+ // --- With a per-request proxy --------------------------------------------
56
+ // CapSkip does not force a proxy. At volume, though, hCaptcha scores the
57
+ // network a token comes from as well as the browser, so send a proxy with
58
+ // each task (or configure a pool in the app: Settings, Captcha types,
59
+ // hCaptcha, Configure Proxies). A proxy sent with a task always takes
60
+ // priority over the pool.
61
+ //
62
+ // const result = await solver.hcaptcha(SITEKEY, PAGE_URL, {
63
+ // proxy: { type: 'HTTP', uri: 'login:password@1.2.3.4:8080' },
64
+ // });
65
+
66
+ // --- Submitting the token ------------------------------------------------
67
+ // The widget writes its token into a hidden textarea named
68
+ // h-captcha-response (and a copy named g-recaptcha-response). Put the token
69
+ // in both and submit the form, or call the widget's data-callback with it.
70
+ // Without a browser, post it as those form fields with the User-Agent from
71
+ // the result:
72
+ //
73
+ // await fetch(FORM_URL, {
74
+ // method: 'POST',
75
+ // headers: { 'User-Agent': result.userAgent },
76
+ // body: new URLSearchParams({
77
+ // email: 'someone@example.com',
78
+ // 'h-captcha-response': result.token,
79
+ // 'g-recaptcha-response': result.token,
80
+ // }),
81
+ // });
82
+ //
83
+ // Tokens are single use and expire about two minutes after they are issued,
84
+ // so submit each one soon and solve again for the next form submission.
85
+ console.log('Form fields:', {
86
+ 'h-captcha-response': `${result.token.slice(0, 40)}...`,
87
+ 'g-recaptcha-response': `${result.token.slice(0, 40)}...`,
88
+ });
89
+
90
+ // `code` holds the same string as `token`, which is what you forward if you
91
+ // are porting code written against another solver's API.
92
+ assert.strictEqual(result.code, result.token);
93
+ }
94
+
95
+ main().catch((error) => {
96
+ console.error(`${error.name}: ${error.message}`);
97
+ process.exitCode = 1;
98
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "capskip",
3
- "version": "1.5.0",
4
- "description": "Captcha solver for Node.js: solve reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest, AWS WAF, DataDome and image captchas with CapSkip, a local unlimited captcha solver with no per-solve fees.",
3
+ "version": "1.6.0",
4
+ "description": "Captcha solver for Node.js: solve reCAPTCHA v2/v3, Cloudflare Turnstile, hCaptcha, GeeTest, AWS WAF, DataDome 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": {
@@ -33,6 +33,9 @@
33
33
  "recaptcha-v2",
34
34
  "recaptcha-v3",
35
35
  "recaptcha-enterprise",
36
+ "hcaptcha",
37
+ "hcaptcha-solver",
38
+ "hcaptcha-enterprise",
36
39
  "cloudflare",
37
40
  "turnstile",
38
41
  "turnstile-solver",
package/src/apiParams.js CHANGED
@@ -59,6 +59,18 @@ const AMAZON_WAF_SCRIPTS = ['challenge_script', 'captcha_script', 'jsapiScript']
59
59
  // the three AWS WAF script files.
60
60
  const AMAZON_WAF_SCRIPT_PATH = /^(\/[A-Za-z0-9_-]+)+\/(challenge|captcha|jsapi)\.js$/;
61
61
 
62
+ const HCAPTCHA_SUBMIT = new Set([
63
+ 'method', 'sitekey', 'pageurl', 'invisible', 'data', 'useragent', 'json',
64
+ 'proxy', 'proxytype',
65
+ ]);
66
+
67
+ // What the server reads as "invisible widget" ("1" or "true") and as its
68
+ // default. Strings are matched case-insensitively. A boolean would otherwise
69
+ // reach the form body as 'true' or 'false', and the server reads 'false' as
70
+ // neither, so every accepted form is normalized to the 1 the server reads.
71
+ const HCAPTCHA_INVISIBLE_ON = [true, 1, '1', 'true'];
72
+ const HCAPTCHA_INVISIBLE_OFF = [false, 0, '0', 'false'];
73
+
62
74
  const DATADOME_SUBMIT = new Set([
63
75
  'method', 'captcha_url', 'pageurl', 'useragent', 'json', 'proxy', 'proxytype',
64
76
  ]);
@@ -648,6 +660,75 @@ function validateDataDomeSubmit(params) {
648
660
  }
649
661
  }
650
662
 
663
+ /**
664
+ * Drop unset and empty optionals, and send `invisible` as the 1 the server
665
+ * reads. `invisible: 0` is the server's default, so it is not sent at all.
666
+ */
667
+ function normalizeHCaptchaSubmit(params) {
668
+ const out = {};
669
+ for (const [key, value] of Object.entries(dropUnset(params))) {
670
+ if (value !== '') {
671
+ out[key] = value;
672
+ }
673
+ }
674
+ if (has(out, 'invisible')) {
675
+ const raw = out.invisible;
676
+ const key = typeof raw === 'string' ? raw.toLowerCase() : raw;
677
+ if (HCAPTCHA_INVISIBLE_ON.includes(key)) {
678
+ out.invisible = 1;
679
+ } else if (HCAPTCHA_INVISIBLE_OFF.includes(key)) {
680
+ delete out.invisible;
681
+ } else {
682
+ const shown = typeof raw === 'string' ? `'${raw}'` : String(raw);
683
+ throw new ValidationException(
684
+ `invisible must be 0 or 1 (or a boolean) for hCaptcha, not ${shown}.`,
685
+ );
686
+ }
687
+ }
688
+ return out;
689
+ }
690
+
691
+ /** An absolute http(s) URL with a host. */
692
+ function isHttpUrl(url) {
693
+ let parsed;
694
+ try {
695
+ parsed = new URL(url);
696
+ } catch (error) {
697
+ return false;
698
+ }
699
+ return (parsed.protocol === 'http:' || parsed.protocol === 'https:')
700
+ && parsed.hostname !== '';
701
+ }
702
+
703
+ function validateHCaptchaSubmit(params) {
704
+ // The server answers ERROR_RECAPTCHA_INVALID_SITEKEY for a missing sitekey
705
+ // and ERROR_PAGEURL for a page URL that is missing or not http(s). Fail
706
+ // locally so neither costs a round-trip.
707
+ const sitekey = params.sitekey;
708
+ if (sitekey === undefined || sitekey === null || !String(sitekey).trim()) {
709
+ throw new ValidationException("'sitekey' is required for hCaptcha.");
710
+ }
711
+ const rawPageurl = params.pageurl;
712
+ const pageurl = rawPageurl === undefined || rawPageurl === null
713
+ ? ''
714
+ : String(rawPageurl).trim();
715
+ if (!pageurl) {
716
+ throw new ValidationException("'pageurl' is required for hCaptcha.");
717
+ }
718
+ if (!isHttpUrl(pageurl)) {
719
+ throw new ValidationException(
720
+ `pageurl must be an absolute http or https URL for hCaptcha, not '${pageurl}'.`,
721
+ );
722
+ }
723
+
724
+ const unknown = unknownKeys(params, HCAPTCHA_SUBMIT);
725
+ if (unknown.length > 0) {
726
+ throw new ValidationException(
727
+ `Unsupported parameters for hCaptcha: ${reprList(unknown)}.`,
728
+ );
729
+ }
730
+ }
731
+
651
732
  function validateProxyType(params) {
652
733
  const proxytype = params.proxytype;
653
734
  if (proxytype === undefined || proxytype === null || proxytype === '') {
@@ -691,6 +772,9 @@ function prepareSubmitParams(params, captchaType, version = 'v2') {
691
772
  } else if (captchaType === 'datadome') {
692
773
  prepared = normalizeDataDomeSubmit(prepared);
693
774
  validateDataDomeSubmit(prepared);
775
+ } else if (captchaType === 'hcaptcha') {
776
+ prepared = normalizeHCaptchaSubmit(prepared);
777
+ validateHCaptchaSubmit(prepared);
694
778
  }
695
779
 
696
780
  // Skipped for 'normal', which rejects proxy outright with a clearer message.
@@ -716,6 +800,7 @@ module.exports = {
716
800
  DATADOME_SUBMIT,
717
801
  DATADOME_CHALLENGE_DOMAIN,
718
802
  DATADOME_CHALLENGE_PATHS,
803
+ HCAPTCHA_SUBMIT,
719
804
  CAPY_VERSIONS,
720
805
  FRIENDLY_CAPTCHA_VERSIONS,
721
806
  PROXY_TYPES,
@@ -740,6 +825,8 @@ module.exports = {
740
825
  isDataDomeUserAgent,
741
826
  normalizeDataDomeSubmit,
742
827
  validateDataDomeSubmit,
828
+ normalizeHCaptchaSubmit,
829
+ validateHCaptchaSubmit,
743
830
  validateProxyType,
744
831
  prepareSubmitParams,
745
832
  };
package/src/index.js CHANGED
@@ -27,5 +27,5 @@ module.exports = {
27
27
  NetworkException,
28
28
  ApiException,
29
29
  TimeoutException,
30
- version: '1.5.0',
30
+ version: '1.6.0',
31
31
  };
package/src/solver.js CHANGED
@@ -89,6 +89,11 @@ function applyPollResult(result, polled) {
89
89
  if (polled.proxy) {
90
90
  result.proxy = polled.proxy;
91
91
  }
92
+ // hCaptcha hands the widget a second value next to the token. It is
93
+ // carried even when empty, which is how the server reports "none".
94
+ if (Object.prototype.hasOwnProperty.call(polled, 'respKey')) {
95
+ result.respKey = polled.respKey;
96
+ }
92
97
  // ALTCHA's createTask-shaped `solution` object. Carried through so
93
98
  // applyAltchaSolution can read the counter the server already worked out,
94
99
  // which is the only reliable source for a proof-of-work v2 answer; that
@@ -325,6 +330,20 @@ function applyTokenSolution(result) {
325
330
  return result;
326
331
  }
327
332
 
333
+ /**
334
+ * Expose the hCaptcha token as `token`, next to its `respKey`.
335
+ *
336
+ * `code` keeps the token as the server sent it and `token` is the same string,
337
+ * for the `h-captcha-response` field. `respKey` is always present, empty when
338
+ * the widget handed out none, so callers can read it without a key check.
339
+ */
340
+ function applyHCaptchaSolution(result) {
341
+ applyTokenSolution(result);
342
+ const { respKey } = result;
343
+ result.respKey = typeof respKey === 'string' ? respKey : '';
344
+ return result;
345
+ }
346
+
328
347
  // CapSkip's in.php returns OK|<id> by default, or {"status":1,"request":"<id>"}
329
348
  // when the submit carried json=1. Accept both so submitting with json=1 works.
330
349
  function parseSubmitResponse(response) {
@@ -351,7 +370,7 @@ function parseSubmitResponse(response) {
351
370
  /**
352
371
  * Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile,
353
372
  * GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF,
354
- * DataDome).
373
+ * DataDome, hCaptcha).
355
374
  */
356
375
  class CapSkip {
357
376
  constructor({
@@ -700,6 +719,61 @@ class CapSkip {
700
719
  return applyDataDomeSolution(result);
701
720
  }
702
721
 
722
+ /**
723
+ * Solve an hCaptcha widget and return its token.
724
+ *
725
+ * `sitekey` and `url` are required; everything else goes in `options`:
726
+ *
727
+ * - `sitekey` is the widget's `data-sitekey` (or the `sitekey` option of
728
+ * `hcaptcha.render()`).
729
+ * - `url` is the full address of the page the widget is on. `pageurl` in
730
+ * `options` is accepted in its place.
731
+ * - `options.invisible` is `1` (or `true`) for a `data-size="invisible"`
732
+ * widget.
733
+ * - `options.data` is the `rqdata` value an hCaptcha Enterprise site passes
734
+ * to `hcaptcha.setData()`. It is single use and expires quickly, so fetch
735
+ * a fresh one for every task.
736
+ * - `options.userAgent` is the User-Agent you will submit the form with.
737
+ * CapSkip uses it only when it matches the browser it solves with.
738
+ * - `options.proxy` is a URI string or
739
+ * `{ type: 'HTTP', uri: 'login:password@IP:PORT' }`.
740
+ *
741
+ * hCaptcha Enterprise and invisible widgets use this same method.
742
+ *
743
+ * The result carries the token as both `code` and `token` (for the
744
+ * `h-captcha-response` and `g-recaptcha-response` fields), `respKey` (the
745
+ * second value hCaptcha hands the widget; most sites never read it, and it
746
+ * can be empty), and `userAgent`, the User-Agent the token was earned with.
747
+ * Submit the token with that User-Agent, soon: hCaptcha tokens are single
748
+ * use and expire about two minutes after they are issued.
749
+ */
750
+ async hcaptcha(sitekey, url, options = {}) {
751
+ // An unset value is dropped rather than sent as undefined, so
752
+ // `hcaptcha(sitekey, undefined, { pageurl })` reaches the alias without
753
+ // tripping the conflict check; an empty one is dropped during validation.
754
+ const given = {};
755
+ const add = (key, value) => {
756
+ if (value !== undefined && value !== null) {
757
+ given[key] = value;
758
+ }
759
+ };
760
+ add('sitekey', sitekey);
761
+ add('url', url);
762
+ for (const [key, value] of Object.entries(options)) {
763
+ add(key, value);
764
+ }
765
+
766
+ // A real browser session, like reCAPTCHA, so it gets the longer of the
767
+ // two timeouts unless the caller asked for a specific one.
768
+ const result = await this.solve({
769
+ timeout: this.recaptchaTimeout,
770
+ ...given,
771
+ method: 'hcaptcha',
772
+ poll_json: 1,
773
+ });
774
+ return applyHCaptchaSolution(result);
775
+ }
776
+
703
777
  async solve(options = {}) {
704
778
  const {
705
779
  timeout = 0,
@@ -806,6 +880,9 @@ class CapSkip {
806
880
  if (method === 'datadome') {
807
881
  return prepareSubmitParams(params, 'datadome');
808
882
  }
883
+ if (method === 'hcaptcha') {
884
+ return prepareSubmitParams(params, 'hcaptcha');
885
+ }
809
886
  return applyProxy(applyParamAliases(params));
810
887
  }
811
888
  }
@@ -822,6 +899,7 @@ module.exports = {
822
899
  applyCapySolution,
823
900
  applyAmazonWafSolution,
824
901
  applyDataDomeSolution,
902
+ applyHCaptchaSolution,
825
903
  expandObjectAnswer,
826
904
  applyTokenSolution,
827
905
  };
package/types/index.d.ts CHANGED
@@ -20,7 +20,7 @@ export interface CapSkipOptions {
20
20
  defaultTimeout?: number;
21
21
  /**
22
22
  * Seconds to poll reCAPTCHA / Turnstile / GeeTest / CaptchaFox / Friendly
23
- * Captcha / AWS WAF / DataDome before timing out. Default `300`. ALTCHA and Capy use
23
+ * Captcha / AWS WAF / DataDome / hCaptcha before timing out. Default `300`. ALTCHA and Capy use
24
24
  * `defaultTimeout` instead — neither is a browser solve.
25
25
  */
26
26
  recaptchaTimeout?: number;
@@ -42,9 +42,9 @@ export interface SolveResult {
42
42
  */
43
43
  code: string | Record<string, unknown>;
44
44
  /**
45
- * Turnstile, CaptchaFox and DataDome — the User-Agent to use when submitting
46
- * the token. For CaptchaFox this is the browser's own, not one you sent, and
47
- * it is absent when the solve captured none rather than guessed. For DataDome
45
+ * Turnstile, CaptchaFox, hCaptcha and DataDome — the User-Agent to use when
46
+ * submitting the token. For CaptchaFox and hCaptcha this is the browser's own,
47
+ * not one you sent, and it is absent when the solve reported none. For DataDome
48
48
  * it is the User-Agent the cookie was earned with — the one you sent, or
49
49
  * CapSkip's own browser's — and every request carrying the cookie must send it.
50
50
  */
@@ -72,7 +72,12 @@ export interface SolveResult {
72
72
  */
73
73
  answer?: string;
74
74
  /**
75
- * Capy only — empty for a puzzle solve. Present for shape compatibility with
75
+ * hCaptcha — the second value hCaptcha hands the widget next to the token.
76
+ * Always present on an `hcaptcha()` result, `''` when the widget handed out
77
+ * none; most sites never read it. Other polled results carry it only when
78
+ * the server sent one.
79
+ *
80
+ * Capy — empty for a puzzle solve. Present for shape compatibility with
76
81
  * 2Captcha's documented response; it carries nothing.
77
82
  */
78
83
  respKey?: string;
@@ -324,6 +329,43 @@ export interface DataDomeOptions {
324
329
  [key: string]: unknown;
325
330
  }
326
331
 
332
+ /** Extra options for {@link CapSkip.hcaptcha}. */
333
+ export interface HCaptchaOptions {
334
+ /** The page URL (alias for the `url` argument). */
335
+ pageurl?: string;
336
+ /** The page URL (alias for the `url` argument). */
337
+ url?: string;
338
+ /**
339
+ * `1` / `true` for a `data-size="invisible"` widget; `0` / `false` (the
340
+ * default) is not sent. The strings `"1"`, `"0"`, `"true"` and `"false"`
341
+ * are accepted too; anything else is refused.
342
+ */
343
+ invisible?: boolean | 0 | 1 | '0' | '1' | 'true' | 'false';
344
+ /**
345
+ * hCaptcha Enterprise `rqdata`, the value the site passes to
346
+ * `hcaptcha.setData()`. Single use and short-lived: fetch a fresh one per task.
347
+ */
348
+ data?: string;
349
+ /**
350
+ * The User-Agent you will submit the form with. CapSkip uses it only when it
351
+ * matches the browser it solves with.
352
+ */
353
+ userAgent?: string;
354
+ /** Alias for `userAgent`. */
355
+ useragent?: string;
356
+ /** Proxy used for solving. */
357
+ proxy?: Proxy | string;
358
+ /** Proxy type when `proxy` is a bare string. Default `HTTP`. */
359
+ proxytype?: string;
360
+ /** Poll timeout in seconds. Default: the client's `recaptchaTimeout`. */
361
+ timeout?: number;
362
+ /** Max seconds between polls for this solve. */
363
+ polling_interval?: number;
364
+ /** `1` to request the raw JSON response from CapSkip. */
365
+ json?: number;
366
+ [key: string]: unknown;
367
+ }
368
+
327
369
  /** Options for the {@link CapSkip.solve} manual workflow. */
328
370
  export interface SolveOptions {
329
371
  /** Poll timeout in seconds (falls back to the configured default). */
@@ -436,6 +478,22 @@ export class CapSkip {
436
478
  url: string,
437
479
  options?: DataDomeOptions,
438
480
  ): Promise<SolveResult>;
481
+ /**
482
+ * Solve an hCaptcha widget (checkbox, invisible or Enterprise) and return
483
+ * its token.
484
+ *
485
+ * `sitekey` is the widget's `data-sitekey`, `url` the page it is on
486
+ * (`options.pageurl` is accepted in its place). The result carries the token
487
+ * as `code` and `token` (for `h-captcha-response` and
488
+ * `g-recaptcha-response`), `respKey` (always present, `''` when none) and
489
+ * `userAgent`, the User-Agent the token was earned with. Tokens are single
490
+ * use and expire about two minutes after they are issued.
491
+ */
492
+ hcaptcha(
493
+ sitekey: string,
494
+ url: string | undefined,
495
+ options?: HCaptchaOptions,
496
+ ): Promise<SolveResult>;
439
497
  /** Submit then poll to completion. Used by the higher-level solve methods. */
440
498
  solve(options?: SolveOptions): Promise<SolveResult>;
441
499
  /** Submit a captcha without polling; resolves to the captcha id. */