capskip 1.3.0 → 1.4.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,47 @@ 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.4.0] - 2026-11-01
9
+
10
+ ### Added
11
+
12
+ - **AWS WAF support** via `amazonWaf(sitekey, url, options)`, sent as
13
+ `method=amazon_waf`. Only `sitekey` and `url` are required.
14
+ On a challenge page (the HTTP 405 page with `window.gokuProps`), `sitekey`,
15
+ `iv` and `context` are `gokuProps.key`, `.iv` and `.context`, and
16
+ `challenge_script` / `captcha_script` are the page's challenge.js and
17
+ captcha.js. On a widget page (`AwsWafCaptcha.renderCaptcha`), `sitekey` is the
18
+ widget's `apiKey` and `jsapiScript` the URL of jsapi.js.
19
+ An AWS WAF answer is **not a token**: the result expands into
20
+ `captcha_voucher` and `existing_token`, which the site's `voucher` endpoint
21
+ (beside challenge.js) trades for the `aws-waf-token` cookie. `code` keeps the
22
+ raw answer.
23
+ - Parameter aliases `challengeScript` for `challenge_script`, `captchaScript`
24
+ for `captcha_script`, and `jsapi_script` for `jsapiScript`.
25
+ - TypeScript definitions for `AmazonWafOptions`, plus the new `SolveResult`
26
+ fields.
27
+ - `examples/amazon_waf.js`.
28
+
29
+ ### Changed
30
+
31
+ - The Capy answer expansion is now a shared helper, used by Capy and AWS WAF
32
+ alike. Capy results are unchanged.
33
+ - The exported `version` constant now reports the package version; it had been
34
+ left at `1.1.0`.
35
+
36
+ ### Notes
37
+
38
+ - `iv` and `context` are optional but travel together: one without the other is
39
+ refused locally, as the server would refuse it. Left out, CapSkip reads both
40
+ from the page at solve time. An optional passed as `''` is treated as not
41
+ sent, the way the server reads it.
42
+ - Every script URL must be `https` on an `awswaf.com` host, with no credentials,
43
+ no port other than 443, and a path ending in `challenge.js`, `captcha.js` or
44
+ `jsapi.js`. CapSkip posts the solution to the host the script names, so any
45
+ other script is refused before the request is made.
46
+ - AWS WAF uses `recaptchaTimeout`: a real browser mints the token the voucher is
47
+ issued for.
48
+
8
49
  ## [1.3.0] - 2026-09-22
9
50
 
10
51
  ### 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 and image captchas from Node.js.**
8
+ **Solve reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, GeeTest, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF 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()`. 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()`. Nothing leaves your network, and there is no credit balance to keep an eye on.
23
23
 
24
24
  ## Supported captcha types
25
25
 
@@ -38,6 +38,8 @@ This SDK is a thin wrapper over that API, with the method names you would expect
38
38
  | **Capy Puzzle solver** (slide puzzle) | `solver.capy(sitekey, url)` |
39
39
  | **CaptchaFox solver** (widget) | `solver.captchafox(sitekey, url)` |
40
40
  | **Friendly Captcha solver** (proof-of-work) | `solver.friendlyCaptcha(sitekey, url, { version })` |
41
+ | **AWS WAF solver** (challenge page) | `solver.amazonWaf(sitekey, url, { iv, context })` |
42
+ | AWS WAF (widget) | `solver.amazonWaf(sitekey, url, { jsapiScript })` |
41
43
 
42
44
  **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()`.
43
45
 
@@ -135,7 +137,7 @@ const solver = new CapSkip({
135
137
  port: 8080, // CapSkip port from app settings
136
138
  defaultTimeout: 120, // seconds — image captcha polling timeout
137
139
  recaptchaTimeout: 300, // seconds — reCAPTCHA / Turnstile / GeeTest / CaptchaFox /
138
- // Friendly Captcha polling timeout
140
+ // Friendly Captcha / AWS WAF polling timeout
139
141
  // (ALTCHA and Capy use defaultTimeout — neither is a browser solve)
140
142
  pollingInterval: 5, // max seconds between res.php polls (starts at 0.25s, backs off to this)
141
143
  });
@@ -313,6 +315,48 @@ a script URL, v1 is assumed. Pass `api_server: 'eu'` for a sitekey on the EU
313
315
  data-residency tenant. A v2 token is roughly six kilobytes, so size whatever
314
316
  carries it accordingly.
315
317
 
318
+ ### AWS WAF
319
+
320
+ AWS WAF puts its CAPTCHA either on a **challenge page** — the HTTP 405
321
+ interstitial that sets `window.gokuProps` — or in a **widget** an ordinary page
322
+ renders with `AwsWafCaptcha.renderCaptcha`. Only `sitekey` and `url` are required.
323
+
324
+ ```js
325
+ // Challenge page: sitekey, iv and context are gokuProps.key, .iv and .context
326
+ const result = await solver.amazonWaf(
327
+ 'AQIDAHjcYuEXAMPLEghicBgQ/7bFaQZ+m5FKCMDnO+vTbNg96AHDh0IR5vgzHNceHYqZR+GOAAAAfjB8',
328
+ 'https://example.com/protected',
329
+ {
330
+ iv: 'CgAFRDIjfgAAA5yY',
331
+ context: 'Rbp3r8fvbuhmrCqpUe8Mh1TLvs8OSKJZiSs2vCF4dt9rxVZI7xYJa2n5k/9r+XaXe7KUIu2I8nXTDHHkDvIOTDc2',
332
+ challenge_script: 'https://3f2a9c7d41b0.edge.sdk.awswaf.com/3f2a9c7d41b0/8e51d0c7a2f4/challenge.js',
333
+ },
334
+ );
335
+
336
+ // Widget page: sitekey is the widget's apiKey, plus the jsapi.js it loads
337
+ const result = await solver.amazonWaf(
338
+ 'Xr9kEXAMPLE2bWQx1vTq8LmZ0aYc4dHs7nFp3gJt6uKw5eRi9AbCdEfGh=_0_1',
339
+ 'https://example.com/protected',
340
+ { jsapiScript: 'https://5b7e0a19c3d8.us-east-1.captcha-sdk.awswaf.com/5b7e0a19c3d8/jsapi.js' },
341
+ );
342
+
343
+ result.captcha_voucher, result.existing_token;
344
+ ```
345
+
346
+ `iv` and `context` are optional and travel together: send both or neither. Left
347
+ out, CapSkip reads them from the page at solve time, which is the safer choice
348
+ when your copy of the page is more than a few minutes old.
349
+
350
+ The answer is not a token. POST `captcha_voucher` and `existing_token` as JSON
351
+ to the `voucher` endpoint beside the page's `challenge.js`; the response's
352
+ `token` is the `aws-waf-token` cookie. The pair is short-lived, so exchange it
353
+ immediately rather than storing it.
354
+
355
+ Every script URL (`challenge_script`, `captcha_script`, `jsapiScript`) must be an
356
+ https URL on an `awswaf.com` host. CapSkip posts the solution to the host the
357
+ script names, so anything else is refused before the request is made. See the
358
+ [AWS WAF API docs](https://capskip.com/api-docs/#aws-waf).
359
+
316
360
  ### With a proxy (every method except image captcha)
317
361
 
318
362
  ```js
@@ -411,7 +455,8 @@ GeeTest additionally expands its answer into `challenge`, `validate`, and
411
455
  `seccode`, while `code` keeps the raw JSON string. CaptchaFox and Friendly
412
456
  Captcha add `token`, the same string as `code`. Capy expands its answer into
413
457
  `captchakey`, `challengekey` and `answer` — it returns an object rather than a
414
- token, and `code` keeps the raw answer. ALTCHA adds `token` (the same
458
+ token, and `code` keeps the raw answer. AWS WAF likewise returns an object,
459
+ expanded into `captcha_voucher` and `existing_token`. ALTCHA adds `token` (the same
415
460
  string as `code`) and `number`, the counter that solved it.
416
461
 
417
462
  ---
@@ -457,7 +502,7 @@ const result: SolveResult = await solver.recaptcha('...', 'https://example.com')
457
502
 
458
503
  ### How do I solve a captcha in Node.js?
459
504
 
460
- Install the CapSkip desktop app, `npm install capskip`, then call the method that matches the widget — `recaptcha()`, `turnstile()`, `geetest()`, `altcha()`, `capy()`, `captchafox()`, `friendlyCaptcha()` 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.
505
+ Install the CapSkip desktop app, `npm install capskip`, then call the method that matches the widget — `recaptcha()`, `turnstile()`, `geetest()`, `altcha()`, `capy()`, `captchafox()`, `friendlyCaptcha()`, `amazonWaf()` 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.
461
506
 
462
507
  ### Is this a free captcha solver?
463
508
 
@@ -465,7 +510,7 @@ The SDK itself is MIT-licensed and free. Solving needs the CapSkip app, which is
465
510
 
466
511
  ### Which captchas can it solve?
467
512
 
468
- reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3, reCAPTCHA Enterprise, Cloudflare Turnstile, GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, and image/text captchas. Not hCaptcha, and not FunCaptcha/Arkose.
513
+ reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3, reCAPTCHA Enterprise, Cloudflare Turnstile, GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF, and image/text captchas. Not hCaptcha, and not FunCaptcha/Arkose.
469
514
 
470
515
  ### Does it work with Puppeteer and Playwright?
471
516
 
@@ -9,7 +9,7 @@ POST http://<host>:<port>/in.php → submit captcha
9
9
  GET http://<host>:<port>/res.php → poll result
10
10
  ```
11
11
 
12
- The SDK only supports the four captcha types documented by CapSkip. Every solve
12
+ The SDK only supports the captcha types documented by CapSkip. Every solve
13
13
  method returns a `Promise`.
14
14
 
15
15
  ---
@@ -27,6 +27,7 @@ method returns a `Promise`.
27
27
  | Capy Puzzle (slide) | `capy()` | `capy` |
28
28
  | CaptchaFox (widget) | `captchafox()` | `captchafox` |
29
29
  | Friendly Captcha (proof-of-work) | `friendlyCaptcha()` | `friendly_captcha` |
30
+ | AWS WAF (challenge page or widget) | `amazonWaf()` | `amazon_waf` |
30
31
 
31
32
  **Proxy** is supported for every method except image captcha. For ALTCHA the
32
33
  proxy is used only for the `challengeUrl` fetch; for Capy, only for the
@@ -612,6 +613,106 @@ browser.
612
613
 
613
614
  ---
614
615
 
616
+ ## 10. AWS WAF — `amazonWaf(sitekey, url, { ... })`
617
+
618
+ AWS WAF puts its CAPTCHA either on a challenge page — the HTTP 405 interstitial
619
+ that sets `window.gokuProps` — or in a widget an ordinary page renders with
620
+ `AwsWafCaptcha.renderCaptcha`. CapSkip solves both in a real browser. See the
621
+ [CapSkip API docs](https://capskip.com/api-docs/#aws-waf).
622
+
623
+ ### POST `/in.php`
624
+
625
+ | Parameter | Type | Required | Description |
626
+ |---|---|---|---|
627
+ | `key` | string | Yes | CapSkip API key |
628
+ | `method` | string | Yes | `amazon_waf` |
629
+ | `sitekey` | string | Yes | Challenge page: `gokuProps.key`. Widget page: the widget's `apiKey` |
630
+ | `pageurl` | string | Yes | Full http or https URL of the protected page |
631
+ | `iv` | string | No | Challenge page: `gokuProps.iv`. Send together with `context` |
632
+ | `context` | string | No | Challenge page: `gokuProps.context`. Send together with `iv` |
633
+ | `challenge_script` | string | No | Full URL of the page's `challenge.js` |
634
+ | `captcha_script` | string | No | Full URL of the page's `captcha.js` |
635
+ | `jsapiScript` | string | No | Widget page: full URL of `jsapi.js` |
636
+ | `json` | int | No | `0` plain text, `1` JSON |
637
+ | `proxy` | string | No | Proxy address |
638
+ | `proxytype` | string | No | Proxy type |
639
+
640
+ The SDK also accepts `challengeScript`, `captchaScript` and `jsapi_script`, and
641
+ sends the documented spelling of each. An optional left `undefined`, `null` or
642
+ `''` is not sent — the server reads an empty value as absent.
643
+
644
+ ### Challenge page or widget page
645
+
646
+ | | Challenge page | Widget page |
647
+ |---|---|---|
648
+ | Recognized by | HTTP 405 with `window.gokuProps` in an inline script | `AwsWafCaptcha.renderCaptcha(container, { apiKey, … })` |
649
+ | `sitekey` | `gokuProps.key` | the widget's `apiKey` |
650
+ | `iv` / `context` | `gokuProps.iv` / `gokuProps.context`, or neither | not sent |
651
+ | Scripts | `challenge_script`, `captcha_script` | `jsapiScript` |
652
+
653
+ `iv` and `context` come off the same page and only work as a pair, so the SDK
654
+ refuses one without the other. Both are optional: left out, CapSkip reads them
655
+ from `pageurl` at solve time, which is the safer choice when your copy of the
656
+ page is more than a few minutes old.
657
+
658
+ ### Script URLs
659
+
660
+ CapSkip posts the solution to the host a script URL names, so it refuses any
661
+ script that is not on AWS WAF. The SDK applies the server's rule before the
662
+ request is made: the URL must be `https`, its host must end in `.awswaf.com`, it
663
+ must carry no credentials and no port other than 443, and its path must be one
664
+ or more segments followed by `challenge.js`, `captcha.js` or `jsapi.js`. A query
665
+ string is allowed. Anything else throws `ValidationException`, because CapSkip is
666
+ not an AWS WAF integration for scripts hosted elsewhere.
667
+
668
+ ### GET `/res.php`
669
+
670
+ The answer is **not a token**. With `json=1` it is an object in `request`:
671
+
672
+ ```json
673
+ {"status":1,"request":{"captcha_voucher":"…","existing_token":"…"}}
674
+ ```
675
+
676
+ and in plain text the same object as one line of JSON after `OK|`. The SDK
677
+ expands it into `captcha_voucher` and `existing_token`; see
678
+ [Return value](#return-value).
679
+
680
+ ### Exchanging the voucher
681
+
682
+ The two values only work as a pair. POST them as JSON to the `voucher` endpoint
683
+ beside the page's `challenge.js` (the same directory, with `voucher` in place of
684
+ `challenge.js`). The response's `token` is the `aws-waf-token` cookie. Exchange
685
+ them immediately rather than storing them for later.
686
+
687
+ ### SDK usage
688
+
689
+ ```js
690
+ // Challenge page
691
+ const result = await solver.amazonWaf(
692
+ 'AQIDAHjcYuEXAMPLEghicBgQ/7bFaQZ+m5FKCMDnO+vTbNg96AHDh0IR5vgzHNceHYqZR+GOAAAAfjB8',
693
+ 'https://example.com/protected',
694
+ {
695
+ iv: 'CgAFRDIjfgAAA5yY',
696
+ context: 'Rbp3r8fvbuhmrCqpUe8Mh1TLvs8OSKJZiSs2vCF4dt9rxVZI7xYJa2n5k/9r+XaXe7KUIu2I8nXTDHHkDvIOTDc2',
697
+ challenge_script: 'https://3f2a9c7d41b0.edge.sdk.awswaf.com/3f2a9c7d41b0/8e51d0c7a2f4/challenge.js',
698
+ },
699
+ );
700
+
701
+ // Widget page
702
+ const result = await solver.amazonWaf(
703
+ 'Xr9kEXAMPLE2bWQx1vTq8LmZ0aYc4dHs7nFp3gJt6uKw5eRi9AbCdEfGh=_0_1',
704
+ 'https://example.com/protected',
705
+ { jsapiScript: 'https://5b7e0a19c3d8.us-east-1.captcha-sdk.awswaf.com/5b7e0a19c3d8/jsapi.js' },
706
+ );
707
+
708
+ result.captcha_voucher, result.existing_token;
709
+ ```
710
+
711
+ Uses `recaptchaTimeout`: a real browser mints the token the voucher is issued
712
+ for, and the puzzle rounds go through AWS on top of that.
713
+
714
+ ---
715
+
615
716
  ## Return value
616
717
 
617
718
  Every solve method resolves to:
@@ -663,6 +764,19 @@ target form takes (`code` keeps the raw answer):
663
764
  }
664
765
  ```
665
766
 
767
+ AWS WAF returns an object too, expanded into the voucher pair the site's
768
+ `voucher` endpoint trades for the `aws-waf-token` cookie (`code` keeps the raw
769
+ answer):
770
+
771
+ ```js
772
+ {
773
+ captchaId: '12345',
774
+ code: { captcha_voucher: '…', existing_token: '…' },
775
+ captcha_voucher: 'eyJ2b3VjaGVyIjoiRVhBTVBMRSJ9.9QwZcEXAMPLEvoucherJk4LmN0pQ',
776
+ existing_token: '7d4c2a91-EXAMPLE-4f0b-9e6d-3a1b5c8e2f70:EQoAeXAMPLEtokenAAAA:Wm9wc2E=',
777
+ }
778
+ ```
779
+
666
780
  GeeTest additionally expands its answer into `challenge`, `validate`, and
667
781
  `seccode` (`code` keeps the raw JSON string):
668
782
 
@@ -697,6 +811,9 @@ Convenience aliases mapped before sending to CapSkip:
697
811
  | `captchaKey` | `captchakey` |
698
812
  | `moduleScript` | `module_script` |
699
813
  | `nomoduleScript` / `noModuleScript` | `nomodule_script` |
814
+ | `challengeScript` | `challenge_script` |
815
+ | `captchaScript` | `captcha_script` |
816
+ | `jsapi_script` | `jsapiScript` |
700
817
  | `proxy` object | `proxy` + `proxytype` strings |
701
818
 
702
819
  ```js
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Solve an AWS WAF CAPTCHA.
5
+ *
6
+ * AWS WAF shows up in two forms, and the values you need depend on which one
7
+ * the target page uses:
8
+ *
9
+ * * A challenge page -- the HTTP 405 interstitial that sets `window.gokuProps`
10
+ * in an inline script. `sitekey` is `gokuProps.key`; `iv` and `context` are
11
+ * `gokuProps.iv` and `gokuProps.context`. `challenge_script` and
12
+ * `captcha_script` are the full URLs of the page's challenge.js and
13
+ * captcha.js.
14
+ * * A widget page -- an ordinary page that calls
15
+ * `AwsWafCaptcha.renderCaptcha(container, { apiKey, ... })`. `sitekey` is
16
+ * that `apiKey`, and `jsapiScript` is the full URL of jsapi.js.
17
+ *
18
+ * Only `sitekey` and the page URL are required. `iv` and `context` travel
19
+ * together: send both or neither. Left out, CapSkip reads them from the page
20
+ * at solve time, which is the safer choice when your copy of the page is more
21
+ * than a few minutes old.
22
+ *
23
+ * Every script URL must be an https URL on an awswaf.com host. CapSkip posts
24
+ * the solution to the host the script names, so it refuses anything else
25
+ * before the request is made.
26
+ *
27
+ * Unlike reCAPTCHA or Turnstile, the answer is NOT a token you put in a form.
28
+ * It is a voucher pair that AWS trades for the `aws-waf-token` cookie.
29
+ */
30
+
31
+ const { CapSkip } = require('capskip');
32
+
33
+ const solver = new CapSkip({
34
+ apiKey: process.env.CAPSKIP_API_KEY || 'capskip',
35
+ host: process.env.CAPSKIP_HOST || '127.0.0.1',
36
+ port: Number(process.env.CAPSKIP_PORT || 8080),
37
+ });
38
+
39
+ const PAGE_URL = 'https://example.com/protected';
40
+
41
+ // From the challenge page's window.gokuProps.
42
+ const SITEKEY = 'AQIDAHjcYuEXAMPLEghicBgQ/7bFaQZ+m5FKCMDnO+vTbNg96AHDh0IR5vgzHNceHYqZR+GOAAAAfjB8';
43
+ const IV = 'CgAFRDIjfgAAA5yY';
44
+ const CONTEXT = 'Rbp3r8fvbuhmrCqpUe8Mh1TLvs8OSKJZiSs2vCF4dt9rxVZI7xYJa2n5k/9r+XaXe7KUIu2I8nXTDHHkDvIOTDc2';
45
+ const CHALLENGE_JS = 'https://3f2a9c7d41b0.edge.sdk.awswaf.com/3f2a9c7d41b0/8e51d0c7a2f4/challenge.js';
46
+
47
+ async function main() {
48
+ const result = await solver.amazonWaf(SITEKEY, PAGE_URL, {
49
+ iv: IV,
50
+ context: CONTEXT,
51
+ challenge_script: CHALLENGE_JS,
52
+ });
53
+
54
+ console.log('Captcha ID: ', result.captchaId);
55
+ console.log('Voucher: ', `${result.captcha_voucher.slice(0, 32)}...`);
56
+ console.log('Existing token:', `${result.existing_token.slice(0, 32)}...`);
57
+
58
+ // --- A widget page -------------------------------------------------------
59
+ // The widget's apiKey as the sitekey, and the jsapi.js it loads. No iv or
60
+ // context: a widget page has none.
61
+ //
62
+ // await solver.amazonWaf(
63
+ // 'Xr9kEXAMPLE2bWQx1vTq8LmZ0aYc4dHs7nFp3gJt6uKw5eRi9AbCdEfGh=_0_1',
64
+ // PAGE_URL,
65
+ // { jsapiScript: 'https://5b7e0a19c3d8.us-east-1.captcha-sdk.awswaf.com/5b7e0a19c3d8/jsapi.js' },
66
+ // );
67
+ //
68
+ // --- With a per-request proxy --------------------------------------------
69
+ // await solver.amazonWaf(SITEKEY, PAGE_URL, {
70
+ // proxy: { type: 'HTTP', uri: 'login:password@1.2.3.4:8080' },
71
+ // });
72
+
73
+ // Exchange the pair for the aws-waf-token cookie: POST both values as JSON
74
+ // to the `voucher` endpoint that sits beside the page's challenge.js. The
75
+ // response's `token` is the cookie value. Do this immediately -- the voucher
76
+ // is short-lived, so do not store the pair for a later request.
77
+ const voucherUrl = new URL('voucher', CHALLENGE_JS).href;
78
+ console.log('Voucher URL: ', voucherUrl);
79
+ //
80
+ // const response = await fetch(voucherUrl, {
81
+ // method: 'POST',
82
+ // headers: { 'Content-Type': 'application/json' },
83
+ // body: JSON.stringify({
84
+ // captcha_voucher: result.captcha_voucher,
85
+ // existing_token: result.existing_token,
86
+ // }),
87
+ // });
88
+ // const { token } = await response.json();
89
+ //
90
+ // // Then request the protected page with the cookie:
91
+ // await fetch(PAGE_URL, { headers: { Cookie: `aws-waf-token=${token}` } });
92
+ }
93
+
94
+ main().catch((error) => {
95
+ console.error(`${error.name}: ${error.message}`);
96
+ process.exitCode = 1;
97
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "capskip",
3
- "version": "1.3.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.",
3
+ "version": "1.4.0",
4
+ "description": "Captcha solver for Node.js: solve reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest, AWS WAF 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": {
@@ -39,6 +39,8 @@
39
39
  "cloudflare-turnstile",
40
40
  "geetest",
41
41
  "geetest-solver",
42
+ "aws-waf",
43
+ "aws-waf-captcha",
42
44
  "image-captcha",
43
45
  "ocr",
44
46
  "2captcha-alternative",
package/src/apiParams.js CHANGED
@@ -46,6 +46,19 @@ const FRIENDLY_CAPTCHA_SUBMIT = new Set([
46
46
  'api_server', 'useragent', 'json', 'proxy', 'proxytype',
47
47
  ]);
48
48
 
49
+ const AMAZON_WAF_SUBMIT = new Set([
50
+ 'method', 'sitekey', 'pageurl', 'iv', 'context', 'challenge_script',
51
+ 'captcha_script', 'jsapiScript', 'json', 'proxy', 'proxytype',
52
+ ]);
53
+
54
+ // The script parameters of an AWS WAF task. CapSkip POSTs the solution to the
55
+ // host these name, so it refuses any that is not a genuine AWS WAF one.
56
+ const AMAZON_WAF_SCRIPTS = ['challenge_script', 'captcha_script', 'jsapiScript'];
57
+
58
+ // The server's own test: one or more [A-Za-z0-9_-] path segments, then one of
59
+ // the three AWS WAF script files.
60
+ const AMAZON_WAF_SCRIPT_PATH = /^(\/[A-Za-z0-9_-]+)+\/(challenge|captcha|jsapi)\.js$/;
61
+
49
62
  // CapSkip solves the puzzle family only. `avatar` is a different challenge
50
63
  // behind a different endpoint; the server refuses it at submit time rather than
51
64
  // answering it with a puzzle answer, which would bill for a solve the target
@@ -81,6 +94,12 @@ const PARAM_ALIASES = {
81
94
  moduleScript: 'module_script',
82
95
  nomoduleScript: 'nomodule_script',
83
96
  noModuleScript: 'nomodule_script',
97
+ // AWS WAF: the API documents challenge_script / captcha_script in snake case
98
+ // and jsapiScript in camel case, as 2Captcha does. Accept the other spelling
99
+ // of each, which is what createTask-style callers arrive with.
100
+ challengeScript: 'challenge_script',
101
+ captchaScript: 'captcha_script',
102
+ jsapi_script: 'jsapiScript',
84
103
  };
85
104
 
86
105
  function has(obj, key) {
@@ -353,6 +372,90 @@ function validateFriendlyCaptchaSubmit(params) {
353
372
  }
354
373
  }
355
374
 
375
+ /**
376
+ * True for a script URL CapSkip will accept on an AWS WAF task.
377
+ *
378
+ * Mirrors the server: https, a host under .awswaf.com, no credentials or
379
+ * explicit port, and a path ending in challenge.js, captcha.js or jsapi.js.
380
+ * The server only applies the check once the task is queued, so failing here
381
+ * saves a submit that could never solve.
382
+ */
383
+ function isAmazonWafScriptUrl(url) {
384
+ let parsed;
385
+ try {
386
+ parsed = new URL(String(url).trim());
387
+ } catch (error) {
388
+ return false;
389
+ }
390
+ // WHATWG URL already lowercases the scheme and host, and drops a :443 on an
391
+ // https URL, so `port` is empty for both the default and an explicit 443.
392
+ return parsed.protocol === 'https:'
393
+ && parsed.hostname.toLowerCase().endsWith('.awswaf.com')
394
+ && parsed.username === ''
395
+ && parsed.password === ''
396
+ && (parsed.port === '' || parsed.port === '443')
397
+ && AMAZON_WAF_SCRIPT_PATH.test(parsed.pathname);
398
+ }
399
+
400
+ /**
401
+ * Drop unset optionals, and the empty ones too: an AWS WAF optional left as ''
402
+ * means "not sent" to the server, exactly like an absent one, so it is dropped
403
+ * rather than tripping the iv/context pairing.
404
+ */
405
+ function normalizeAmazonWafSubmit(params) {
406
+ const out = {};
407
+ for (const [key, value] of Object.entries(dropUnset(params))) {
408
+ if (value !== '') {
409
+ out[key] = value;
410
+ }
411
+ }
412
+ return out;
413
+ }
414
+
415
+ function validateAmazonWafSubmit(params) {
416
+ // Without sitekey the server answers ERROR_BAD_PARAMETERS and without
417
+ // pageurl ERROR_PAGEURL. Fail locally so a missing value costs no round-trip.
418
+ for (const key of ['sitekey', 'pageurl']) {
419
+ if (!params[key]) {
420
+ throw new ValidationException(`'${key}' is required for Amazon WAF.`);
421
+ }
422
+ }
423
+
424
+ const pageurl = String(params.pageurl).trim();
425
+ if (!/^https?:\/\//i.test(pageurl)) {
426
+ throw new ValidationException(
427
+ `'pageurl' must be a full http or https URL for Amazon WAF, got '${pageurl}'.`,
428
+ );
429
+ }
430
+
431
+ // iv and context come off the same challenge page and only work as a pair;
432
+ // the server refuses one without the other with ERROR_BAD_PARAMETERS.
433
+ if (Boolean(params.iv) !== Boolean(params.context)) {
434
+ throw new ValidationException(
435
+ "Amazon WAF 'iv' and 'context' go together: send both (a challenge "
436
+ + 'page) or neither (a widget page, or let CapSkip read them from pageurl).',
437
+ );
438
+ }
439
+
440
+ for (const key of AMAZON_WAF_SCRIPTS) {
441
+ const value = params[key];
442
+ if (value && !isAmazonWafScriptUrl(value)) {
443
+ throw new ValidationException(
444
+ `'${key}' must be the https URL of an AWS WAF script on an awswaf.com `
445
+ + `host (challenge.js, captcha.js or jsapi.js), got '${value}'. CapSkip `
446
+ + 'refuses any other script, because it is not an AWS WAF integration.',
447
+ );
448
+ }
449
+ }
450
+
451
+ const unknown = unknownKeys(params, AMAZON_WAF_SUBMIT);
452
+ if (unknown.length > 0) {
453
+ throw new ValidationException(
454
+ `Unsupported parameters for Amazon WAF: ${reprList(unknown)}.`,
455
+ );
456
+ }
457
+ }
458
+
356
459
  function validateProxyType(params) {
357
460
  const proxytype = params.proxytype;
358
461
  if (proxytype === undefined || proxytype === null || proxytype === '') {
@@ -390,6 +493,9 @@ function prepareSubmitParams(params, captchaType, version = 'v2') {
390
493
  } else if (captchaType === 'friendly_captcha') {
391
494
  prepared = dropUnset(prepared);
392
495
  validateFriendlyCaptchaSubmit(prepared);
496
+ } else if (captchaType === 'amazon_waf') {
497
+ prepared = normalizeAmazonWafSubmit(prepared);
498
+ validateAmazonWafSubmit(prepared);
393
499
  }
394
500
 
395
501
  // Skipped for 'normal', which rejects proxy outright with a clearer message.
@@ -410,6 +516,8 @@ module.exports = {
410
516
  CAPY_SUBMIT,
411
517
  CAPTCHAFOX_SUBMIT,
412
518
  FRIENDLY_CAPTCHA_SUBMIT,
519
+ AMAZON_WAF_SUBMIT,
520
+ AMAZON_WAF_SCRIPTS,
413
521
  CAPY_VERSIONS,
414
522
  FRIENDLY_CAPTCHA_VERSIONS,
415
523
  PROXY_TYPES,
@@ -426,6 +534,9 @@ module.exports = {
426
534
  validateCapySubmit,
427
535
  validateCaptchaFoxSubmit,
428
536
  validateFriendlyCaptchaSubmit,
537
+ isAmazonWafScriptUrl,
538
+ normalizeAmazonWafSubmit,
539
+ validateAmazonWafSubmit,
429
540
  validateProxyType,
430
541
  prepareSubmitParams,
431
542
  };
package/src/index.js CHANGED
@@ -27,5 +27,5 @@ module.exports = {
27
27
  NetworkException,
28
28
  ApiException,
29
29
  TimeoutException,
30
- version: '1.1.0',
30
+ version: '1.4.0',
31
31
  };
package/src/solver.js CHANGED
@@ -222,7 +222,7 @@ function applyAltchaSolution(result) {
222
222
  const CAPY_FIELDS = ['captchakey', 'challengekey', 'answer', 'respKey'];
223
223
 
224
224
  /**
225
- * Expand the Capy answer into `captchakey` / `challengekey` / `answer`.
225
+ * Copy `fields` out of an answer that is an object rather than a token.
226
226
  *
227
227
  * `code` keeps the raw answer -- an object when polled with json=1, where the
228
228
  * server puts it straight into `request`, or the JSON string it sends after
@@ -232,7 +232,7 @@ const CAPY_FIELDS = ['captchakey', 'challengekey', 'answer', 'respKey'];
232
232
  * If the answer does not parse, the result is returned untouched rather than
233
233
  * masking the server's reply.
234
234
  */
235
- function applyCapySolution(result) {
235
+ function expandObjectAnswer(result, fields) {
236
236
  let payload = result.solution;
237
237
  delete result.solution;
238
238
 
@@ -253,7 +253,7 @@ function applyCapySolution(result) {
253
253
  return result;
254
254
  }
255
255
 
256
- for (const field of CAPY_FIELDS) {
256
+ for (const field of fields) {
257
257
  if (Object.prototype.hasOwnProperty.call(payload, field)) {
258
258
  result[field] = payload[field];
259
259
  }
@@ -262,6 +262,21 @@ function applyCapySolution(result) {
262
262
  return result;
263
263
  }
264
264
 
265
+ /** Expand the Capy answer into `captchakey` / `challengekey` / `answer`. */
266
+ function applyCapySolution(result) {
267
+ return expandObjectAnswer(result, CAPY_FIELDS);
268
+ }
269
+
270
+ // An AWS WAF solution is two values that only work as a pair: the voucher AWS
271
+ // issued for the answered puzzle, and the token it was issued for. The site's
272
+ // own /voucher endpoint trades them for the aws-waf-token cookie.
273
+ const AMAZON_WAF_FIELDS = ['captcha_voucher', 'existing_token'];
274
+
275
+ /** Expand the AWS WAF answer into `captcha_voucher` / `existing_token`. */
276
+ function applyAmazonWafSolution(result) {
277
+ return expandObjectAnswer(result, AMAZON_WAF_FIELDS);
278
+ }
279
+
265
280
  /**
266
281
  * Expose a single-token answer as `token`, named for the form field it fills.
267
282
  *
@@ -309,7 +324,10 @@ function parseSubmitResponse(response) {
309
324
  throw new ApiException(`cannot recognize response ${response}`);
310
325
  }
311
326
 
312
- /** Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile, GeeTest v3, ALTCHA). */
327
+ /**
328
+ * Client for the CapSkip local captcha solver (image, reCAPTCHA, Turnstile,
329
+ * GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, AWS WAF).
330
+ */
313
331
  class CapSkip {
314
332
  constructor({
315
333
  apiKey = 'capskip',
@@ -553,6 +571,55 @@ class CapSkip {
553
571
  return applyTokenSolution(result);
554
572
  }
555
573
 
574
+ /**
575
+ * Solve an AWS WAF (Amazon WAF) CAPTCHA.
576
+ *
577
+ * Only `sitekey` and `url` are required:
578
+ *
579
+ * - On a challenge page (the HTTP 405 page with `window.gokuProps`), `sitekey`
580
+ * is `gokuProps.key`, and `iv` and `context` are `gokuProps.iv` and
581
+ * `gokuProps.context`. Send both or neither -- left out, CapSkip reads them
582
+ * from `url` at solve time, which is the safer choice when your copy of the
583
+ * page is more than a few minutes old. `challenge_script` and
584
+ * `captcha_script` are the full URLs of the page's challenge.js and
585
+ * captcha.js.
586
+ * - On a widget page (`AwsWafCaptcha.renderCaptcha`), `sitekey` is the
587
+ * widget's `apiKey`; pass `jsapiScript` with the full URL of jsapi.js and no
588
+ * `iv` / `context`.
589
+ *
590
+ * Every script URL must be an https URL on an awswaf.com host; anything else
591
+ * is refused before the request is sent.
592
+ *
593
+ * The result carries `captcha_voucher` and `existing_token`, plus the raw
594
+ * answer as `code`. The two only work as a pair: POST them as JSON to the
595
+ * `voucher` endpoint next to the page's challenge.js and the response's
596
+ * `token` is the aws-waf-token cookie. Exchange them as soon as they arrive
597
+ * rather than storing them for later.
598
+ */
599
+ async amazonWaf(sitekey, url, options = {}) {
600
+ // An unset optional is dropped rather than sent as undefined; an empty one
601
+ // is dropped during validation, since the server reads '' as "not sent".
602
+ const given = {};
603
+ for (const [key, value] of Object.entries(options)) {
604
+ if (value !== undefined && value !== null) {
605
+ given[key] = value;
606
+ }
607
+ }
608
+
609
+ // A real browser mints the token the voucher is issued for, and the puzzle
610
+ // rounds go through AWS on top of that, so it gets the longer of the two
611
+ // timeouts unless the caller asked for a specific one.
612
+ const result = await this.solve({
613
+ timeout: this.recaptchaTimeout,
614
+ sitekey,
615
+ url,
616
+ ...given,
617
+ method: 'amazon_waf',
618
+ poll_json: 1,
619
+ });
620
+ return applyAmazonWafSolution(result);
621
+ }
622
+
556
623
  async solve(options = {}) {
557
624
  const {
558
625
  timeout = 0,
@@ -653,6 +720,9 @@ class CapSkip {
653
720
  if (method === 'friendly_captcha') {
654
721
  return prepareSubmitParams(params, 'friendly_captcha');
655
722
  }
723
+ if (method === 'amazon_waf') {
724
+ return prepareSubmitParams(params, 'amazon_waf');
725
+ }
656
726
  return applyProxy(applyParamAliases(params));
657
727
  }
658
728
  }
@@ -667,5 +737,7 @@ module.exports = {
667
737
  applyGeetestSolution,
668
738
  applyAltchaSolution,
669
739
  applyCapySolution,
740
+ applyAmazonWafSolution,
741
+ expandObjectAnswer,
670
742
  applyTokenSolution,
671
743
  };
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 before timing out. Default `300`. ALTCHA and Capy use
23
+ * Captcha / AWS WAF before timing out. Default `300`. ALTCHA and Capy use
24
24
  * `defaultTimeout` instead — neither is a browser solve.
25
25
  */
26
26
  recaptchaTimeout?: number;
@@ -36,8 +36,8 @@ export interface SolveResult {
36
36
  * The solution: recognized text for images, a token otherwise. For GeeTest
37
37
  * this is the raw JSON string CapSkip returns — prefer the parsed
38
38
  * `challenge` / `validate` / `seccode` fields below. For ALTCHA it is the
39
- * base64 token, also exposed as `token`. For Capy it is the raw answer
40
- * object, expanded into the three fields below.
39
+ * base64 token, also exposed as `token`. For Capy and AWS WAF it is the raw
40
+ * answer object, expanded into the fields below.
41
41
  */
42
42
  code: string | Record<string, unknown>;
43
43
  /**
@@ -73,6 +73,15 @@ export interface SolveResult {
73
73
  * 2Captcha's documented response; it carries nothing.
74
74
  */
75
75
  respKey?: string;
76
+ /**
77
+ * AWS WAF only — the voucher AWS issued for the answered puzzle. Only valid
78
+ * together with `existing_token`: POST both as JSON to the `voucher` endpoint
79
+ * beside the page's challenge.js, whose response `token` is the
80
+ * `aws-waf-token` cookie. Exchange immediately.
81
+ */
82
+ captcha_voucher?: string;
83
+ /** AWS WAF only — the token the voucher was issued for. Pair with `captcha_voucher`. */
84
+ existing_token?: string;
76
85
  }
77
86
 
78
87
  /** Extra options for {@link CapSkip.normal}. */
@@ -239,6 +248,38 @@ export interface FriendlyCaptchaOptions {
239
248
  [key: string]: unknown;
240
249
  }
241
250
 
251
+ /** Extra options for {@link CapSkip.amazonWaf}. */
252
+ export interface AmazonWafOptions {
253
+ /**
254
+ * Challenge page: `gokuProps.iv`. Send together with `context`, or leave both
255
+ * out and CapSkip reads them from the page at solve time.
256
+ */
257
+ iv?: string;
258
+ /** Challenge page: `gokuProps.context`. Send together with `iv`. */
259
+ context?: string;
260
+ /** Full https URL of the page's `challenge.js` on an `awswaf.com` host. */
261
+ challenge_script?: string;
262
+ /** Full https URL of the page's `challenge.js` (alias for `challenge_script`). */
263
+ challengeScript?: string;
264
+ /** Full https URL of the page's `captcha.js` on an `awswaf.com` host. */
265
+ captcha_script?: string;
266
+ /** Full https URL of the page's `captcha.js` (alias for `captcha_script`). */
267
+ captchaScript?: string;
268
+ /** Widget page: full https URL of `jsapi.js` on an `awswaf.com` host. */
269
+ jsapiScript?: string;
270
+ /** Widget page: full https URL of `jsapi.js` (alias for `jsapiScript`). */
271
+ jsapi_script?: string;
272
+ /** Poll timeout in seconds. Default: the client's `recaptchaTimeout`. */
273
+ timeout?: number;
274
+ /** `1` to request the raw JSON response from CapSkip. */
275
+ json?: number;
276
+ /** Proxy used for solving. */
277
+ proxy?: Proxy | string;
278
+ /** Proxy type when `proxy` is a bare string. */
279
+ proxytype?: string;
280
+ [key: string]: unknown;
281
+ }
282
+
242
283
  /** Options for the {@link CapSkip.solve} manual workflow. */
243
284
  export interface SolveOptions {
244
285
  /** Poll timeout in seconds (falls back to the configured default). */
@@ -320,6 +361,21 @@ export class CapSkip {
320
361
  url: string,
321
362
  options?: FriendlyCaptchaOptions,
322
363
  ): Promise<SolveResult>;
364
+ /**
365
+ * Solve an AWS WAF CAPTCHA.
366
+ *
367
+ * On a challenge page, `sitekey` is `gokuProps.key` and `iv` / `context` are
368
+ * `gokuProps.iv` / `gokuProps.context` (both or neither). On a widget page,
369
+ * `sitekey` is the widget's `apiKey`; pass `jsapiScript`. Script URLs must be
370
+ * https on an `awswaf.com` host. The result carries `captcha_voucher` and
371
+ * `existing_token`, which the site's `voucher` endpoint trades for the
372
+ * `aws-waf-token` cookie.
373
+ */
374
+ amazonWaf(
375
+ sitekey: string,
376
+ url: string,
377
+ options?: AmazonWafOptions,
378
+ ): Promise<SolveResult>;
323
379
  /** Submit then poll to completion. Used by the higher-level solve methods. */
324
380
  solve(options?: SolveOptions): Promise<SolveResult>;
325
381
  /** Submit a captcha without polling; resolves to the captcha id. */