capskip 1.0.2 → 1.1.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,22 @@ 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.1.0] - 2026-07-26
9
+
10
+ ### Added
11
+
12
+ - **GeeTest v3 (slide) support** via `geetest(gt, challenge, url, options)`.
13
+ Accepts the optional `api_server` domain override and the usual `proxy`
14
+ option. The result exposes the answer as the parsed `challenge`, `validate`,
15
+ and `seccode` fields, while `code` keeps the raw JSON string CapSkip returns.
16
+ - Parameter aliases `apiServer` and `api_subdomain` for `api_server`.
17
+ - TypeScript definitions for `geetest()`, `GeetestOptions`, and the new
18
+ `SolveResult` fields.
19
+ - `proxytype` is now validated against the values CapSkip accepts (`HTTP`,
20
+ `HTTPS`, `SOCKS5`, `SOCKS5H`, case-insensitive) for every proxy-capable captcha
21
+ type. `SOCKS4` and other values previously reached the server and came back as
22
+ `ERROR_BAD_PARAMETERS`; they now raise `ValidationException` locally.
23
+
8
24
  ## [1.0.2] - 2026-07-15
9
25
 
10
26
  ### Fixed
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Official Node.js client for the [CapSkip](https://capskip.com) **local** captcha solver.
8
8
 
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 cloud service and no per-solve API fees beyond your CapSkip license.
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
10
 
11
11
  ---
12
12
 
@@ -70,6 +70,7 @@ Every solve method returns a Promise — use `await` or `.then()`.
70
70
  | reCAPTCHA v3 Enterprise | `solver.recaptcha(sitekey, url, { version: 'v3', enterprise: 1 })` |
71
71
  | Cloudflare Turnstile (widget) | `solver.turnstile(sitekey, url)` |
72
72
  | Cloudflare Turnstile (challenge page) | `solver.turnstile(sitekey, url, { data, pagedata })` |
73
+ | GeeTest v3 (slide) | `solver.geetest(gt, challenge, url)` |
73
74
 
74
75
  ---
75
76
 
@@ -97,7 +98,7 @@ const solver = new CapSkip({
97
98
  host: '127.0.0.1', // CapSkip host
98
99
  port: 8080, // CapSkip port from app settings
99
100
  defaultTimeout: 120, // seconds — image captcha polling timeout
100
- recaptchaTimeout: 300, // seconds — reCAPTCHA / Turnstile polling timeout
101
+ recaptchaTimeout: 300, // seconds — reCAPTCHA / Turnstile / GeeTest polling timeout
101
102
  pollingInterval: 5, // max seconds between res.php polls (starts at 0.25s, backs off to this)
102
103
  });
103
104
  ```
@@ -161,7 +162,23 @@ const v3 = await solver.recaptcha('...', 'https://example.com', {
161
162
  const result = await solver.turnstile('0x4AAAAAAA...', 'https://example.com');
162
163
  ```
163
164
 
164
- ### With a proxy (reCAPTCHA & Turnstile only)
165
+ ### GeeTest v3
166
+
167
+ `gt` is static per site, but `challenge` is single-use and expires in about a
168
+ minute — fetch a fresh pair right before solving.
169
+
170
+ ```js
171
+ const result = await solver.geetest(
172
+ '81388ea1fc187e0c335c0a8907ff2625',
173
+ '7cf6a8b1a2c34d5e6f7089abcdef0123',
174
+ 'https://example.com/login',
175
+ );
176
+
177
+ // Post these back exactly as the site's own front-end would
178
+ result.challenge, result.validate, result.seccode;
179
+ ```
180
+
181
+ ### With a proxy (reCAPTCHA, Turnstile & GeeTest only)
165
182
 
166
183
  ```js
167
184
  // Proxy is not supported for image captcha
@@ -208,6 +225,9 @@ Every solve method resolves to:
208
225
  }
209
226
  ```
210
227
 
228
+ GeeTest additionally expands its answer into `challenge`, `validate`, and
229
+ `seccode`, while `code` keeps the raw JSON string.
230
+
211
231
  ---
212
232
 
213
233
  ## Error handling
@@ -22,8 +22,9 @@ method returns a `Promise`.
22
22
  | reCAPTCHA v2 | `recaptcha(sitekey, url)` | `userrecaptcha` |
23
23
  | reCAPTCHA v3 | `recaptcha(sitekey, url, { version: 'v3' })` | `userrecaptcha` + `version=v3` |
24
24
  | Cloudflare Turnstile | `turnstile()` | `turnstile` |
25
+ | GeeTest v3 (slide) | `geetest()` | `geetest` |
25
26
 
26
- **Proxy** is supported for reCAPTCHA and Turnstile only — not for image captcha.
27
+ **Proxy** is supported for reCAPTCHA, Turnstile, and GeeTest — not for image captcha.
27
28
 
28
29
  ---
29
30
 
@@ -206,6 +207,66 @@ const challenge = await solver.turnstile('0x4AAAAAAA...', 'https://example.com',
206
207
 
207
208
  ---
208
209
 
210
+ ## 5. GeeTest v3 — `geetest(gt, challenge, url, { ... })`
211
+
212
+ ### POST `/in.php`
213
+
214
+ | Parameter | Type | Required | Description |
215
+ |---|---|---|---|
216
+ | `key` | string | Yes | CapSkip API key |
217
+ | `method` | string | Yes | `geetest` |
218
+ | `gt` | string | Yes | Static per-site GeeTest id |
219
+ | `challenge` | string | Yes | Single-use challenge token |
220
+ | `pageurl` | string | Yes | Full page URL |
221
+ | `api_server` | string | No | GeeTest API server domain, e.g. `api-na.geetest.com` |
222
+ | `json` | int | No | `0` plain text, `1` JSON |
223
+ | `proxy` | string | No | Proxy address |
224
+ | `proxytype` | string | No | Proxy type |
225
+
226
+ ### Getting `gt` and `challenge`
227
+
228
+ Both come from the target site, which fetches them from an endpoint returning
229
+ `{"gt": "...", "challenge": "..."}` (often `.../register.php` or a `gettype`/`get.php`
230
+ request). Find it in DevTools → Network, or read them out of the
231
+ `initGeetest({ gt, challenge })` call in the page scripts.
232
+
233
+ > **`challenge` is single-use and expires in about a minute.** Fetch a fresh pair
234
+ > immediately before each solve. If a solve comes back with a bad-challenge error,
235
+ > request a new pair and retry — reusing one never succeeds.
236
+
237
+ ### SDK usage
238
+
239
+ ```js
240
+ const result = await solver.geetest(
241
+ '81388ea1fc187e0c335c0a8907ff2625',
242
+ '7cf6a8b1a2c34d5e6f7089abcdef0123',
243
+ 'https://example.com/login',
244
+ );
245
+
246
+ result.challenge; // geetest_challenge
247
+ result.validate; // geetest_validate
248
+ result.seccode; // geetest_seccode
249
+ result.code; // the same answer as a raw JSON string
250
+ ```
251
+
252
+ Post the three fields back exactly as the site's own front-end would:
253
+
254
+ ```js
255
+ await fetch(LOGIN_URL, {
256
+ method: 'POST',
257
+ body: new URLSearchParams({
258
+ geetest_challenge: result.challenge,
259
+ geetest_validate: result.validate,
260
+ geetest_seccode: result.seccode,
261
+ }),
262
+ });
263
+ ```
264
+
265
+ GeeTest is a real browser solve, so it uses the longer `recaptchaTimeout` budget
266
+ rather than `defaultTimeout`.
267
+
268
+ ---
269
+
209
270
  ## Return value
210
271
 
211
272
  Every solve method resolves to:
@@ -218,6 +279,19 @@ Every solve method resolves to:
218
279
  }
219
280
  ```
220
281
 
282
+ GeeTest additionally expands its answer into `challenge`, `validate`, and
283
+ `seccode` (`code` keeps the raw JSON string):
284
+
285
+ ```js
286
+ {
287
+ captchaId: '12345',
288
+ code: '{"geetest_challenge":"...","geetest_validate":"...","geetest_seccode":"..."}',
289
+ challenge: '...',
290
+ validate: '...',
291
+ seccode: '...',
292
+ }
293
+ ```
294
+
221
295
  ---
222
296
 
223
297
  ## SDK parameter aliases
@@ -231,6 +305,8 @@ Convenience aliases mapped before sending to CapSkip:
231
305
  | `minScore` | `min_score` |
232
306
  | `datas` | `data-s` |
233
307
  | `data_s` | `data-s` |
308
+ | `apiServer` | `api_server` |
309
+ | `api_subdomain` | `api_server` |
234
310
  | `proxy` object | `proxy` + `proxytype` strings |
235
311
 
236
312
  ```js
@@ -136,6 +136,7 @@ node examples/recaptcha.js
136
136
  | `image_captcha.js` | Image captcha from file, URL, or base64 |
137
137
  | `recaptcha.js` | reCAPTCHA v2, v3, invisible, enterprise, proxy |
138
138
  | `turnstile.js` | Cloudflare Turnstile widget and challenge page |
139
+ | `geetest.js` | GeeTest v3 slider, including fetching a fresh `gt`/`challenge` pair |
139
140
  | `async_example.js` | Parallel solving |
140
141
  | `verify_connection.js` | Check CapSkip is running |
141
142
 
@@ -152,6 +152,48 @@ const result = await solver.recaptcha('...', '...', { proxy });
152
152
 
153
153
  ---
154
154
 
155
+ ## GeeTest keeps failing with a bad-challenge error
156
+
157
+ **Symptom:** `geetest()` rejects with `ApiException` (or times out) even though
158
+ `gt` and `challenge` were copied correctly from the page.
159
+
160
+ **Cause:** The `challenge` value is **single-use and expires in about a minute**.
161
+ A pair copied out of DevTools minutes earlier, cached in a config file, or reused
162
+ across two solves is already dead.
163
+
164
+ **Fix:** Fetch a fresh pair programmatically immediately before each solve, and on
165
+ failure request a *new* pair rather than retrying the old one:
166
+
167
+ ```js
168
+ const { gt, challenge } = await (await fetch(REGISTER_URL)).json();
169
+ const result = await solver.geetest(gt, challenge, PAGE_URL);
170
+ ```
171
+
172
+ If the site loads GeeTest from a non-default API server domain, pass it through as well:
173
+
174
+ ```js
175
+ await solver.geetest(gt, challenge, url, { api_server: 'api-na.geetest.com' });
176
+ ```
177
+
178
+ ---
179
+
180
+ ## GeeTest result — where are challenge/validate/seccode?
181
+
182
+ `result.code` holds the raw JSON string CapSkip returns. The SDK also parses it
183
+ for you, so prefer the individual fields:
184
+
185
+ ```js
186
+ result.challenge; // geetest_challenge
187
+ result.validate; // geetest_validate
188
+ result.seccode; // geetest_seccode
189
+ ```
190
+
191
+ Submit all three under their `geetest_`-prefixed names, exactly as the site's own
192
+ front-end does. Sending only `validate` is the most common reason a correct solve
193
+ is rejected.
194
+
195
+ ---
196
+
155
197
  ## NetworkException during manual polling
156
198
 
157
199
  **Symptom:** `getResult()` keeps rejecting with `NetworkException`.
package/docs/TUTORIAL.md CHANGED
@@ -12,14 +12,15 @@ Work through it top to bottom, or jump to the section you need.
12
12
  5. [reCAPTCHA v2](#5-recaptcha-v2)
13
13
  6. [reCAPTCHA v3](#6-recaptcha-v3)
14
14
  7. [Cloudflare Turnstile](#7-cloudflare-turnstile)
15
- 8. [Using a proxy](#8-using-a-proxy)
16
- 9. [Concurrency](#9-concurrency)
17
- 10. [The manual workflow](#10-the-manual-workflow)
18
- 11. [Return values](#11-return-values)
19
- 12. [Error handling](#12-error-handling)
20
- 13. [End-to-end: solve and submit](#13-end-to-end-solve-and-submit)
21
- 14. [Parameter reference](#14-parameter-reference)
22
- 15. [Best practices](#15-best-practices)
15
+ 8. [GeeTest v3](#8-geetest-v3)
16
+ 9. [Using a proxy](#9-using-a-proxy)
17
+ 10. [Concurrency](#10-concurrency)
18
+ 11. [The manual workflow](#11-the-manual-workflow)
19
+ 12. [Return values](#12-return-values)
20
+ 13. [Error handling](#13-error-handling)
21
+ 14. [End-to-end: solve and submit](#14-end-to-end-solve-and-submit)
22
+ 15. [Parameter reference](#15-parameter-reference)
23
+ 16. [Best practices](#16-best-practices)
23
24
 
24
25
  ---
25
26
 
@@ -77,7 +78,7 @@ const solver = new CapSkip({
77
78
  host: '127.0.0.1', // where CapSkip is listening
78
79
  port: 8080, // API port from CapSkip settings
79
80
  defaultTimeout: 120, // seconds to wait for an image captcha
80
- recaptchaTimeout: 300, // seconds to wait for reCAPTCHA / Turnstile
81
+ recaptchaTimeout: 300, // seconds to wait for reCAPTCHA / Turnstile / GeeTest
81
82
  pollingInterval: 5, // max seconds between result polls (starts at 0.25s, backs off to this)
82
83
  });
83
84
  ```
@@ -149,7 +150,7 @@ await solver.normal('captcha.png', { json: 1 });
149
150
  ```
150
151
 
151
152
  > **Note:** Proxies are **not** supported for image captcha — passing one throws
152
- > `ValidationException`. Proxies apply only to reCAPTCHA and Turnstile.
153
+ > `ValidationException`. Proxies apply only to reCAPTCHA, Turnstile, and GeeTest.
153
154
 
154
155
  ---
155
156
 
@@ -231,24 +232,94 @@ await solver.turnstile('0x4AAAAAAA...', 'https://example.com', {
231
232
 
232
233
  ---
233
234
 
234
- ## 8. Using a proxy
235
+ ## 8. GeeTest v3
236
+
237
+ `solver.geetest(gt, challenge, url)` solves the GeeTest v3 slide puzzle.
238
+
239
+ ### Finding `gt` and `challenge`
240
+
241
+ Unlike a sitekey, GeeTest needs **two** values, and one of them is short-lived:
242
+
243
+ | Value | Lifetime | Where it comes from |
244
+ |---|---|---|
245
+ | `gt` | Static per site | The same place as `challenge` |
246
+ | `challenge` | **Single-use, expires in ~1 minute** | An endpoint the site calls that returns `{"gt": "...", "challenge": "..."}` |
247
+
248
+ Open DevTools → Network on the target page and look for a request to something like
249
+ `.../register.php`, `gettype`, or `get.php`. You can also read the values out of the
250
+ `initGeetest({ gt, challenge })` call in the page scripts.
251
+
252
+ ```js
253
+ // Ask the target site for a fresh pair immediately before solving.
254
+ const resp = await fetch('https://example.com/captcha/register.php');
255
+ const { gt, challenge } = await resp.json();
256
+
257
+ const result = await solver.geetest(gt, challenge, 'https://example.com/login');
258
+ ```
259
+
260
+ ### Using the answer
261
+
262
+ The result carries the three values the site's own front-end would submit:
263
+
264
+ ```js
265
+ result.challenge; // geetest_challenge
266
+ result.validate; // geetest_validate
267
+ result.seccode; // geetest_seccode
268
+
269
+ result.code; // the same answer as a raw JSON string
270
+ ```
271
+
272
+ Post them back exactly as the site expects:
273
+
274
+ ```js
275
+ await fetch('https://example.com/login', {
276
+ method: 'POST',
277
+ body: new URLSearchParams({
278
+ username: '...',
279
+ password: '...',
280
+ geetest_challenge: result.challenge,
281
+ geetest_validate: result.validate,
282
+ geetest_seccode: result.seccode,
283
+ }),
284
+ });
285
+ ```
286
+
287
+ > **`challenge` is one-shot.** Never cache or reuse a pair. If a solve fails with a
288
+ > bad-challenge error, request a *new* pair and retry — retrying with the same
289
+ > `challenge` can never succeed.
290
+
291
+ If the site uses a non-default GeeTest API server domain, pass it through:
292
+
293
+ ```js
294
+ await solver.geetest(gt, challenge, url, { api_server: 'api-na.geetest.com' });
295
+ ```
296
+
297
+ Because GeeTest is a real browser solve (load, slide, verify), it uses the longer
298
+ `recaptchaTimeout` budget rather than `defaultTimeout`.
299
+
300
+ ---
301
+
302
+ ## 9. Using a proxy
235
303
 
236
304
  Solving through the same IP you will submit from greatly improves acceptance rates
237
- for reCAPTCHA and Turnstile. Pass the proxy as an object with `type` and `uri`:
305
+ for reCAPTCHA, Turnstile, and GeeTest. Pass the proxy as an object with `type` and `uri`:
238
306
 
239
307
  ```js
240
308
  const proxy = { type: 'HTTPS', uri: 'user:pass@1.2.3.4:3128' };
241
309
 
242
310
  await solver.recaptcha('...', 'https://example.com', { proxy });
243
311
  await solver.turnstile('...', 'https://example.com', { proxy });
312
+ await solver.geetest('gt', 'challenge', 'https://example.com', { proxy });
244
313
  ```
245
314
 
246
- Supported proxy types: `HTTP`, `HTTPS`, `SOCKS5`, `SOCKS5H`. The `uri` may include
315
+ Supported proxy types: `HTTP`, `HTTPS`, `SOCKS5`, `SOCKS5H` — matched
316
+ case-insensitively. Anything else (including `SOCKS4`) raises
317
+ `ValidationException` before the request is sent. The `uri` may include
247
318
  credentials (`login:password@host:port`) or be a bare `host:port`.
248
319
 
249
320
  ---
250
321
 
251
- ## 9. Concurrency
322
+ ## 10. Concurrency
252
323
 
253
324
  Every solve method returns a Promise, so you can solve many captchas concurrently
254
325
  with `Promise.all`:
@@ -279,7 +350,7 @@ const results = await Promise.allSettled([task1, task2]);
279
350
 
280
351
  ---
281
352
 
282
- ## 10. The manual workflow
353
+ ## 11. The manual workflow
283
354
 
284
355
  If you want to submit now and collect the answer later, use the two low-level
285
356
  steps directly.
@@ -316,7 +387,7 @@ Pass `1` as the second argument to `getResult` to get the full object (including
316
387
 
317
388
  ---
318
389
 
319
- ## 11. Return values
390
+ ## 12. Return values
320
391
 
321
392
  Every high-level solve method (`normal`, `recaptcha`, `turnstile`, `solve`)
322
393
  resolves to an object:
@@ -334,7 +405,7 @@ the solution string (or an object when called with `json = 1`).
334
405
 
335
406
  ---
336
407
 
337
- ## 12. Error handling
408
+ ## 13. Error handling
338
409
 
339
410
  The SDK throws four error types, all subclasses of `CapSkipError`:
340
411
 
@@ -388,7 +459,7 @@ try {
388
459
 
389
460
  ---
390
461
 
391
- ## 13. End-to-end: solve and submit
462
+ ## 14. End-to-end: solve and submit
392
463
 
393
464
  A realistic flow — solve a reCAPTCHA, then submit the token to the target site
394
465
  through the **same** proxy:
@@ -453,7 +524,7 @@ await fetch(CHALLENGE_URL, {
453
524
 
454
525
  ---
455
526
 
456
- ## 14. Parameter reference
527
+ ## 15. Parameter reference
457
528
 
458
529
  ### Solve methods
459
530
 
@@ -462,6 +533,7 @@ await fetch(CHALLENGE_URL, {
462
533
  | Image | `normal(file, { json })` |
463
534
  | reCAPTCHA | `recaptcha(sitekey, url, { version, enterprise, ... })` |
464
535
  | Turnstile | `turnstile(sitekey, url, { ... })` |
536
+ | GeeTest v3 | `geetest(gt, challenge, url, { ... })` |
465
537
  | Manual submit | `send(params) -> Promise<id>` |
466
538
  | Manual poll | `getResult(id, json)` |
467
539
 
@@ -474,6 +546,7 @@ The SDK accepts friendly names and converts them to the raw API parameters:
474
546
  | `url` | `pageurl` |
475
547
  | `score`, `minScore` | `min_score` |
476
548
  | `datas`, `data_s` | `data-s` |
549
+ | `apiServer`, `api_subdomain` | `api_server` |
477
550
  | `proxy` (object) | `proxy` + `proxytype` strings |
478
551
 
479
552
  Anything CapSkip does not document for a given captcha type is rejected with
@@ -481,14 +554,17 @@ Anything CapSkip does not document for a given captcha type is rejected with
481
554
 
482
555
  ---
483
556
 
484
- ## 15. Best practices
557
+ ## 16. Best practices
485
558
 
486
559
  - **Keep CapSkip running.** The SDK talks to a local app; if it is not running you
487
560
  get `NetworkException`.
488
561
  - **Use the token immediately.** reCAPTCHA and Turnstile tokens expire within a
489
562
  couple of minutes.
490
563
  - **Match sitekey and pageurl exactly** to the page the widget loads on.
491
- - **Solve and submit from the same IP** (same proxy) for reCAPTCHA and Turnstile.
564
+ - **Fetch a fresh GeeTest `challenge` per solve.** It is single-use and expires in
565
+ about a minute; a cached pair always fails.
566
+ - **Solve and submit from the same IP** (same proxy) for reCAPTCHA, Turnstile, and
567
+ GeeTest.
492
568
  - **Never commit secrets.** Read `CAPSKIP_API_KEY` and proxy credentials from the
493
569
  environment, not source code.
494
570
  - **Tune timeouts** for slow captcha types with `recaptchaTimeout` and
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Solve a GeeTest v3 slider.
5
+ *
6
+ * GeeTest v3 needs two values from the target site:
7
+ *
8
+ * - `gt` static per site
9
+ * - `challenge` single-use, expires in about a minute
10
+ *
11
+ * The site fetches them itself from an endpoint that returns
12
+ * `{"gt": "...", "challenge": "..."}` (often `.../register.php` or a
13
+ * `gettype`/`get.php` request). Open DevTools -> Network to find that request,
14
+ * then request a *fresh* pair right before solving, as this example does.
15
+ */
16
+
17
+ const { CapSkip } = require('capskip');
18
+
19
+ const solver = new CapSkip({
20
+ apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
21
+ host: process.env.CAPSKIP_HOST || '127.0.0.1',
22
+ port: Number(process.env.CAPSKIP_PORT || 8080),
23
+ });
24
+
25
+ // A public GeeTest v3 demo page, and the endpoint that page calls to issue a
26
+ // fresh gt/challenge pair. Safe to run as-is.
27
+ const PAGE_URL = 'https://2captcha.com/demo/geetest';
28
+ const REGISTER_URL = 'https://2captcha.com/api/v1/captcha-demo/gee-test/init-params';
29
+
30
+ /** Get a fresh gt/challenge pair. Replace with the endpoint your target uses. */
31
+ async function fetchChallenge() {
32
+ const resp = await fetch(REGISTER_URL);
33
+ if (!resp.ok) {
34
+ throw new Error(`Could not fetch a gt/challenge pair: HTTP ${resp.status}`);
35
+ }
36
+ return resp.json();
37
+ }
38
+
39
+ (async () => {
40
+ const { gt, challenge } = await fetchChallenge();
41
+
42
+ const result = await solver.geetest(gt, challenge, PAGE_URL);
43
+
44
+ console.log('Captcha ID:', result.captchaId);
45
+ console.log('Challenge: ', result.challenge);
46
+ console.log('Validate: ', result.validate);
47
+ console.log('Seccode: ', result.seccode);
48
+
49
+ // `code` holds the same answer as a raw JSON string, which is what you forward
50
+ // if you are porting code written against another solver's API.
51
+ console.log('Raw code: ', result.code);
52
+
53
+ // Post these back exactly as the site's own front-end would, e.g.:
54
+ //
55
+ // await fetch(LOGIN_URL, {
56
+ // method: 'POST',
57
+ // body: new URLSearchParams({
58
+ // geetest_challenge: result.challenge,
59
+ // geetest_validate: result.validate,
60
+ // geetest_seccode: result.seccode,
61
+ // }),
62
+ // });
63
+ console.log('Form fields:', {
64
+ geetest_challenge: result.challenge,
65
+ geetest_validate: result.validate,
66
+ geetest_seccode: result.seccode,
67
+ });
68
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capskip",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Node.js SDK for the CapSkip local captcha solver",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -26,6 +26,7 @@
26
26
  "recaptcha",
27
27
  "cloudflare",
28
28
  "turnstile",
29
+ "geetest",
29
30
  "capskip",
30
31
  "automation"
31
32
  ],
package/src/apiParams.js CHANGED
@@ -21,12 +21,24 @@ const TURNSTILE_SUBMIT = new Set([
21
21
  'proxy', 'proxytype',
22
22
  ]);
23
23
 
24
+ const GEETEST_SUBMIT = new Set([
25
+ 'method', 'gt', 'challenge', 'pageurl', 'api_server', 'json',
26
+ 'proxy', 'proxytype',
27
+ ]);
28
+
29
+ // The only values CapSkip maps to a proxy scheme; it answers
30
+ // ERROR_BAD_PARAMETERS for anything else, SOCKS4 included. Matched
31
+ // case-insensitively, as the server does.
32
+ const PROXY_TYPES = ['HTTP', 'HTTPS', 'SOCKS5', 'SOCKS5H'];
33
+
24
34
  const PARAM_ALIASES = {
25
35
  url: 'pageurl',
26
36
  score: 'min_score',
27
37
  minScore: 'min_score',
28
38
  datas: 'data-s',
29
39
  data_s: 'data-s',
40
+ apiServer: 'api_server',
41
+ api_subdomain: 'api_server',
30
42
  };
31
43
 
32
44
  function has(obj, key) {
@@ -145,6 +157,38 @@ function validateTurnstileSubmit(params) {
145
157
  }
146
158
  }
147
159
 
160
+ function validateGeetestSubmit(params) {
161
+ // All three are documented as required. gt is static per site, challenge is
162
+ // single-use and expires in about a minute; without them CapSkip answers
163
+ // ERROR_BAD_PARAMETERS, and without pageurl ERROR_PAGEURL. Fail locally so a
164
+ // missing value does not cost a round-trip.
165
+ for (const key of ['gt', 'challenge', 'pageurl']) {
166
+ if (!params[key]) {
167
+ throw new ValidationException(`'${key}' is required for GeeTest v3.`);
168
+ }
169
+ }
170
+
171
+ const unknown = unknownKeys(params, GEETEST_SUBMIT);
172
+ if (unknown.length > 0) {
173
+ throw new ValidationException(
174
+ `Unsupported parameters for GeeTest: ${reprList(unknown)}.`,
175
+ );
176
+ }
177
+ }
178
+
179
+ function validateProxyType(params) {
180
+ const proxytype = params.proxytype;
181
+ if (proxytype === undefined || proxytype === null || proxytype === '') {
182
+ return;
183
+ }
184
+ if (!PROXY_TYPES.includes(String(proxytype).toUpperCase())) {
185
+ throw new ValidationException(
186
+ `Unsupported proxytype '${proxytype}'. `
187
+ + `CapSkip accepts: ${PROXY_TYPES.join(', ')}.`,
188
+ );
189
+ }
190
+ }
191
+
148
192
  function prepareSubmitParams(params, captchaType, version = 'v2') {
149
193
  let prepared = applyParamAliases(params);
150
194
  prepared = applyProxy(prepared);
@@ -155,6 +199,13 @@ function prepareSubmitParams(params, captchaType, version = 'v2') {
155
199
  validateRecaptchaSubmit(prepared, version);
156
200
  } else if (captchaType === 'turnstile') {
157
201
  validateTurnstileSubmit(prepared);
202
+ } else if (captchaType === 'geetest') {
203
+ validateGeetestSubmit(prepared);
204
+ }
205
+
206
+ // Skipped for 'normal', which rejects proxy outright with a clearer message.
207
+ if (captchaType !== 'normal') {
208
+ validateProxyType(prepared);
158
209
  }
159
210
 
160
211
  return prepared;
@@ -165,11 +216,15 @@ module.exports = {
165
216
  RECAPTCHA_V2_SUBMIT,
166
217
  RECAPTCHA_V3_SUBMIT,
167
218
  TURNSTILE_SUBMIT,
219
+ GEETEST_SUBMIT,
220
+ PROXY_TYPES,
168
221
  PARAM_ALIASES,
169
222
  applyParamAliases,
170
223
  applyProxy,
171
224
  validateNormalSubmit,
172
225
  validateRecaptchaSubmit,
173
226
  validateTurnstileSubmit,
227
+ validateGeetestSubmit,
228
+ validateProxyType,
174
229
  prepareSubmitParams,
175
230
  };
package/src/index.js CHANGED
@@ -27,5 +27,5 @@ module.exports = {
27
27
  NetworkException,
28
28
  ApiException,
29
29
  TimeoutException,
30
- version: '1.0.2',
30
+ version: '1.1.0',
31
31
  };
package/src/solver.js CHANGED
@@ -89,6 +89,43 @@ function applyPollResult(result, polled) {
89
89
  return result;
90
90
  }
91
91
 
92
+ // GeeTest answers come back as a JSON string in `request`, keyed with the
93
+ // geetest_ prefix that the target site's own form fields use.
94
+ const GEETEST_FIELDS = [
95
+ ['challenge', 'geetest_challenge'],
96
+ ['validate', 'geetest_validate'],
97
+ ['seccode', 'geetest_seccode'],
98
+ ];
99
+
100
+ /**
101
+ * Expand the GeeTest answer into `challenge` / `validate` / `seccode`.
102
+ *
103
+ * `code` keeps the raw JSON string so callers that forward it verbatim (or that
104
+ * were written against another solver's API) keep working. If it does not parse,
105
+ * the result is returned untouched rather than masking the server's reply.
106
+ */
107
+ function applyGeetestSolution(result) {
108
+ let payload;
109
+ try {
110
+ payload = JSON.parse(result.code || '');
111
+ } catch (err) {
112
+ return result;
113
+ }
114
+
115
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
116
+ return result;
117
+ }
118
+
119
+ for (const [short, prefixed] of GEETEST_FIELDS) {
120
+ const value = payload[prefixed] !== undefined ? payload[prefixed] : payload[short];
121
+ if (value !== undefined && value !== null) {
122
+ result[short] = value;
123
+ }
124
+ }
125
+
126
+ return result;
127
+ }
128
+
92
129
  // CapSkip's in.php returns OK|<id> by default, or {"status":1,"request":"<id>"}
93
130
  // when the submit carried json=1. Accept both so submitting with json=1 works.
94
131
  function parseSubmitResponse(response) {
@@ -112,7 +149,7 @@ function parseSubmitResponse(response) {
112
149
  throw new ApiException(`cannot recognize response ${response}`);
113
150
  }
114
151
 
115
- /** Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile). */
152
+ /** Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile, GeeTest v3). */
116
153
  class CapSkip {
117
154
  constructor({
118
155
  apiKey = 'capskip',
@@ -167,6 +204,32 @@ class CapSkip {
167
204
  });
168
205
  }
169
206
 
207
+ /**
208
+ * Solve a GeeTest v3 slider.
209
+ *
210
+ * `gt` is static per site; `challenge` is single-use and expires in about a
211
+ * minute, so fetch a fresh pair immediately before calling this. Pass
212
+ * `api_server` when the site uses a non-default GeeTest API server domain.
213
+ *
214
+ * The result carries the raw answer as `code` (a JSON string) plus the parsed
215
+ * `challenge`, `validate`, and `seccode` fields to post back to the target site.
216
+ */
217
+ async geetest(gt, challenge, url, options = {}) {
218
+ // Like reCAPTCHA, this is a real browser solve (load, slide, verify) and can
219
+ // retry internally, so it gets the longer of the two timeouts unless the
220
+ // caller asked for a specific one.
221
+ const result = await this.solve({
222
+ timeout: this.recaptchaTimeout,
223
+ gt,
224
+ challenge,
225
+ url,
226
+ ...options,
227
+ method: 'geetest',
228
+ poll_json: 1,
229
+ });
230
+ return applyGeetestSolution(result);
231
+ }
232
+
170
233
  async solve(options = {}) {
171
234
  const {
172
235
  timeout = 0,
@@ -252,6 +315,9 @@ class CapSkip {
252
315
  if (method === 'turnstile') {
253
316
  return prepareSubmitParams(params, 'turnstile');
254
317
  }
318
+ if (method === 'geetest') {
319
+ return prepareSubmitParams(params, 'geetest');
320
+ }
255
321
  return applyProxy(applyParamAliases(params));
256
322
  }
257
323
  }
@@ -263,4 +329,5 @@ module.exports = {
263
329
  parsePollResponse,
264
330
  parseSubmitResponse,
265
331
  applyPollResult,
332
+ applyGeetestSolution,
266
333
  };
package/types/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Type definitions for the CapSkip Node.js SDK.
2
2
 
3
- /** Proxy passed to reCAPTCHA / Turnstile solves. */
3
+ /** Proxy passed to reCAPTCHA / Turnstile / GeeTest solves. */
4
4
  export interface Proxy {
5
5
  /** Proxy type: `HTTP`, `HTTPS`, `SOCKS5`, or `SOCKS5H`. */
6
6
  type: string;
@@ -18,7 +18,7 @@ export interface CapSkipOptions {
18
18
  port?: number;
19
19
  /** Seconds to poll an image captcha before timing out. Default `120`. */
20
20
  defaultTimeout?: number;
21
- /** Seconds to poll reCAPTCHA / Turnstile before timing out. Default `300`. */
21
+ /** Seconds to poll reCAPTCHA / Turnstile / GeeTest before timing out. Default `300`. */
22
22
  recaptchaTimeout?: number;
23
23
  /** Max seconds between polls; starts at `0.25` and backs off to this. Default `5`. */
24
24
  pollingInterval?: number;
@@ -28,10 +28,20 @@ export interface CapSkipOptions {
28
28
  export interface SolveResult {
29
29
  /** CapSkip's internal id for this solve. */
30
30
  captchaId: string;
31
- /** The solution: recognized text for images, a token otherwise. */
31
+ /**
32
+ * The solution: recognized text for images, a token otherwise. For GeeTest
33
+ * this is the raw JSON string CapSkip returns — prefer the parsed
34
+ * `challenge` / `validate` / `seccode` fields below.
35
+ */
32
36
  code: string;
33
37
  /** Turnstile only — the User-Agent to use when submitting the token. */
34
38
  userAgent?: string;
39
+ /** GeeTest only — the `geetest_challenge` value to post back. */
40
+ challenge?: string;
41
+ /** GeeTest only — the `geetest_validate` value to post back. */
42
+ validate?: string;
43
+ /** GeeTest only — the `geetest_seccode` value to post back. */
44
+ seccode?: string;
35
45
  }
36
46
 
37
47
  /** Extra options for {@link CapSkip.normal}. */
@@ -84,6 +94,23 @@ export interface TurnstileOptions {
84
94
  [key: string]: unknown;
85
95
  }
86
96
 
97
+ /** Extra options for {@link CapSkip.geetest}. */
98
+ export interface GeetestOptions {
99
+ /** GeeTest API server domain (alias for `api_server`). */
100
+ apiServer?: string;
101
+ /** GeeTest API server domain (alias for `api_server`). */
102
+ api_subdomain?: string;
103
+ /** GeeTest API server domain, e.g. `"api-na.geetest.com"`. */
104
+ api_server?: string;
105
+ /** `1` to request the raw JSON response from CapSkip. */
106
+ json?: number;
107
+ /** Proxy used for solving. */
108
+ proxy?: Proxy | string;
109
+ /** Proxy type when `proxy` is a bare string. */
110
+ proxytype?: string;
111
+ [key: string]: unknown;
112
+ }
113
+
87
114
  /** Options for the {@link CapSkip.solve} manual workflow. */
88
115
  export interface SolveOptions {
89
116
  /** Poll timeout in seconds (falls back to the configured default). */
@@ -111,6 +138,18 @@ export class CapSkip {
111
138
  recaptcha(sitekey: string, url: string, options?: RecaptchaOptions): Promise<SolveResult>;
112
139
  /** Solve Cloudflare Turnstile (widget or challenge page). */
113
140
  turnstile(sitekey: string, url: string, options?: TurnstileOptions): Promise<SolveResult>;
141
+ /**
142
+ * Solve a GeeTest v3 slider.
143
+ *
144
+ * `gt` is static per site; `challenge` is single-use and expires in about a
145
+ * minute, so fetch a fresh pair immediately before calling this.
146
+ */
147
+ geetest(
148
+ gt: string,
149
+ challenge: string,
150
+ url: string,
151
+ options?: GeetestOptions,
152
+ ): Promise<SolveResult>;
114
153
  /** Submit then poll to completion. Used by the higher-level solve methods. */
115
154
  solve(options?: SolveOptions): Promise<SolveResult>;
116
155
  /** Submit a captcha without polling; resolves to the captcha id. */