From 5bfe9fcd32b6ed58a26dc810c5e9bf8faa383074 Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Tue, 29 Sep 2026 17:01:21 +0300 Subject: [PATCH 1/2] fix: support user agent for Turnstile Challenge --- README.md | 11 +++++------ examples/async/turnstile.js | 12 ++++++------ examples/sync/turnstile.js | 12 ++++++------ src/tasks.ts | 14 +++++++++----- tests/unit/sync/turnstile.test.js | 4 ++-- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 1128a15..3145906 100644 --- a/README.md +++ b/README.md @@ -317,13 +317,12 @@ that the target page expects in `cf-turnstile-response`. | `action` | no | Value of the widget's `data-action` attribute, if set. | | `data` | no | Custom payload from the widget's `data-cdata` attribute, if set. | | `pagedata` | no | Value of the `chlPageData` parameter, needed for some Cloudflare challenge pages beyond the basic widget. Note the lowercase, single-word spelling -- unlike every other field on this page, the real API does not accept `pageData`. | +| `userAgent` | no | Current browser User-Agent. Pass it for Cloudflare Challenge pages; it is not needed for a standalone widget. | -There's no `userAgent` input for this type -- the worker picks its own while solving -and returns it in the response instead (see below). - -**Response:** `token` -- submit as `cf-turnstile-response` -- and `userAgent`, the User-Agent the -worker actually solved with. The token is tied to that fingerprint, so submit it with that exact -User-Agent, not one you chose yourself. +**Response:** `token` -- submit as `cf-turnstile-response`. For Cloudflare Challenge +pages, pass `action`, `data`, `pagedata`, and the current browser `userAgent` in the +task. Keep the browser User-Agent unchanged when invoking the callback. The solution +may also contain `userAgent`; do not switch the browser to this returned value. ```javascript import { CaptchaClient, Tasks } from '@captcha-solver-api/javascript-sdk'; diff --git a/examples/async/turnstile.js b/examples/async/turnstile.js index 6bb9d93..06c0271 100644 --- a/examples/async/turnstile.js +++ b/examples/async/turnstile.js @@ -4,7 +4,8 @@ * Prerequisites: * Set the CAPTCHA_API_KEY environment variable. * Replace websiteURL and websiteKey with values from your target page. - * For Cloudflare Challenge pages, also extract and pass action, data, and pagedata. + * For Cloudflare Challenge pages, also extract and pass action, data, pagedata, + * and the current browser userAgent. */ import 'dotenv/config'; @@ -15,18 +16,17 @@ const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- -// There's no userAgent input field here -- the worker picks its own User-Agent -// while solving and returns it as result.userAgent. The token is tied to that -// browser fingerprint, so submit it with the exact User-Agent the response gives -// you, not one you chose yourself. +// For a Cloudflare Challenge page, pass the current browser User-Agent together +// with action, data, and pagedata. Do not change the browser User-Agent after solving. try { const task = new Tasks.TurnstileProxyless({ websiteURL: 'https://example.com/login', // Full URL of the page with Turnstile - websiteKey: '0x4AAAAAAAxxxxxxxxxxxxxxxx' // data-sitekey attribute value + websiteKey: '0x4AAAAAAAxxxxxxxxxxxxxxxx', // data-sitekey attribute value // Optional fields (pass only if the target site sets them) // action: 'login', // Value of data-action attribute // data: 'custom-cdata-value', // Value of data-cdata attribute // pagedata: 'chl-page-data-value', // Value of chlPageData parameter + // userAgent: 'Mozilla/5.0 ...', // Current browser User-Agent }); const result = await captchaSolver.solve(task); // Solution contains { token: "0.zxcv...", userAgent: "Mozilla/5.0 ..." } diff --git a/examples/sync/turnstile.js b/examples/sync/turnstile.js index 02b0656..8eaea0b 100644 --- a/examples/sync/turnstile.js +++ b/examples/sync/turnstile.js @@ -4,7 +4,8 @@ * Prerequisites: * Set the CAPTCHA_API_KEY environment variable. * Replace websiteURL and websiteKey with values from your target page. - * For Cloudflare Challenge pages, also extract and pass action, data, and pagedata. + * For Cloudflare Challenge pages, also extract and pass action, data, pagedata, + * and the current browser userAgent. */ import 'dotenv/config'; @@ -15,17 +16,16 @@ const apiKey = process.env.CAPTCHA_API_KEY || 'YOUR_API_KEY'; const captchaSolver = new CaptchaClient({ clientKey: apiKey }); // --- Proxyless example --- -// There's no userAgent input field here -- the worker picks its own User-Agent -// while solving and returns it as result.userAgent. The token is tied to that -// browser fingerprint, so submit it with the exact User-Agent the response gives -// you, not one you chose yourself. +// For a Cloudflare Challenge page, pass the current browser User-Agent together +// with action, data, and pagedata. Do not change the browser User-Agent after solving. const proxylessTask = new Tasks.TurnstileProxyless({ websiteURL: 'https://example.com/login', // Full URL of the page with Turnstile - websiteKey: '0x4AAAAAAAxxxxxxxxxxxxxxxx' // data-sitekey attribute value + websiteKey: '0x4AAAAAAAxxxxxxxxxxxxxxxx', // data-sitekey attribute value // Optional fields (pass only if the target site sets them) // action: 'login', // Value of data-action attribute // data: 'custom-cdata-value', // Value of data-cdata attribute // pagedata: 'chl-page-data-value', // Value of chlPageData parameter + // userAgent: 'Mozilla/5.0 ...', // Current browser User-Agent }); captchaSolver.solve(proxylessTask) diff --git a/src/tasks.ts b/src/tasks.ts index eeb850e..7851c95 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -340,6 +340,11 @@ export interface TurnstileProxylessParams { * the API does not accept `pageData`. */ pagedata?: string | null; + /** + * User-Agent of the browser that loaded the challenge. Pass it for + * Cloudflare Challenge pages; it is not needed for a standalone widget. + */ + userAgent?: string | null; } /** Parameters of {@link Turnstile}. */ @@ -347,14 +352,13 @@ export interface TurnstileParams extends TurnstileProxylessParams, ProxyParams { /** * Cloudflare Turnstile without a proxy (`TurnstileTaskProxyless`). - * There is no `userAgent` input: the worker picks one and returns it in the solution. * Solution: {@link TurnstileSolution}. * * @see https://captcha-solver.com/en/docs/captcha-types#cloudflare-turnstile */ export class TurnstileProxyless extends BaseTask { - constructor({ websiteURL, websiteKey, action = null, data = null, pagedata = null }: TurnstileProxylessParams) { - super({ type: 'TurnstileTaskProxyless', websiteURL, websiteKey, action, data, pagedata }); + constructor({ websiteURL, websiteKey, action = null, data = null, pagedata = null, userAgent = null }: TurnstileProxylessParams) { + super({ type: 'TurnstileTaskProxyless', websiteURL, websiteKey, action, data, pagedata, userAgent }); } } @@ -365,8 +369,8 @@ export class TurnstileProxyless extends BaseTask { * @see https://captcha-solver.com/en/docs/captcha-types#cloudflare-turnstile */ export class Turnstile extends BaseTask { - constructor({ websiteURL, websiteKey, action = null, data = null, pagedata = null, proxyType, proxyAddress, proxyPort, proxyLogin = null, proxyPassword = null }: TurnstileParams) { - super({ type: 'TurnstileTask', websiteURL, websiteKey, action, data, pagedata, proxyType, proxyAddress, proxyPort, proxyLogin, proxyPassword }); + constructor({ websiteURL, websiteKey, action = null, data = null, pagedata = null, userAgent = null, proxyType, proxyAddress, proxyPort, proxyLogin = null, proxyPassword = null }: TurnstileParams) { + super({ type: 'TurnstileTask', websiteURL, websiteKey, action, data, pagedata, userAgent, proxyType, proxyAddress, proxyPort, proxyLogin, proxyPassword }); } } diff --git a/tests/unit/sync/turnstile.test.js b/tests/unit/sync/turnstile.test.js index 72948ec..9414d5e 100644 --- a/tests/unit/sync/turnstile.test.js +++ b/tests/unit/sync/turnstile.test.js @@ -36,7 +36,7 @@ describe('TurnstileProxyless', () => { expect(result.pageData).toBeUndefined(); }); - test('does not accept userAgent as input -- the API only returns it in the solution', () => { + test('includes the browser userAgent for a Cloudflare Challenge', () => { const task = new Tasks.TurnstileProxyless({ websiteURL: 'https://example.com', websiteKey: 'test_key', @@ -44,7 +44,7 @@ describe('TurnstileProxyless', () => { }); const result = task.toDict(); - expect(result.userAgent).toBeUndefined(); + expect(result.userAgent).toBe('Mozilla/5.0'); }); }); From 7a1857cfc7dc39a203282d35c1024e7dec824d51 Mon Sep 17 00:00:00 2001 From: dariyaanis Date: Tue, 29 Sep 2026 18:08:51 +0300 Subject: [PATCH 2/2] docs: align Turnstile test documentation --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index ced524e..dee11c3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -135,7 +135,7 @@ Common to every type's serialization: | `recaptcha_v2.test.js` | 6 / 1 | the field names `recaptchaDataSValue` and `apiDomain` (not `dataSValue`); `isInvisible`, `userAgent`; proxy variant. `solve()` goes through an intermediate `status: 'processing'` — exactly 3 requests | | `recaptcha_v2_enterprise.test.js` | 4 / 1 | `enterprisePayload` as an object, `isInvisible`; an exact `toDict()` match for the minimal field set | | `recaptcha_v3.test.js` | 4 / 1 | `minScore` is required — without it the constructor throws `ValidationError`; `pageAction`, `apiDomain` | -| `turnstile.test.js` | 5 / 1 | the field names `data` and `pagedata` (not `cData` or `pageData` -- `pagedata` is the one field on this type the real API takes lowercase); `userAgent` is rejected as input entirely -- it's response-only on this type | +| `turnstile.test.js` | 5 / 1 | the field names `data` and `pagedata` (not `cData` or `pageData` -- `pagedata` is the one field on this type the real API takes lowercase); browser `userAgent` support for Cloudflare Challenge pages; proxy variant | | `geetest.test.js` | 7 / 2 | v3: `gt` + `challenge`, no `version` field. v4: `version: 4` + `initParameters`. Plus `geetestApiServerSubdomain` and `risk_type` (never `cookies` -- not a real field on this type). `solve()` is tested separately for v3 and v4 — their solution shapes differ | | `yandex_smartcaptcha.test.js` | 4 / 1 | `userAgent`, `cookies`; proxy variant | | `tencent.test.js` | 4 / 1 | `appId`, optional `captchaScript` |