From ad269e59a98bed5bd98ae6c6310dc69409b782f4 Mon Sep 17 00:00:00 2001 From: Kirill Bushmin Date: Tue, 6 Oct 2026 13:10:38 +0300 Subject: [PATCH] CCL-8819 add a local SDK self-check for README examples and module loading. CCL-8608 add build step in README and CCL-8644 fix the README reCAPTCHA v2 TypeScript example under ESM. Load Node builtins through nodeRequire so import no longer throws ReferenceError: module is not defined when the example creates a client. --- .cursor/skills/sdk-self-verify/SKILL.md | 113 +++ .cursor/skills/sdk-self-verify/reference.md | 54 ++ .../skills/sdk-self-verify/report-template.md | 68 ++ .cursor/skills/sdk-self-verify/scripts/run.sh | 4 + .gitignore | 4 +- README.md | 13 + js-fix-esm-imports.js | 8 +- package.json | 4 +- src/CapMonsterCloudClient.ts | 13 +- src/CapMonsterCloudClientFactory.i.spec.ts | 7 +- src/CapMonsterCloudClientFactory.ts | 8 +- src/HttpClient.ts | 17 +- src/Logger.ts | 8 +- .../AlibabaRequest/AlibabaRequestBase.ts | 2 + src/Requests/AmazonRequest/AmazonRequest.ts | 55 +- .../AmazonRequest/AmazonRequestBase.ts | 72 +- .../DataDomeRequest/DataDomeRequest.ts | 10 +- .../FunCaptchaRequest/FunCaptchaRequest.ts | 12 +- .../GeeTestRequest/GeeTestRequestBase.ts | 6 +- src/Requests/HuntRequest/HuntRequestBase.ts | 4 + src/Requests/ImpervaRequest/ImpervaRequest.ts | 8 +- .../ProsopoRequest/ProsopoRequestBase.ts | 9 +- src/Requests/RequestsSerialization.u.spec.ts | 96 ++- src/Requests/TenDIRequest/TenDIRequestBase.ts | 5 + .../TurnstileRequest/Turnstile.i.spec.ts | 42 + .../TurnstileRequest/TurnstileRequest.ts | 81 +- src/Responses/TurnstileResponse.ts | 10 +- src/nodeRequire.ts | 51 ++ src/nodeRequire.u.spec.ts | 10 + tools/self-verify/docs-catalog.json | 144 ++++ tools/self-verify/lib/mock-api.js | 48 ++ tools/self-verify/reports/.gitkeep | 0 tools/self-verify/reports/latest.json | 749 ++++++++++++++++++ tools/self-verify/reports/latest.md | 128 +++ tools/self-verify/request-fixtures.js | 347 ++++++++ tools/self-verify/run-module-matrix.js | 276 +++++++ tools/self-verify/run-readme-examples.js | 185 +++++ tools/self-verify/run.js | 188 +++++ 38 files changed, 2748 insertions(+), 111 deletions(-) create mode 100644 .cursor/skills/sdk-self-verify/SKILL.md create mode 100644 .cursor/skills/sdk-self-verify/reference.md create mode 100644 .cursor/skills/sdk-self-verify/report-template.md create mode 100755 .cursor/skills/sdk-self-verify/scripts/run.sh create mode 100644 src/nodeRequire.ts create mode 100644 src/nodeRequire.u.spec.ts create mode 100644 tools/self-verify/docs-catalog.json create mode 100644 tools/self-verify/lib/mock-api.js create mode 100644 tools/self-verify/reports/.gitkeep create mode 100644 tools/self-verify/reports/latest.json create mode 100644 tools/self-verify/reports/latest.md create mode 100644 tools/self-verify/request-fixtures.js create mode 100644 tools/self-verify/run-module-matrix.js create mode 100644 tools/self-verify/run-readme-examples.js create mode 100644 tools/self-verify/run.js diff --git a/.cursor/skills/sdk-self-verify/SKILL.md b/.cursor/skills/sdk-self-verify/SKILL.md new file mode 100644 index 0000000..9ae9735 --- /dev/null +++ b/.cursor/skills/sdk-self-verify/SKILL.md @@ -0,0 +1,113 @@ +--- +name: sdk-self-verify +description: >- + Runs the CapMonster Cloud Node.js SDK maintainer self-check before QA: README + examples, CommonJS and ES Modules functionality, API docs comparison, and a + structured report. Use when the user asks to verify the SDK, check README + examples, compare the client with CapMonster API docs, or prepare a change + for QA. +--- + +# SDK self-verify + +Run this before handing JS SDK changes to QA. Combine the mechanical runner with a docs comparison. Do not treat static code reading as a substitute for an actual command. + +## Command + +From the repository root: + +```bash +npm run self-verify +``` + +Options for `node tools/self-verify/run.js`: + +- `--skip-build` if `dist/` is already current +- `--skip-tests` to skip Jest +- `--live` to hit the real API (needs `CMC_CLIENT_KEY` or `CAPMONSTER_API_KEY`) +- `--live-solve` to also run README `Solve()` examples live +- `--out path/to/report.json` + +Mechanical outputs: + +- `tools/self-verify/reports/latest.json` +- `tools/self-verify/reports/latest.md` + +Then write the full report using [report-template.md](report-template.md). Keep the JSON as evidence. + +## Workflow + +Copy and track: + +``` +Self-verify: +- [ ] npm run self-verify +- [ ] README examples: build/run/expected result +- [ ] CommonJS matrix +- [ ] ES Modules matrix +- [ ] Unit + integration tests +- [ ] API docs comparison +- [ ] Final report +``` + +1. Run `npm run self-verify`. If build fails, stop and report that. Do not claim later steps were executed. +2. Read `tools/self-verify/reports/latest.json`. Every check is `executed`, `analysis`, or `skipped`. +3. Confirm README examples: each fenced `ts` / `js` / `javascript` block was written to `tools/self-verify/tmp/readme-examples/` and actually run. Record exit code, stdout/stderr, and expected result. +4. Confirm module matrix: CJS `require(dist/index.js)` and ESM `import(dist/esm/index.js)` both load, construct every request fixture (including Hunt `widgetUrl` and Alibaba `punishUrl` / `cookieRequired`), call `getBalance` / `Solve`, and cover error paths. +5. Compare the SDK with live API docs. Catalog and field notes are in [reference.md](reference.md) and `tools/self-verify/docs-catalog.json`. Fetch current docs; do not rely only on the catalog. +6. Write the report. Separate code analysis from execution. List unverified scenarios and documentation contradictions in their own sections. Failed checks need reproduction commands and a docs URL. + +## Evidence rules + +- Executed: a command ran in this session. Quote the command and the result. +- Analysis: source or docs were read, nothing ran. Say so. +- Skipped: missing API key, `--live-solve` not set, build failed, or the scenario cannot be automated. Say why. + +Never upgrade analysis or skipped into pass. + +## README examples + +Source of truth: `README.md` fenced blocks. + +For each runnable example record: + +- module format (CJS or ESM) +- compile/run command +- expected result (printed `solution`, printed `Balance`, process exit 0) +- actual result +- docs link if the example maps to a task type + +Default run uses a local mock (`serviceUrl` injected). Live `Solve()` is skip/unverified unless `--live-solve` and an API key are present. + +## Module matrix + +Must cover both `dist/index.js` (CommonJS) and `dist/esm/index.js` (ES Modules): + +- named exports used in README +- every request class in `tools/self-verify/request-fixtures.js` +- required vs optional fields that the fixture exercises +- createTask payload (`type`, `class`, metadata) +- getBalance success and `KEY_DOES_NOT_EXIST` +- Solve success and `ZERO_BALANCE` +- invalid `websiteURL` + +If ESM fails with `ReferenceError: module is not defined`, that is a product defect. Reproduce with: + +```bash +node --input-type=module -e "import('./dist/esm/index.js').then(m => m.CapMonsterCloudClientFactory.Create(new m.ClientOptions({ clientKey: 'x' })))" +``` + +## Docs comparison + +Fetch at least: + +- https://docs.capmonster.cloud/docs/captchas/ +- https://docs.capmonster.cloud/docs/methods/ +- Hunt: https://docs.capmonster.cloud/docs/captchas/hunt-task/ +- Alibaba: https://docs.capmonster.cloud/docs/captchas/alibaba-task/ + +For each docs task type, check whether the SDK has a request class, matching required/optional fields, and a response shape. Missing or stale SDK fields are defects. Docs that disagree with each other go under contradictions, not SDK bugs. + +## Report + +Use [report-template.md](report-template.md). Save under `tools/self-verify/reports/` as `YYYY-MM-DD-self-verify.md` plus `latest.md`. diff --git a/.cursor/skills/sdk-self-verify/reference.md b/.cursor/skills/sdk-self-verify/reference.md new file mode 100644 index 0000000..7b4b398 --- /dev/null +++ b/.cursor/skills/sdk-self-verify/reference.md @@ -0,0 +1,54 @@ +# SDK self-verify reference + +Official docs to fetch on every run (do not treat this file as a substitute): + +- Captcha types index: https://docs.capmonster.cloud/docs/captchas/ +- API methods: https://docs.capmonster.cloud/docs/methods/ +- Getting started: https://docs.capmonster.cloud/docs/getting-start/ + +Known SDK class → docs URL map: `tools/self-verify/docs-catalog.json`. + +## Fields that recently lagged the API + +Hunt (`CustomTask` / `class: "HUNT"`), https://docs.capmonster.cloud/docs/captchas/hunt-task/ + +| Field | Required | Notes | +| --- | --- | --- | +| websiteURL | yes | Page URL | +| metadata.apiGetLib | yes | Full `api.js` URL | +| metadata.data | no | `meta.token`. Do not send with `widgetUrl` | +| metadata.widgetUrl | no | Full widget URL. Do not send with `data` | +| userAgent | no | Current Windows UA | +| proxy | yes | Own proxies required | + +Alibaba (`CustomTask` / `class: "alibaba"`), https://docs.capmonster.cloud/docs/captchas/alibaba-task/ + +| Field | Required | Notes | +| --- | --- | --- | +| websiteURL | yes | Page URL | +| metadata.sceneId | for standard flow | Not required for punish-only | +| metadata.prefix | for standard flow | Not required for punish-only | +| metadata.punishUrl | no | Full `/punish` URL with query | +| metadata.cookieRequired | no | Adds `solution.domains` cookies | +| metadata.userId, userUserId, verifyType, region, UserCertifyId, apiGetLib | no | | +| userAgent | no | | +| proxy | no | | + +## Mechanical coverage + +| Check | Command | Kind | +| --- | --- | --- | +| Build | `npm run build` | executed | +| README examples | `npm run test-readme` | executed (mock) or skipped (live Solve) | +| CJS + ESM matrix | `npm run test-modules` | executed | +| Unit tests | `npm run test-unit` | executed | +| Integration tests | `npm run test-integr` | executed | +| Docs field parity | fetch docs + read `src/Requests` | analysis | + +## ESM smoke + +```bash +node --input-type=module -e "import('./dist/esm/index.js').then((m) => { const c = m.CapMonsterCloudClientFactory.Create(new m.ClientOptions({ clientKey: 'x' })); console.log('ok', typeof c.Solve); })" +``` + +A `ReferenceError: module is not defined` here is a ship-blocking CJS leak in the ESM build. diff --git a/.cursor/skills/sdk-self-verify/report-template.md b/.cursor/skills/sdk-self-verify/report-template.md new file mode 100644 index 0000000..f1d129f --- /dev/null +++ b/.cursor/skills/sdk-self-verify/report-template.md @@ -0,0 +1,68 @@ +# SDK self-verify report template + +Fill this after `npm run self-verify`. Keep executed results and analysis in separate sections. + +```markdown +# CapMonster Node.js SDK self-verify + +- Date: +- Git HEAD: +- Node: +- Command: `npm run self-verify` +- Mechanical JSON: `tools/self-verify/reports/latest.json` + +## Verdict + +PASS / FAIL — one paragraph. Do not send to QA on FAIL. + +## Summary + +| Area | Result | Kind | +| --- | --- | --- | +| Build | | executed | +| README examples | | executed / skipped | +| CommonJS | | executed | +| ES Modules | | executed | +| Unit tests | | executed | +| Integration tests | | executed | +| API docs comparison | | analysis | + +## Executed results + +For each failure: + +- Check name +- Expected +- Actual +- Reproduction command +- Docs URL + +## README examples + +| Example | Module | Run | Expected | Actual | Kind | +| --- | --- | --- | --- | --- | --- | +| | CJS/ESM | command | | | executed/skipped | + +## CommonJS and ES Modules + +Cover request types, required/optional fields, requests, responses, errors. Note CJS vs ESM differences. + +## API docs comparison (analysis) + +| Docs task | Docs URL | SDK class | Gap | +| --- | --- | --- | --- | +| | | | missing field / stale field / none | + +## Unverified scenarios + +What was not run and why (no key, no proxy, live Solve disabled, missing sandbox, etc.). + +## Documentation contradictions + +Docs vs docs, or docs vs SDK where it is unclear which is right. Do not file these as SDK bugs without a source-of-truth decision. + +## Attachments + +- latest.json +- failing command output +``` diff --git a/.cursor/skills/sdk-self-verify/scripts/run.sh b/.cursor/skills/sdk-self-verify/scripts/run.sh new file mode 100755 index 0000000..256c615 --- /dev/null +++ b/.cursor/skills/sdk-self-verify/scripts/run.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +cd "$(dirname "$0")/../../../.." +npm run self-verify -- "$@" diff --git a/.gitignore b/.gitignore index 970f14a..a54e330 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ dist/ node_modules/ .idea -.vscode \ No newline at end of file +.vscode + +tools/self-verify/tmp/ diff --git a/README.md b/README.md index 8b15c7e..3b22816 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,19 @@ Install the package from npm: npm install @zennolab_com/capmonstercloud-client ``` +The package published on npm already includes the compiled files and is ready to use after installation. + +### Build from this repository + +Git does not contain the compiled `dist` output. The examples below import `@zennolab_com/capmonstercloud-client`, and inside this repository that name points at `dist`. After cloning, install dependencies and build before running an example or a test: + +```bash +npm install +npm run build +``` + +Run `npm run build` again after changing the source. + --- ## 🚀 Quick Start diff --git a/js-fix-esm-imports.js b/js-fix-esm-imports.js index 9a17509..ddacbec 100644 --- a/js-fix-esm-imports.js +++ b/js-fix-esm-imports.js @@ -30,9 +30,11 @@ function resolveImportPath(filePath, importPath) { function fixImports(filePath) { const source = fs.readFileSync(filePath, 'utf8'); - const fixed = source.replace(/(from\s+['"])([^'"]+)(['"])/g, (_, prefix, importPath, suffix) => { - return `${prefix}${resolveImportPath(filePath, importPath)}${suffix}`; - }); + const fixed = source + .replace(/(from\s+['"])([^'"]+)(['"])/g, (_, prefix, importPath, suffix) => { + return `${prefix}${resolveImportPath(filePath, importPath)}${suffix}`; + }) + .replace(/eval\((['"])import\.meta\.url\1\)/g, 'import.meta.url'); fs.writeFileSync(filePath, fixed); } diff --git a/package.json b/package.json index aab620d..cd616e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zennolab_com/capmonstercloud-client", - "version": "2.9.0", + "version": "2.9.1", "description": "Official JS client library for https://capmonster.cloud/ captcha recognition service", "main": "dist/index.js", "module": "dist/esm/index.js", @@ -31,9 +31,11 @@ ], "scripts": { "build": "node js-rimraf.js && npx tsc && npx tsc -p tsconfig.esm.json && node js-fix-esm-imports.js && node -e \"require('fs').writeFileSync('dist/esm/package.json', JSON.stringify({ type: 'module' }, null, 2))\"", + "prepare": "npm run build", "test": "jest", "test-unit": "jest -c jest.config.unit.js", "test-integr": "jest -c jest.config.integration.js", + "self-verify": "node tools/self-verify/run.js", "lint": "eslint . --ext .ts,.js", "lint:fix": "eslint . --ext .ts,.js --fix" }, diff --git a/src/CapMonsterCloudClient.ts b/src/CapMonsterCloudClient.ts index accb184..65a7c45 100644 --- a/src/CapMonsterCloudClient.ts +++ b/src/CapMonsterCloudClient.ts @@ -175,7 +175,8 @@ export class CapMonsterCloudClient { } } /** - * Solve FunCaptchaTask task +ч>>ч>> * Solve FunCaptchaTask task. + * Own proxy is required. * You will get response within 10 - 80 secs period depending on service workload. */ public async Solve( @@ -248,7 +249,8 @@ export class CapMonsterCloudClient { cancellationController?: AbortController, ): Promise>; /** - * Solve DataDomeRequest task + * Solve DataDomeRequest task. + * Own proxy is required. * You will get response within 10 - 180 secs period depending on service workload. */ public async Solve( @@ -275,7 +277,8 @@ export class CapMonsterCloudClient { cancellationController?: AbortController, ): Promise>; /** - * Solve Imperva task + * Solve Imperva task. + * Own proxy is required. * You will get response within 10 - 180 secs period depending on service workload. */ public async Solve( @@ -383,7 +386,9 @@ export class CapMonsterCloudClient { cancellationController?: AbortController, ): Promise>; /** - * Solve Turnstile task + * Solve Turnstile task. + * Own proxy is required for Cloudflare Challenge (cf_clearance) and Waiting Room. + * Those modes return cf_clearance. Turnstile and Challenge token return token. * You will get response within 10 - 180 secs period depending on service workload. */ public async Solve( diff --git a/src/CapMonsterCloudClientFactory.i.spec.ts b/src/CapMonsterCloudClientFactory.i.spec.ts index 100c8cb..4a64470 100644 --- a/src/CapMonsterCloudClientFactory.i.spec.ts +++ b/src/CapMonsterCloudClientFactory.i.spec.ts @@ -800,13 +800,18 @@ describe('Check integration tests for CapMonsterCloudClientFactory()', () => { incapsulaScriptUrl: '_Incapsula_Resource?SWJIYLWA=719d34d31c8e3a6e6fffd425f7e032f3', incapsulaCookies: 'l/LsGnrvyB9lNhXI8borDKa2IGc', }, + proxy: { + proxyType: 'http', + proxyAddress: '8.8.8.8', + proxyPort: 8080, + }, }); const task = await cmcClient.Solve(impervaRequest); expect(srv.caughtRequests[0]).toHaveProperty( 'body', - '{"clientKey":"","task":{"type":"CustomTask","websiteURL":"https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=middle","metadata":{"incapsulaScriptUrl":"_Incapsula_Resource?SWJIYLWA=719d34d31c8e3a6e6fffd425f7e032f3","incapsulaCookies":"l/LsGnrvyB9lNhXI8borDKa2IGc"},"class":"Imperva"},"softId":54}', + '{"clientKey":"","task":{"type":"CustomTask","websiteURL":"https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=middle","metadata":{"incapsulaScriptUrl":"_Incapsula_Resource?SWJIYLWA=719d34d31c8e3a6e6fffd425f7e032f3","incapsulaCookies":"l/LsGnrvyB9lNhXI8borDKa2IGc"},"class":"Imperva","proxyType":"http","proxyAddress":"8.8.8.8","proxyPort":8080},"softId":54}', ); expect(srv.caughtRequests[1]).toHaveProperty('body', '{"clientKey":"","taskId":1234567}'); expect(task).toHaveProperty('solution'); diff --git a/src/CapMonsterCloudClientFactory.ts b/src/CapMonsterCloudClientFactory.ts index 5f77d80..001177a 100644 --- a/src/CapMonsterCloudClientFactory.ts +++ b/src/CapMonsterCloudClientFactory.ts @@ -3,6 +3,7 @@ import { ClientOptions } from './ClientOptions'; import { ClientURL } from './ClientURL'; import { CsMap, isNode } from './Utils'; import { HttpClient } from './HttpClient'; +import { readOwnPackageVersion } from './nodeRequire'; export class CapMonsterCloudClientFactory { static httpTimeout = 1000 * 21; @@ -30,12 +31,7 @@ export class CapMonsterCloudClientFactory { } static CreateUserAgentString() { - let productVersion = 'ProductVersion'; - if (isNode) { - // require('./package.json') hide require call from browser bundler, e.g. webpack - const packageJSON = module[`require`].call(module, '../package.json'); - productVersion = packageJSON.version; - } + const productVersion = isNode ? readOwnPackageVersion() : 'ProductVersion'; return `${CapMonsterCloudClientFactory.productName}/${productVersion}`; } diff --git a/src/HttpClient.ts b/src/HttpClient.ts index a610f9e..5954b24 100644 --- a/src/HttpClient.ts +++ b/src/HttpClient.ts @@ -4,6 +4,17 @@ import { Agent } from 'https'; import { isNode } from './Utils'; import { debugHttp, debugNet } from './Logger'; import { ClientURL } from './ClientURL'; +import { nodeRequire } from './nodeRequire'; + +type NodeClientRequest = { + on(event: string, listener: (err: Error) => void): NodeClientRequest; + end(body?: string): void; +}; + +type NodeHttpLike = { + Agent: new (options?: { keepAlive?: boolean; timeout?: number }) => Agent; + request(options: object, callback: (res: IncomingMessage) => void): NodeClientRequest; +}; type APIMethod = 'getBalance' | 'createTask' | 'getTaskResult'; @@ -83,8 +94,7 @@ export class HttpClient { private createAgent(): Promise { return new Promise((resolve) => { debugNet('Try create agent instance'); - // require('http') or require('https') hide require call from browser bundler, e.g. webpack - const requester = module[`require`].call(module, this.url.protocol === 'https:' ? 'https' : 'http'); + const requester = nodeRequire(this.url.protocol === 'https:' ? 'https' : 'http'); this._agent = new requester.Agent({ keepAlive: true, timeout: this.timeout }); resolve(); }); @@ -201,8 +211,7 @@ export class HttpClient { debugHttp('Request options', options); debugHttp('Request body', data); if (isNode) { - // require('http') or require('https') hide require call from browser bundler, e.g. webpack - const requester = module[`require`].call(module, this.url.protocol === 'https:' ? 'https' : 'http'); + const requester = nodeRequire(this.url.protocol === 'https:' ? 'https' : 'http'); requester .request(options, (res: IncomingMessage) => { debugHttp('Response headers received', res.statusCode, res.statusMessage); diff --git a/src/Logger.ts b/src/Logger.ts index 7129673..8d61c83 100644 --- a/src/Logger.ts +++ b/src/Logger.ts @@ -1,12 +1,14 @@ +import { isNode } from './Utils'; +import { nodeRequire } from './nodeRequire'; + let createDebugger = (_: string) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (...args: any[]) => ({ [_]: args, }); }; -if (typeof process === 'object' && 'env' in process && process.env.DEBUG) { - // require('debug') hide require call from browser bundler, e.g. webpack - createDebugger = module[`require`].call(module, 'debug'); +if (isNode && typeof process === 'object' && 'env' in process && process.env.DEBUG) { + createDebugger = nodeRequire('debug'); } export const debugNet = createDebugger('cmc-net'); diff --git a/src/Requests/AlibabaRequest/AlibabaRequestBase.ts b/src/Requests/AlibabaRequest/AlibabaRequestBase.ts index 88a6ed9..5ace5fd 100644 --- a/src/Requests/AlibabaRequest/AlibabaRequestBase.ts +++ b/src/Requests/AlibabaRequest/AlibabaRequestBase.ts @@ -9,6 +9,8 @@ export type AlibabaMetadata = { region?: string; UserCertifyId?: string; apiGetLib?: string; + punishUrl?: string; + cookieRequired?: boolean; }; export type AlibabaRequestBaseIn = { diff --git a/src/Requests/AmazonRequest/AmazonRequest.ts b/src/Requests/AmazonRequest/AmazonRequest.ts index 30f9ec0..7b7b734 100644 --- a/src/Requests/AmazonRequest/AmazonRequest.ts +++ b/src/Requests/AmazonRequest/AmazonRequest.ts @@ -1,19 +1,60 @@ import { TaskType } from '../../TaskType'; -import { AmazonRequestBase, AmazonRequestBaseIn } from './AmazonRequestBase'; +import { AmazonCaptchaScriptIn, AmazonChallengeIn, AmazonInvisibleChallengeIn, AmazonRequestBase } from './AmazonRequestBase'; import { ProxyInfo, ProxyInfoIn } from '../ProxyInfo'; -export type AmazonRequestIn = Pick> & { proxy?: ProxyInfoIn }; +export type AmazonRequestIn = ( + | AmazonCaptchaScriptIn + | AmazonChallengeIn + | AmazonInvisibleChallengeIn +) & { proxy?: ProxyInfoIn }; + +function present(value: unknown): value is string { + return typeof value === 'string' && value.length > 0; +} + +function assertAmazonFlow(args: AmazonRequestIn): void { + const websiteKey = 'websiteKey' in args && present(args.websiteKey); + const captchaScript = 'captchaScript' in args && present(args.captchaScript); + const challengeScript = 'challengeScript' in args && present(args.challengeScript); + const context = 'context' in args && typeof args.context === 'string'; + const iv = 'iv' in args && typeof args.iv === 'string'; + + const captchaFlow = websiteKey && captchaScript && !challengeScript && !context && !iv; + const challengeFlow = challengeScript && websiteKey && context && iv; + const invisibleFlow = challengeScript && context && iv && !websiteKey && !captchaScript; + + if (captchaFlow || challengeFlow || invisibleFlow) { + return; + } + + throw new Error( + 'AmazonRequest must use one documented flow: websiteKey + captchaScript, challengeScript + websiteKey + context + iv, or challengeScript + context + iv', + ); +} /** * Amazon recognition request. - * {@link https://zenno.link/doc-amazon-en} + * {@link https://docs.capmonster.cloud/docs/captchas/amazon-task/} */ export class AmazonRequest extends AmazonRequestBase { - constructor({ proxy, ...restArgsObj }: AmazonRequestIn) { - super({ type: TaskType.AmazonTask, ...restArgsObj }); + constructor(args: AmazonRequestIn) { + assertAmazonFlow(args); + + super({ + type: TaskType.AmazonTask, + nocache: args.nocache, + websiteURL: args.websiteURL, + cookieSolution: args.cookieSolution, + userAgent: args.userAgent, + websiteKey: 'websiteKey' in args ? args.websiteKey : undefined, + captchaScript: 'captchaScript' in args ? args.captchaScript : undefined, + challengeScript: 'challengeScript' in args ? args.challengeScript : undefined, + context: 'context' in args ? args.context : undefined, + iv: 'iv' in args ? args.iv : undefined, + }); - if (proxy) { - Object.assign(this, new ProxyInfo(proxy)); + if (args.proxy) { + Object.assign(this, new ProxyInfo(args.proxy)); } } } diff --git a/src/Requests/AmazonRequest/AmazonRequestBase.ts b/src/Requests/AmazonRequest/AmazonRequestBase.ts index 1225344..109976d 100644 --- a/src/Requests/AmazonRequest/AmazonRequestBase.ts +++ b/src/Requests/AmazonRequest/AmazonRequestBase.ts @@ -1,18 +1,58 @@ import { CaptchaRequestBase, CaptchaRequestBaseIn } from '../CaptchaRequestBase'; -export type AmazonRequestBaseIn = { +type AmazonRequestCommonIn = { websiteURL: string; - challengeScript: string; + cookieSolution?: boolean; + userAgent?: string; + nocache?: boolean; +}; + +/** + * Captcha script flow: jsapi.js. https://docs.capmonster.cloud/docs/captchas/amazon-task/ + */ +export type AmazonCaptchaScriptIn = AmazonRequestCommonIn & { + websiteKey: string; captchaScript: string; +}; + +/** + * Challenge flow: challenge.js plus gokuProps key, context, and iv. + * captcha.js is optional when the page only loads a challenge. + */ +export type AmazonChallengeIn = AmazonRequestCommonIn & { + challengeScript: string; websiteKey: string; context: string; iv: string; - cookieSolution?: boolean; - userAgent?: string; -} & CaptchaRequestBaseIn; + captchaScript?: string; +}; + +/** + * Invisible captcha: only challenge.js is loaded. + * context and iv are required and may be empty strings. + */ +export type AmazonInvisibleChallengeIn = AmazonRequestCommonIn & { + challengeScript: string; + context: string; + iv: string; +}; + +export type AmazonRequestBaseIn = + | (AmazonCaptchaScriptIn & CaptchaRequestBaseIn) + | (AmazonChallengeIn & CaptchaRequestBaseIn) + | (AmazonInvisibleChallengeIn & CaptchaRequestBaseIn); + +type AmazonRequestStoredIn = AmazonRequestCommonIn & + CaptchaRequestBaseIn & { + challengeScript?: string; + captchaScript?: string; + websiteKey?: string; + context?: string; + iv?: string; + }; /** - * Base GeeTest recognition request + * Base Amazon recognition request. */ export abstract class AmazonRequestBase extends CaptchaRequestBase { /** @@ -23,27 +63,27 @@ export abstract class AmazonRequestBase extends CaptchaRequestBase { /** * Link to challenge.js */ - public challengeScript!: string; + public challengeScript?: string; /** - * Link to captcha.js + * Link to captcha.js or jsapi.js */ - public captchaScript!: string; + public captchaScript?: string; /** - * A string that can be retrieved from an html page with a captcha or with javascript by executing the window.gokuProps.key + * Captcha apiKey, or window.gokuProps.key for the challenge flow. */ - public websiteKey!: string; + public websiteKey?: string; /** - * A string that can be retrieved from an html page with a captcha or with javascript by executing the window.gokuProps.context + * window.gokuProps.context. An empty string is valid for an invisible captcha. */ - public context!: string; + public context?: string; /** - * A string that can be retrieved from an html page with a captcha or with javascript by executing the window.gokuProps.iv + * window.gokuProps.iv. An empty string is valid for an invisible captcha. */ - public iv!: string; + public iv?: string; /** * By default false. If you need to use cookies "aws-waf-token", specify the value true. Otherwise, what you will get in return is "captcha_voucher" and "existing_token". @@ -66,7 +106,7 @@ export abstract class AmazonRequestBase extends CaptchaRequestBase { iv, cookieSolution, userAgent, - }: AmazonRequestBaseIn) { + }: AmazonRequestStoredIn) { super({ type, nocache }); this.websiteURL = this.validateWebsiteURL(websiteURL); this.challengeScript = challengeScript; diff --git a/src/Requests/DataDomeRequest/DataDomeRequest.ts b/src/Requests/DataDomeRequest/DataDomeRequest.ts index 382b7ee..447ac0f 100644 --- a/src/Requests/DataDomeRequest/DataDomeRequest.ts +++ b/src/Requests/DataDomeRequest/DataDomeRequest.ts @@ -3,20 +3,18 @@ import { DataDomeRequestBase, DataDomeRequestBaseIn } from './DataDomeRequestBas import { ProxyInfo, ProxyInfoIn } from '../ProxyInfo'; export type DataDomeRequestIn = Pick> & { - proxy?: ProxyInfoIn; + proxy: ProxyInfoIn; }; /** * DataDome recognition request. - * {@link https://zenno.link/doc-datadome} + * Own proxy is required. + * {@link https://docs.capmonster.cloud/docs/captchas/datadome/} */ export class DataDomeRequest extends DataDomeRequestBase { public declare class: 'DataDome'; constructor({ proxy, ...argsObj }: DataDomeRequestIn) { super({ type: TaskType.CustomTask, _class: 'DataDome', ...argsObj }); - - if (proxy) { - Object.assign(this, new ProxyInfo(proxy)); - } + Object.assign(this, new ProxyInfo(proxy)); } } diff --git a/src/Requests/FunCaptchaRequest/FunCaptchaRequest.ts b/src/Requests/FunCaptchaRequest/FunCaptchaRequest.ts index aa9ffff..a631734 100644 --- a/src/Requests/FunCaptchaRequest/FunCaptchaRequest.ts +++ b/src/Requests/FunCaptchaRequest/FunCaptchaRequest.ts @@ -2,18 +2,18 @@ import { TaskType } from '../../TaskType'; import { FunCaptchaRequestBase, FunCaptchaRequestBaseIn } from './FunCaptchaRequestBase'; import { ProxyInfo, ProxyInfoIn } from '../ProxyInfo'; -export type FunCaptchaRequestIn = Pick> & { proxy?: ProxyInfoIn }; +export type FunCaptchaRequestIn = Pick> & { + proxy: ProxyInfoIn; +}; /** * FunCaptcha recognition request. - * {@link https://zennolab.atlassian.net/wiki/spaces/APIS/pages/735805497/FunCaptchaTask+solving+FunCaptcha} + * Own proxy is required. + * {@link https://docs.capmonster.cloud/docs/captchas/funcaptcha-task/} */ export class FunCaptchaRequest extends FunCaptchaRequestBase { constructor({ proxy, ...restArgs }: FunCaptchaRequestIn) { super({ type: TaskType.FunCaptchaTask, ...restArgs }); - - if (proxy) { - Object.assign(this, new ProxyInfo(proxy)); - } + Object.assign(this, new ProxyInfo(proxy)); } } diff --git a/src/Requests/GeeTestRequest/GeeTestRequestBase.ts b/src/Requests/GeeTestRequest/GeeTestRequestBase.ts index f259404..7303d39 100644 --- a/src/Requests/GeeTestRequest/GeeTestRequestBase.ts +++ b/src/Requests/GeeTestRequest/GeeTestRequestBase.ts @@ -9,7 +9,7 @@ export type GeeTestRequestBaseIn = { geetestApiServerSubdomain?: string; geetestGetLib?: string; userAgent?: string; - version?: '3' | '4'; + version: 3 | 4; initParameters?: InitParamsType; } & CaptchaRequestBaseIn; @@ -46,9 +46,9 @@ export abstract class GeeTestRequestBase extends CaptchaRequestBase { public challenge?: string; /** - * Version of Geetest. + * GeeTest version. Send 3 or 4 as a number. */ - public version?: '3' | '4'; + public version!: 3 | 4; /** * May be required for some sites. diff --git a/src/Requests/HuntRequest/HuntRequestBase.ts b/src/Requests/HuntRequest/HuntRequestBase.ts index a7bfc0a..b2ad7ff 100644 --- a/src/Requests/HuntRequest/HuntRequestBase.ts +++ b/src/Requests/HuntRequest/HuntRequestBase.ts @@ -11,6 +11,10 @@ export type HuntMetadata = { * The `data` parameter (required only in some solving modes). */ data?: string; + /** + * Full widget URL. Do not send together with `data`. + */ + widgetUrl?: string; }; export type HuntRequestBaseIn = { diff --git a/src/Requests/ImpervaRequest/ImpervaRequest.ts b/src/Requests/ImpervaRequest/ImpervaRequest.ts index 5213540..2fed49a 100644 --- a/src/Requests/ImpervaRequest/ImpervaRequest.ts +++ b/src/Requests/ImpervaRequest/ImpervaRequest.ts @@ -3,11 +3,12 @@ import { ImpervaRequestBase, ImpervaRequestBaseIn } from './ImpervaRequestBase'; import { ProxyInfo, ProxyInfoIn } from '../ProxyInfo'; export type ImpervaRequestIn = Pick> & { - proxy?: ProxyInfoIn; + proxy: ProxyInfoIn; }; /** * Imperva (Incapsula) recognition request. + * Own proxy is required. * {@link https://docs.capmonster.cloud/docs/captchas/incapsula/} */ export class ImpervaRequest extends ImpervaRequestBase { @@ -15,9 +16,6 @@ export class ImpervaRequest extends ImpervaRequestBase { constructor({ proxy, ...argsObj }: ImpervaRequestIn) { super({ type: TaskType.CustomTask, _class: 'Imperva', ...argsObj }); - - if (proxy) { - Object.assign(this, new ProxyInfo(proxy)); - } + Object.assign(this, new ProxyInfo(proxy)); } } diff --git a/src/Requests/ProsopoRequest/ProsopoRequestBase.ts b/src/Requests/ProsopoRequest/ProsopoRequestBase.ts index 4b9a34f..724f86f 100644 --- a/src/Requests/ProsopoRequest/ProsopoRequestBase.ts +++ b/src/Requests/ProsopoRequest/ProsopoRequestBase.ts @@ -3,6 +3,7 @@ import { CaptchaRequestBase, CaptchaRequestBaseIn } from '../CaptchaRequestBase' export type ProsopoRequestBaseIn = { websiteURL: string; websiteKey: string; + userAgent?: string; } & CaptchaRequestBaseIn; /** @@ -19,9 +20,15 @@ export abstract class ProsopoRequestBase extends CaptchaRequestBase { */ public websiteKey!: string; - constructor({ type, nocache, websiteURL, websiteKey }: ProsopoRequestBaseIn) { + /** + * Browser User-Agent. Pass only the actual UA from Windows OS. + */ + public userAgent?: string; + + constructor({ type, nocache, websiteURL, websiteKey, userAgent }: ProsopoRequestBaseIn) { super({ type, nocache }); this.websiteURL = this.validateWebsiteURL(websiteURL); this.websiteKey = websiteKey; + this.userAgent = userAgent; } } diff --git a/src/Requests/RequestsSerialization.u.spec.ts b/src/Requests/RequestsSerialization.u.spec.ts index 2084854..b3bd745 100644 --- a/src/Requests/RequestsSerialization.u.spec.ts +++ b/src/Requests/RequestsSerialization.u.spec.ts @@ -171,15 +171,17 @@ describe('Check unit tests for SerializeObject()', () => { }); }); - it(`should serialize FunCaptchaProxylessRequest`, () => { + it(`should serialize FunCaptchaRequest without optional fields`, () => { const serialized = SerializeObject({ clientKey: '', task: new FunCaptchaRequest({ websiteURL: 'https://funcaptcha.com/fc/api/nojs/?pkey=69A21A01-CC7B-B9C6-0F9A-E7FA06677FFC', websitePublicKey: '69A21A01-CC7B-B9C6-0F9A-E7FA06677FFC', - data: '{"blob":"dyXvXANMbHj1iDyz.Qj97JtSqR2n%2BuoY1V%2FbdgbrG7p%2FmKiqdU9AwJ6MifEt0np4vfYn6TTJDJEfZDlcz9Q1XMn9przeOV%2FCr2%2FIpi%2FC1s%3D"}', - funcaptchaApiJSSubdomain: 'mywebsite-api.funcaptcha.com', - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + proxy: { + proxyType: 'http', + proxyAddress: '8.8.8.8', + proxyPort: 8080, + }, }), }); @@ -189,9 +191,9 @@ describe('Check unit tests for SerializeObject()', () => { type: 'FunCaptchaTask', websiteURL: 'https://funcaptcha.com/fc/api/nojs/?pkey=69A21A01-CC7B-B9C6-0F9A-E7FA06677FFC', websitePublicKey: '69A21A01-CC7B-B9C6-0F9A-E7FA06677FFC', - data: '{"blob":"dyXvXANMbHj1iDyz.Qj97JtSqR2n%2BuoY1V%2FbdgbrG7p%2FmKiqdU9AwJ6MifEt0np4vfYn6TTJDJEfZDlcz9Q1XMn9przeOV%2FCr2%2FIpi%2FC1s%3D"}', - funcaptchaApiJSSubdomain: 'mywebsite-api.funcaptcha.com', - userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + proxyType: 'http', + proxyAddress: '8.8.8.8', + proxyPort: 8080, }, }); }); @@ -300,6 +302,7 @@ describe('Check unit tests for SerializeObject()', () => { websiteURL: 'https://example.com/geetest.php', gt: '81dc9bdb52d04dc20036dbd8313ed055', challenge: 'd93591bdf7860e1e4ee2fca799911215', + version: 3, }), }); @@ -321,7 +324,7 @@ describe('Check unit tests for SerializeObject()', () => { websiteURL: 'https://example.com/geetest.php', gt: '81dc9bdb52d04dc20036dbd8313ed055', challenge: 'd93591bdf7860e1e4ee2fca799911215', - version: '4', + version: 4, initParameters: { riskType: 'slide', }, @@ -334,7 +337,7 @@ describe('Check unit tests for SerializeObject()', () => { type: 'GeeTestTask', websiteURL: 'https://example.com/geetest.php', gt: '81dc9bdb52d04dc20036dbd8313ed055', - version: '4', + version: 4, initParameters: { riskType: 'slide', }, @@ -349,6 +352,7 @@ describe('Check unit tests for SerializeObject()', () => { websiteURL: 'https://example.com/geetest.php', gt: '81dc9bdb52d04dc20036dbd8313ed055', challenge: 'd93591bdf7860e1e4ee2fca799911215', + version: 3, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36', proxy: { proxyType: 'https', @@ -383,7 +387,7 @@ describe('Check unit tests for SerializeObject()', () => { task: new GeeTestRequest({ websiteURL: 'https://example.com/geetest.php', gt: '81dc9bdb52d04dc20036dbd8313ed055', - version: '4', + version: 4, initParameters: { riskType: 'slide', }, @@ -403,7 +407,7 @@ describe('Check unit tests for SerializeObject()', () => { task: { type: 'GeeTestTask', websiteURL: 'https://example.com/geetest.php', - version: '4', + version: 4, gt: '81dc9bdb52d04dc20036dbd8313ed055', initParameters: { riskType: 'slide', @@ -612,6 +616,11 @@ describe('Check unit tests for SerializeObject()', () => { datadomeCookie: '', datadomeVersion: 'new', }, + proxy: { + proxyType: 'http', + proxyAddress: '8.8.8.8', + proxyPort: 8080, + }, }), }); @@ -627,6 +636,9 @@ describe('Check unit tests for SerializeObject()', () => { datadomeCookie: '', datadomeVersion: 'new', }, + proxyType: 'http', + proxyAddress: '8.8.8.8', + proxyPort: 8080, }, }); }); @@ -682,6 +694,11 @@ describe('Check unit tests for SerializeObject()', () => { incapsulaCookies: 'incap_ses_1166_2930313=br7iX33ZNCtf3HlpEXcuEDzz72cAAAAA0suDnBGrq/iA0J4oERYzjQ==', reese84UrlEndpoint: 'Built-with-the-For-hopence-Hurleysurfecting-the-', }, + proxy: { + proxyType: 'http', + proxyAddress: '8.8.8.8', + proxyPort: 8080, + }, }), }); @@ -697,6 +714,9 @@ describe('Check unit tests for SerializeObject()', () => { incapsulaCookies: 'incap_ses_1166_2930313=br7iX33ZNCtf3HlpEXcuEDzz72cAAAAA0suDnBGrq/iA0J4oERYzjQ==', reese84UrlEndpoint: 'Built-with-the-For-hopence-Hurleysurfecting-the-', }, + proxyType: 'http', + proxyAddress: '8.8.8.8', + proxyPort: 8080, }, }); }); @@ -828,6 +848,7 @@ describe('Check unit tests for SerializeObject()', () => { websiteKey: '6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd', metadata: { captchaUrl: 'https://example.com/captcha', + aidEncrypted: 'HHuVfL451oAYXSwVa87xnU4QHER9ufsPpaSomCugX5Il', }, }), }); @@ -842,6 +863,7 @@ describe('Check unit tests for SerializeObject()', () => { websiteKey: '6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd', metadata: { captchaUrl: 'https://example.com/captcha', + aidEncrypted: 'HHuVfL451oAYXSwVa87xnU4QHER9ufsPpaSomCugX5Il', }, }, }); @@ -916,6 +938,58 @@ describe('Check unit tests for SerializeObject()', () => { }); }); + it('should serialize Amazon captcha script flow', () => { + const serialized = SerializeObject({ + clientKey: '', + task: new AmazonRequest({ + websiteURL: 'https://site.com', + websiteKey: 'h15hX7brbaRTRZa1_1', + captchaScript: 'https://example.com/jsapi.js', + }), + }); + + expect(JSON.parse(JSON.stringify(serialized.task))).toEqual({ + type: 'AmazonTask', + websiteURL: 'https://site.com', + websiteKey: 'h15hX7brbaRTRZa1_1', + captchaScript: 'https://example.com/jsapi.js', + }); + }); + + it('should serialize Amazon invisible challenge flow', () => { + const serialized = SerializeObject({ + clientKey: '', + task: new AmazonRequest({ + websiteURL: 'https://site.com', + challengeScript: 'https://example.com/challenge.js', + context: '', + iv: '', + }), + }); + + expect(serialized).toMatchObject({ + task: { + type: 'AmazonTask', + websiteURL: 'https://site.com', + challengeScript: 'https://example.com/challenge.js', + context: '', + iv: '', + }, + }); + }); + + it('should reject an Amazon request that mixes captcha script and challenge fields', () => { + expect( + () => + new AmazonRequest({ + websiteURL: 'https://site.com', + websiteKey: 'key', + captchaScript: 'https://example.com/jsapi.js', + challengeScript: 'https://example.com/challenge.js', + } as never), + ).toThrow(/one documented flow/); + }); + it(`should serialize AlibabaRequest`, () => { const serialized = SerializeObject({ clientKey: '', diff --git a/src/Requests/TenDIRequest/TenDIRequestBase.ts b/src/Requests/TenDIRequest/TenDIRequestBase.ts index 0893f23..f330d0b 100644 --- a/src/Requests/TenDIRequest/TenDIRequestBase.ts +++ b/src/Requests/TenDIRequest/TenDIRequestBase.ts @@ -2,6 +2,11 @@ import { CaptchaRequestBase, CaptchaRequestBaseIn } from '../CaptchaRequestBase' export type TenDIMetadata = { captchaUrl?: string; + /** + * Single-use Aid value from the target site. + * Omit when the site does not return Aid. Obtain a new value for every task. + */ + aidEncrypted?: string; }; export type TenDIRequestBaseIn = { diff --git a/src/Requests/TurnstileRequest/Turnstile.i.spec.ts b/src/Requests/TurnstileRequest/Turnstile.i.spec.ts index 2d30b5f..2bdd29f 100644 --- a/src/Requests/TurnstileRequest/Turnstile.i.spec.ts +++ b/src/Requests/TurnstileRequest/Turnstile.i.spec.ts @@ -81,4 +81,46 @@ describe('Check integration tests for TurnstileTask', () => { expect(await srv.destroy()).toBeUndefined(); }); + + it('should send Turnstile waiting room', async () => { + expect.assertions(5); + + const srv = await createServerMock({ + responses: [ + { responseBody: '{"errorId":0,"taskId":7654323}' }, + { responseBody: '{"errorId":0,"status":"ready","solution":{"cf_clearance":"clearance"}}' }, + ], + }); + + const cmcClient = CapMonsterCloudClientFactory.Create( + new ClientOptions({ clientKey: '', serviceUrl: `http://localhost:${srv.address.port}` }), + ); + + const request = new TurnstileRequest({ + websiteURL: 'https://example.com/', + websiteKey: '0x4AAAAAAABUY0VLtOUMAHxE', + cloudflareTaskType: 'wait_room', + htmlPageBase64: 'htmlPageBase64Here', + userAgent: 'userAgentHere', + proxy: { + proxyType: 'http', + proxyAddress: '8.8.8.8', + proxyPort: 8080, + proxyLogin: 'proxyLoginHere', + proxyPassword: 'proxyPasswordHere', + }, + }); + + const task = await cmcClient.Solve(request); + + expect(srv.caughtRequests[0]).toHaveProperty( + 'body', + '{"clientKey":"","task":{"type":"TurnstileTask","websiteURL":"https://example.com/","websiteKey":"0x4AAAAAAABUY0VLtOUMAHxE","cloudflareTaskType":"wait_room","userAgent":"userAgentHere","htmlPageBase64":"htmlPageBase64Here","proxyType":"http","proxyAddress":"8.8.8.8","proxyPort":8080,"proxyLogin":"proxyLoginHere","proxyPassword":"proxyPasswordHere"},"softId":54}', + ); + expect(srv.caughtRequests[1]).toHaveProperty('body', '{"clientKey":"","taskId":7654323}'); + expect(task).toHaveProperty('solution'); + expect(task).toHaveProperty('solution.cf_clearance', 'clearance'); + + expect(await srv.destroy()).toBeUndefined(); + }); }); diff --git a/src/Requests/TurnstileRequest/TurnstileRequest.ts b/src/Requests/TurnstileRequest/TurnstileRequest.ts index cd720f8..4c68fe9 100644 --- a/src/Requests/TurnstileRequest/TurnstileRequest.ts +++ b/src/Requests/TurnstileRequest/TurnstileRequest.ts @@ -2,31 +2,46 @@ import { TaskType } from '../../TaskType'; import { TurnstileRequestBase, TurnstileRequestBaseIn } from './TurnstileRequestBase'; import { ProxyInfo, ProxyInfoIn } from '../ProxyInfo'; -interface TurnstileTokenType extends Pick> { - cloudflareTaskType?: 'token'; - userAgent: string; - pageAction: string; - data: string; - pageData: string; -} +type TurnstileFields = Pick>; -export type TurnstileCfClearanceType = Pick> & { - cloudflareTaskType?: 'cf_clearance'; - userAgent?: string; - pageAction?: string; - htmlPageBase64?: string; - data?: string; - pageData?: string; - apiJsUrl?: string; -}; - -export type TurnstileRequestIn = (TurnstileTokenType | TurnstileCfClearanceType) & { - proxy?: ProxyInfoIn; -}; +export type TurnstileRequestIn = + | (TurnstileFields & { + cloudflareTaskType?: undefined; + userAgent?: string; + pageAction?: string; + data?: string; + proxy?: ProxyInfoIn; + }) + | (TurnstileFields & { + cloudflareTaskType: 'token'; + userAgent: string; + pageAction: string; + data: string; + pageData: string; + apiJsUrl?: string; + proxy?: ProxyInfoIn; + }) + | (TurnstileFields & { + cloudflareTaskType: 'cf_clearance'; + htmlPageBase64: string; + userAgent: string; + data?: string; + pageData?: string; + pageAction?: string; + apiJsUrl?: string; + proxy: ProxyInfoIn; + }) + | (TurnstileFields & { + cloudflareTaskType: 'wait_room'; + htmlPageBase64: string; + userAgent: string; + proxy: ProxyInfoIn; + }); /** - * TurnstileTask / Cloudflare Challenge (with proxy for cf-clearance). - * {@link https://zennolab.atlassian.net/wiki/spaces/APIS/pages/2313814017/TurnstileTask+Cloudflare+Challenge} + * TurnstileTask, Cloudflare Challenge, and Cloudflare Waiting Room. + * Own proxy is required for cf_clearance and wait_room. + * {@link https://docs.capmonster.cloud/docs/captchas/turnstile-task/} */ export class TurnstileRequest extends TurnstileRequestBase { cloudflareTaskType?: 'token' | 'cf_clearance' | 'wait_room'; @@ -42,15 +57,21 @@ export class TurnstileRequest extends TurnstileRequestBase { this.userAgent = argsObj.userAgent; if (argsObj.cloudflareTaskType === 'cf_clearance') { - this.htmlPageBase64 = argsObj?.htmlPageBase64; - this.data = argsObj?.data; - this.pageData = argsObj?.pageData; - this.apiJsUrl = argsObj?.apiJsUrl; - } - if (argsObj.cloudflareTaskType === 'token') { - this.data = argsObj?.data; - this.pageData = argsObj?.pageData; + this.htmlPageBase64 = argsObj.htmlPageBase64; + this.data = argsObj.data; + this.pageData = argsObj.pageData; + this.apiJsUrl = argsObj.apiJsUrl; + this.pageAction = argsObj.pageAction; + } else if (argsObj.cloudflareTaskType === 'token') { + this.pageAction = argsObj.pageAction; + this.data = argsObj.data; + this.pageData = argsObj.pageData; + this.apiJsUrl = argsObj.apiJsUrl; + } else if (argsObj.cloudflareTaskType === 'wait_room') { + this.htmlPageBase64 = argsObj.htmlPageBase64; + } else { this.pageAction = argsObj.pageAction; + this.data = argsObj.data; } if (argsObj.proxy) { diff --git a/src/Responses/TurnstileResponse.ts b/src/Responses/TurnstileResponse.ts index f9fcfce..bc9d6fd 100644 --- a/src/Responses/TurnstileResponse.ts +++ b/src/Responses/TurnstileResponse.ts @@ -3,8 +3,12 @@ */ export type TurnstileResponse = { /** - * Captcha answer + * Token for Turnstile and Cloudflare Challenge (token). */ - token: string; - userAgent: string; + token?: string; + /** + * Cookie for Cloudflare Challenge (cf_clearance) and Waiting Room. + */ + cf_clearance?: string; + userAgent?: string; }; diff --git a/src/nodeRequire.ts b/src/nodeRequire.ts new file mode 100644 index 0000000..c6ce072 --- /dev/null +++ b/src/nodeRequire.ts @@ -0,0 +1,51 @@ +import { createRequire } from 'module'; + +type RequireFn = (id: string) => unknown; + +const PACKAGE_NAME = '@zennolab_com/capmonstercloud-client'; + +function getModuleFilename(): string { + if (typeof __filename !== 'undefined') { + return __filename; + } + + // Dual CJS/ESM source: tsc cannot emit import.meta in the CommonJS build. + // js-fix-esm-imports.js rewrites this eval() to import.meta.url in dist/esm. + // eslint-disable-next-line no-eval + return eval('import.meta.url') as string; +} + +function getRuntimeRequire(): RequireFn { + if (typeof require === 'function') { + return require; + } + + return createRequire(getModuleFilename()); +} + +/** + * Loads a Node.js module in both CommonJS and ESM builds. + */ +export function nodeRequire(id: string): T { + return getRuntimeRequire()(id) as T; +} + +/** + * Reads this package version from package.json for both dist/ and dist/esm/ layouts. + */ +export function readOwnPackageVersion(): string { + const candidates = ['../package.json', '../../package.json']; + + for (const candidate of candidates) { + try { + const pkg = nodeRequire<{ name?: string; version?: string }>(candidate); + if (pkg && pkg.name === PACKAGE_NAME && typeof pkg.version === 'string') { + return pkg.version; + } + } catch { + // CJS emits to dist/, ESM emits to dist/esm/. + } + } + + return 'ProductVersion'; +} diff --git a/src/nodeRequire.u.spec.ts b/src/nodeRequire.u.spec.ts new file mode 100644 index 0000000..1e6601d --- /dev/null +++ b/src/nodeRequire.u.spec.ts @@ -0,0 +1,10 @@ +import { readOwnPackageVersion } from './nodeRequire'; + +const { version, name } = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires + +describe('nodeRequire helpers', () => { + it('reads this package version from package.json', () => { + expect(name).toBe('@zennolab_com/capmonstercloud-client'); + expect(readOwnPackageVersion()).toBe(version); + }); +}); diff --git a/tools/self-verify/docs-catalog.json b/tools/self-verify/docs-catalog.json new file mode 100644 index 0000000..611f4e7 --- /dev/null +++ b/tools/self-verify/docs-catalog.json @@ -0,0 +1,144 @@ +{ + "apiDocsIndex": "https://docs.capmonster.cloud/docs/captchas/", + "apiMethods": "https://docs.capmonster.cloud/docs/methods/", + "gettingStarted": "https://docs.capmonster.cloud/docs/getting-start/", + "tasks": [ + { + "sdkClass": "RecaptchaV2Request", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v2/" + }, + { + "sdkClass": "RecaptchaV2EnterpriseRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v2-enterprise/" + }, + { + "sdkClass": "RecaptchaV3ProxylessRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v3/" + }, + { + "sdkClass": "RecaptchaV3EnterpriseRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v3-enterprise/" + }, + { + "sdkClass": "TurnstileRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/turnstile-task/" + }, + { + "sdkClass": "HCaptchaRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/hcaptcha/" + }, + { + "sdkClass": "FunCaptchaRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/funcaptcha/" + }, + { + "sdkClass": "GeeTestRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/geetest/" + }, + { + "sdkClass": "ImageToTextRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/image-to-text/" + }, + { + "sdkClass": "AmazonRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/amazon-task/" + }, + { + "sdkClass": "BinanceRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/binance/" + }, + { + "sdkClass": "ProsopoRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/prosopo/" + }, + { + "sdkClass": "YidunRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/yidun/" + }, + { + "sdkClass": "MTCaptchaRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/mtcaptcha/" + }, + { + "sdkClass": "DataDomeRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/datadome/" + }, + { + "sdkClass": "ImpervaRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/incapsula/" + }, + { + "sdkClass": "TenDIRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/tendi/" + }, + { + "sdkClass": "BasiliskRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/Basilisk-task/" + }, + { + "sdkClass": "TemuRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/temu/" + }, + { + "sdkClass": "CastleRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/castle/" + }, + { + "sdkClass": "AltchaRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/altcha-task/" + }, + { + "sdkClass": "FriendlyRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/friendly-captcha/" + }, + { + "sdkClass": "TSPDRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/tspd/" + }, + { + "sdkClass": "HuntRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/hunt-task/", + "requiredFields": ["websiteURL", "metadata.apiGetLib", "proxy"], + "optionalFields": ["userAgent", "metadata.data", "metadata.widgetUrl"] + }, + { + "sdkClass": "AlibabaRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/alibaba-task/", + "requiredFields": ["websiteURL"], + "optionalFields": [ + "userAgent", + "proxy", + "metadata.sceneId", + "metadata.prefix", + "metadata.userId", + "metadata.userUserId", + "metadata.verifyType", + "metadata.region", + "metadata.UserCertifyId", + "metadata.apiGetLib", + "metadata.punishUrl", + "metadata.cookieRequired" + ] + }, + { + "sdkClass": "ComplexImageRecaptchaRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image/" + }, + { + "sdkClass": "ComplexImageHCaptchaRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image/" + }, + { + "sdkClass": "ComplexImageFunCaptchaRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image/" + }, + { + "sdkClass": "ComplexImageTaskRecognitionRequest", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image-recognition/" + }, + { + "sdkClass": "CommonCaptcha", + "docs": "https://docs.capmonster.cloud/docs/methods/" + } + ] +} diff --git a/tools/self-verify/lib/mock-api.js b/tools/self-verify/lib/mock-api.js new file mode 100644 index 0000000..97b83d0 --- /dev/null +++ b/tools/self-verify/lib/mock-api.js @@ -0,0 +1,48 @@ +const http = require('http'); + +const DEFAULTS = { + '/getBalance': '{"errorId":0,"balance":1.23}', + '/createTask': '{"errorId":0,"taskId":1}', + '/getTaskResult': '{"errorId":0,"status":"ready","solution":{"gRecaptchaResponse":"mock-token"}}', +}; + +function createMockApi(responses) { + const caughtRequests = []; + const queue = Array.isArray(responses) ? responses.slice() : []; + + const server = http.createServer((req, res) => { + const chunks = []; + req.on('data', (chunk) => chunks.push(chunk)); + req.on('end', () => { + const body = Buffer.concat(chunks).toString('utf8'); + const route = (req.url || '').split('?')[0]; + caughtRequests.push({ method: req.method, url: req.url, body, userAgent: req.headers['user-agent'] }); + + const queued = queue.length ? queue.shift() : null; + const responseBody = (queued && queued.responseBody) || DEFAULTS[route] || '{"errorId":0}'; + const statusCode = (queued && queued.statusCode) || 200; + const contentType = (queued && queued.contentType) || 'application/json'; + + res.writeHead(statusCode, { 'Content-Type': contentType }); + res.end(responseBody); + }); + }); + + return new Promise((resolve, reject) => { + server.on('error', reject); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + resolve({ + url: `http://127.0.0.1:${address.port}`, + caughtRequests, + async close() { + await new Promise((closeResolve, closeReject) => { + server.close((err) => (err ? closeReject(err) : closeResolve())); + }); + }, + }); + }); + }); +} + +module.exports = { createMockApi }; diff --git a/tools/self-verify/reports/.gitkeep b/tools/self-verify/reports/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tools/self-verify/reports/latest.json b/tools/self-verify/reports/latest.json new file mode 100644 index 0000000..9907f6d --- /dev/null +++ b/tools/self-verify/reports/latest.json @@ -0,0 +1,749 @@ +{ + "generatedAt": "2026-10-06T10:09:02.695Z", + "kind": "mechanical-run", + "status": "pass", + "build": { + "name": "npm run build", + "status": "pass", + "kind": "executed", + "exitCode": 0, + "stdout": "> @zennolab_com/capmonstercloud-client@2.9.1 build\n> node js-rimraf.js && npx tsc && npx tsc -p tsconfig.esm.json && node js-fix-esm-imports.js && node -e \"require('fs').writeFileSync('dist/esm/package.json', JSON.stringify({ type: 'module' }, null, 2))\"", + "stderr": "", + "reproduction": "npm run build" + }, + "readme": { + "status": "pass", + "live": false, + "checks": [ + { + "name": "readme-example-1", + "status": "pass", + "kind": "executed", + "module": "esm", + "language": "ts", + "file": "tools/self-verify/tmp/readme-examples/readme-example-1.mjs", + "expected": "Process exits 0 against the local mock API and prints a solution or balance.", + "exitCode": 0, + "stdout": "{ gRecaptchaResponse: 'mock-token' }", + "stderr": "", + "reproduction": "node tools/self-verify/tmp/readme-examples/readme-example-1.mjs" + }, + { + "name": "readme-example-2", + "status": "pass", + "kind": "executed", + "module": "esm", + "language": "ts", + "file": "tools/self-verify/tmp/readme-examples/readme-example-2.mjs", + "expected": "Process exits 0 against the local mock API and prints a solution or balance.", + "exitCode": 0, + "stdout": "{ gRecaptchaResponse: 'mock-token' }", + "stderr": "", + "reproduction": "node tools/self-verify/tmp/readme-examples/readme-example-2.mjs" + }, + { + "name": "readme-example-3", + "status": "pass", + "kind": "executed", + "module": "cjs", + "language": "javascript", + "file": "tools/self-verify/tmp/readme-examples/readme-example-3.cjs", + "expected": "Process exits 0 against the local mock API and prints a solution or balance.", + "exitCode": 0, + "stdout": "Balance: { errorId: 0, balance: 1.23 }\n{ gRecaptchaResponse: 'mock-token' }", + "stderr": "", + "reproduction": "node tools/self-verify/tmp/readme-examples/readme-example-3.cjs" + }, + { + "name": "readme-example-4", + "status": "pass", + "kind": "executed", + "module": "cjs", + "language": "javascript", + "file": "tools/self-verify/tmp/readme-examples/readme-example-4.cjs", + "expected": "Process exits 0 against the local mock API and prints a solution or balance.", + "exitCode": 0, + "stdout": "CaptchaResult {\n error: undefined,\n solution: { gRecaptchaResponse: 'mock-token' }\n}", + "stderr": "", + "reproduction": "node tools/self-verify/tmp/readme-examples/readme-example-4.cjs" + } + ] + }, + "modules": { + "status": "pass", + "cjs": { + "module": "cjs", + "status": "pass", + "durationMs": 36, + "namedExports": [ + "AlibabaRequest", + "AltchaRequest", + "AmazonRequest", + "BasiliskRequest", + "BinanceRequest", + "CapMonsterCloudClientFactory", + "CapMonsterModules", + "CastleRequest", + "ClientOptions", + "CommonCaptcha", + "CommonCaptchaRequest", + "ComplexImageFunCaptchaRequest", + "ComplexImageHCaptchaRequest", + "ComplexImageRecaptchaRequest", + "ComplexImageTaskRecognitionRequest", + "DataDomeRequest", + "ErrorType", + "FriendlyRequest", + "FunCaptchaRequest", + "GeeTestRequest", + "HCaptchaRequest", + "HuntRequest", + "ImageToTextRequest", + "ImpervaRequest", + "MTCaptchaRequest", + "ProsopoRequest", + "RecaptchaV2EnterpriseRequest", + "RecaptchaV2Request", + "RecaptchaV3EnterpriseRequest", + "RecaptchaV3ProxylessRequest", + "TSPDRequest", + "TaskType", + "TemuRequest", + "TenDIRequest", + "TurnstileRequest", + "YidunRequest" + ], + "checks": [ + { + "name": "cjs:load", + "status": "pass", + "kind": "executed", + "detail": "Loaded 36 named exports" + }, + { + "name": "cjs:export CapMonsterCloudClientFactory", + "status": "pass", + "kind": "executed" + }, + { + "name": "cjs:export ClientOptions", + "status": "pass", + "kind": "executed" + }, + { + "name": "cjs:export HuntRequest", + "status": "pass", + "kind": "executed" + }, + { + "name": "cjs:export AlibabaRequest", + "status": "pass", + "kind": "executed" + }, + { + "name": "cjs:export RecaptchaV2Request", + "status": "pass", + "kind": "executed" + }, + { + "name": "cjs:export TurnstileRequest", + "status": "pass", + "kind": "executed" + }, + { + "name": "cjs:export CommonCaptcha", + "status": "pass", + "kind": "executed" + }, + { + "name": "construct:RecaptchaV2Request", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v2/", + "durationMs": 0 + }, + { + "name": "construct:RecaptchaV2EnterpriseRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v2-enterprise/", + "durationMs": 0 + }, + { + "name": "construct:RecaptchaV3ProxylessRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v3/", + "durationMs": 0 + }, + { + "name": "construct:RecaptchaV3EnterpriseRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v3-enterprise/", + "durationMs": 1 + }, + { + "name": "construct:TurnstileRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/turnstile-task/", + "durationMs": 0 + }, + { + "name": "construct:HCaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/hcaptcha/", + "durationMs": 0 + }, + { + "name": "construct:FunCaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/funcaptcha/", + "durationMs": 0 + }, + { + "name": "construct:GeeTestRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/geetest/", + "durationMs": 0 + }, + { + "name": "construct:ImageToTextRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/image-to-text/", + "durationMs": 0 + }, + { + "name": "construct:AmazonRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/amazon-task/", + "durationMs": 0 + }, + { + "name": "construct:BinanceRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/binance/", + "durationMs": 0 + }, + { + "name": "construct:ProsopoRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/prosopo/", + "durationMs": 0 + }, + { + "name": "construct:YidunRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/yidun/", + "durationMs": 0 + }, + { + "name": "construct:MTCaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/mtcaptcha/", + "durationMs": 0 + }, + { + "name": "construct:DataDomeRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/datadome/", + "durationMs": 0 + }, + { + "name": "construct:ImpervaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/incapsula/", + "durationMs": 0 + }, + { + "name": "construct:TenDIRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/tendi/", + "durationMs": 0 + }, + { + "name": "construct:BasiliskRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/Basilisk-task/", + "durationMs": 0 + }, + { + "name": "construct:TemuRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/temu/", + "durationMs": 1 + }, + { + "name": "construct:CastleRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/castle/", + "durationMs": 0 + }, + { + "name": "construct:AltchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/altcha-task/", + "durationMs": 0 + }, + { + "name": "construct:FriendlyRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/friendly-captcha/", + "durationMs": 0 + }, + { + "name": "construct:TSPDRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/tspd/", + "durationMs": 0 + }, + { + "name": "construct:HuntRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/hunt-task/", + "durationMs": 0 + }, + { + "name": "construct:AlibabaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/alibaba-task/", + "durationMs": 0 + }, + { + "name": "construct:ComplexImageRecaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image/", + "durationMs": 0 + }, + { + "name": "construct:ComplexImageHCaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image/", + "durationMs": 0 + }, + { + "name": "construct:ComplexImageFunCaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image/", + "durationMs": 0 + }, + { + "name": "construct:ComplexImageTaskRecognitionRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image-recognition/", + "durationMs": 0 + }, + { + "name": "construct:CommonCaptcha", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/methods/", + "durationMs": 0 + }, + { + "name": "cjs:getBalance", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/methods/" + }, + { + "name": "cjs:Solve RecaptchaV2Request", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v2/" + }, + { + "name": "cjs:getBalance error", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/methods/" + }, + { + "name": "cjs:Solve createTask error", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/methods/" + }, + { + "name": "cjs:invalid websiteURL", + "status": "pass", + "kind": "executed" + } + ] + }, + "esm": { + "module": "esm", + "status": "pass", + "durationMs": 21, + "namedExports": [ + "AlibabaRequest", + "AltchaRequest", + "AmazonRequest", + "BasiliskRequest", + "BinanceRequest", + "CapMonsterCloudClientFactory", + "CapMonsterModules", + "CastleRequest", + "ClientOptions", + "CommonCaptcha", + "CommonCaptchaRequest", + "ComplexImageFunCaptchaRequest", + "ComplexImageHCaptchaRequest", + "ComplexImageRecaptchaRequest", + "ComplexImageTaskRecognitionRequest", + "DataDomeRequest", + "ErrorType", + "FriendlyRequest", + "FunCaptchaRequest", + "GeeTestRequest", + "HCaptchaRequest", + "HuntRequest", + "ImageToTextRequest", + "ImpervaRequest", + "MTCaptchaRequest", + "ProsopoRequest", + "RecaptchaV2EnterpriseRequest", + "RecaptchaV2Request", + "RecaptchaV3EnterpriseRequest", + "RecaptchaV3ProxylessRequest", + "TSPDRequest", + "TaskType", + "TemuRequest", + "TenDIRequest", + "TurnstileRequest", + "YidunRequest" + ], + "checks": [ + { + "name": "esm:load", + "status": "pass", + "kind": "executed", + "detail": "Loaded 36 named exports" + }, + { + "name": "esm:export CapMonsterCloudClientFactory", + "status": "pass", + "kind": "executed" + }, + { + "name": "esm:export ClientOptions", + "status": "pass", + "kind": "executed" + }, + { + "name": "esm:export HuntRequest", + "status": "pass", + "kind": "executed" + }, + { + "name": "esm:export AlibabaRequest", + "status": "pass", + "kind": "executed" + }, + { + "name": "esm:export RecaptchaV2Request", + "status": "pass", + "kind": "executed" + }, + { + "name": "esm:export TurnstileRequest", + "status": "pass", + "kind": "executed" + }, + { + "name": "esm:export CommonCaptcha", + "status": "pass", + "kind": "executed" + }, + { + "name": "construct:RecaptchaV2Request", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v2/", + "durationMs": 0 + }, + { + "name": "construct:RecaptchaV2EnterpriseRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v2-enterprise/", + "durationMs": 0 + }, + { + "name": "construct:RecaptchaV3ProxylessRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v3/", + "durationMs": 0 + }, + { + "name": "construct:RecaptchaV3EnterpriseRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v3-enterprise/", + "durationMs": 0 + }, + { + "name": "construct:TurnstileRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/turnstile-task/", + "durationMs": 0 + }, + { + "name": "construct:HCaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/hcaptcha/", + "durationMs": 0 + }, + { + "name": "construct:FunCaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/funcaptcha/", + "durationMs": 0 + }, + { + "name": "construct:GeeTestRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/geetest/", + "durationMs": 0 + }, + { + "name": "construct:ImageToTextRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/image-to-text/", + "durationMs": 0 + }, + { + "name": "construct:AmazonRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/amazon-task/", + "durationMs": 0 + }, + { + "name": "construct:BinanceRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/binance/", + "durationMs": 0 + }, + { + "name": "construct:ProsopoRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/prosopo/", + "durationMs": 0 + }, + { + "name": "construct:YidunRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/yidun/", + "durationMs": 0 + }, + { + "name": "construct:MTCaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/mtcaptcha/", + "durationMs": 1 + }, + { + "name": "construct:DataDomeRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/datadome/", + "durationMs": 0 + }, + { + "name": "construct:ImpervaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/incapsula/", + "durationMs": 0 + }, + { + "name": "construct:TenDIRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/tendi/", + "durationMs": 0 + }, + { + "name": "construct:BasiliskRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/Basilisk-task/", + "durationMs": 0 + }, + { + "name": "construct:TemuRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/temu/", + "durationMs": 0 + }, + { + "name": "construct:CastleRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/castle/", + "durationMs": 0 + }, + { + "name": "construct:AltchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/altcha-task/", + "durationMs": 0 + }, + { + "name": "construct:FriendlyRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/friendly-captcha/", + "durationMs": 0 + }, + { + "name": "construct:TSPDRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/tspd/", + "durationMs": 0 + }, + { + "name": "construct:HuntRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/hunt-task/", + "durationMs": 0 + }, + { + "name": "construct:AlibabaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/alibaba-task/", + "durationMs": 0 + }, + { + "name": "construct:ComplexImageRecaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image/", + "durationMs": 0 + }, + { + "name": "construct:ComplexImageHCaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image/", + "durationMs": 0 + }, + { + "name": "construct:ComplexImageFunCaptchaRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image/", + "durationMs": 0 + }, + { + "name": "construct:ComplexImageTaskRecognitionRequest", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/complex-image-recognition/", + "durationMs": 0 + }, + { + "name": "construct:CommonCaptcha", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/methods/", + "durationMs": 0 + }, + { + "name": "esm:getBalance", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/methods/" + }, + { + "name": "esm:Solve RecaptchaV2Request", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/captchas/recaptcha-v2/" + }, + { + "name": "esm:getBalance error", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/methods/" + }, + { + "name": "esm:Solve createTask error", + "status": "pass", + "kind": "executed", + "docs": "https://docs.capmonster.cloud/docs/methods/" + }, + { + "name": "esm:invalid websiteURL", + "status": "pass", + "kind": "executed" + } + ] + }, + "exportDiff": { + "onlyCjs": [], + "onlyEsm": [] + } + }, + "tests": { + "unit": { + "name": "npm run test-unit", + "status": "pass", + "kind": "executed", + "exitCode": 0, + "stdout": "> @zennolab_com/capmonstercloud-client@2.9.1 test-unit\n> jest -c jest.config.unit.js\n\n\u001b[36mRUNNING UNIT TESTS\u001b[0m", + "stderr": "ts-jest[ts-jest-transformer] (WARN) Define `ts-jest` config under `globals` is deprecated. Please do\ntransform: {\n : ['ts-jest', { /* ts-jest config goes here in Jest */ }],\n},\nSee more at https://kulshekhar.github.io/ts-jest/docs/getting-started/presets#advanced\nts-jest[ts-jest-transformer] (WARN) Define `ts-jest` config under `globals` is deprecated. Please do\ntransform: {\n : ['ts-jest', { /* ts-jest config goes here in Jest */ }],\n},\nSee more at https://kulshekhar.github.io/ts-jest/docs/getting-started/presets#advanced\nts-jest[ts-jest-transformer] (WARN) Define `ts-jest` config under `globals` is deprecated. Please do\ntransform: {\n : ['ts-jest', { /* ts-jest config goes here in Jest */ }],\n},\nSee more at https://kulshekhar.github.io/ts-jest/docs/getting-started/presets#advanced\nts-jest[ts-jest-transformer] (WARN) Define `ts-jest` config under `globals` is deprecated. Please do\ntransform: {\n : ['ts-jest', { /* ts-jest config goes here in Jest */ }],\n},\nSee more at https://kulshekhar.github.io/ts-jest/docs/getting-started/presets#advanced\nts-jest[ts-jest-transformer] (WARN) Define `ts-jest` config under `globals` is deprecated. Please do\ntransform: {\n : ['ts-jest', { /* ts-jest config goes here in Jest */ }],\n},\nSee more at https://kulshekhar.github.io/ts-jest/docs/getting-started/presets#advanced\nts-jest[ts-jest-transformer] (WARN) Define `ts-jest` config under `globals` is deprecated. Please do\ntransform: {\n : ['ts-jest', { /* ts-jest config goes here in Jest */ }],\n},\nSee more at https://kulshekhar.github.io/ts-jest/docs/getting-started/presets#advanced\nPASS src/nodeRequire.u.spec.ts\nPASS src/Requests/RequestsSerialization.u.spec.ts\nPASS src/ClientURL.u.spec.ts\nPASS src/ErrorCodeConverter.u.spec.ts\nPASS src/GetResultTimeouts.u.spec.ts\nPASS src/CapMonsterCloudClientFactory.u.spec.ts\n\nTest Suites: 6 passed, 6 total\nTests: 75 passed, 75 total\nSnapshots: 0 total\nTime: 2.205 s\nRan all test suites.", + "reproduction": "npm run test-unit" + }, + "integration": { + "name": "npm run test-integr", + "status": "pass", + "kind": "executed", + "exitCode": 0, + "stdout": "> @zennolab_com/capmonstercloud-client@2.9.1 test-integr\n> jest -c jest.config.integration.js\n\n\u001b[36mRUNNING INTEGRATION TESTS\u001b[0m", + "stderr": "ts-jest[ts-jest-transformer] (WARN) Define `ts-jest` config under `globals` is deprecated. Please do\ntransform: {\n : ['ts-jest', { /* ts-jest config goes here in Jest */ }],\n},\nSee more at https://kulshekhar.github.io/ts-jest/docs/getting-started/presets#advanced\nts-jest[ts-jest-transformer] (WARN) Define `ts-jest` config under `globals` is deprecated. Please do\ntransform: {\n : ['ts-jest', { /* ts-jest config goes here in Jest */ }],\n},\nSee more at https://kulshekhar.github.io/ts-jest/docs/getting-started/presets#advanced\nts-jest[ts-jest-transformer] (WARN) Define `ts-jest` config under `globals` is deprecated. Please do\ntransform: {\n : ['ts-jest', { /* ts-jest config goes here in Jest */ }],\n},\nSee more at https://kulshekhar.github.io/ts-jest/docs/getting-started/presets#advanced\nPASS src/HttpClient.i.spec.ts\nPASS src/Requests/TurnstileRequest/Turnstile.i.spec.ts\nPASS src/CapMonsterCloudClientFactory.i.spec.ts (39.284 s)\n\nTest Suites: 3 passed, 3 total\nTests: 47 passed, 47 total\nSnapshots: 0 total\nTime: 39.623 s\nRan all test suites.", + "reproduction": "npm run test-integr" + } + } +} diff --git a/tools/self-verify/reports/latest.md b/tools/self-verify/reports/latest.md new file mode 100644 index 0000000..a642abc --- /dev/null +++ b/tools/self-verify/reports/latest.md @@ -0,0 +1,128 @@ +# SDK self-verify report + +Generated: 2026-10-06T10:09:02.695Z + +This file mixes **executed** checks (commands actually run) and placeholders for **analysis** (API docs comparison done by the agent). + +## Summary + +- Overall: **pass** +- Build: pass +- README examples: pass +- CommonJS/ESM matrix: pass +- Unit tests: pass +- Integration tests: pass + +## Executed checks + +- PASS `npm run build` +- PASS `readme-example-1` +- PASS `readme-example-2` +- PASS `readme-example-3` +- PASS `readme-example-4` +- PASS `cjs:load` +- PASS `cjs:export CapMonsterCloudClientFactory` +- PASS `cjs:export ClientOptions` +- PASS `cjs:export HuntRequest` +- PASS `cjs:export AlibabaRequest` +- PASS `cjs:export RecaptchaV2Request` +- PASS `cjs:export TurnstileRequest` +- PASS `cjs:export CommonCaptcha` +- PASS `construct:RecaptchaV2Request` +- PASS `construct:RecaptchaV2EnterpriseRequest` +- PASS `construct:RecaptchaV3ProxylessRequest` +- PASS `construct:RecaptchaV3EnterpriseRequest` +- PASS `construct:TurnstileRequest` +- PASS `construct:HCaptchaRequest` +- PASS `construct:FunCaptchaRequest` +- PASS `construct:GeeTestRequest` +- PASS `construct:ImageToTextRequest` +- PASS `construct:AmazonRequest` +- PASS `construct:BinanceRequest` +- PASS `construct:ProsopoRequest` +- PASS `construct:YidunRequest` +- PASS `construct:MTCaptchaRequest` +- PASS `construct:DataDomeRequest` +- PASS `construct:ImpervaRequest` +- PASS `construct:TenDIRequest` +- PASS `construct:BasiliskRequest` +- PASS `construct:TemuRequest` +- PASS `construct:CastleRequest` +- PASS `construct:AltchaRequest` +- PASS `construct:FriendlyRequest` +- PASS `construct:TSPDRequest` +- PASS `construct:HuntRequest` +- PASS `construct:AlibabaRequest` +- PASS `construct:ComplexImageRecaptchaRequest` +- PASS `construct:ComplexImageHCaptchaRequest` +- PASS `construct:ComplexImageFunCaptchaRequest` +- PASS `construct:ComplexImageTaskRecognitionRequest` +- PASS `construct:CommonCaptcha` +- PASS `cjs:getBalance` +- PASS `cjs:Solve RecaptchaV2Request` +- PASS `cjs:getBalance error` +- PASS `cjs:Solve createTask error` +- PASS `cjs:invalid websiteURL` +- PASS `esm:load` +- PASS `esm:export CapMonsterCloudClientFactory` +- PASS `esm:export ClientOptions` +- PASS `esm:export HuntRequest` +- PASS `esm:export AlibabaRequest` +- PASS `esm:export RecaptchaV2Request` +- PASS `esm:export TurnstileRequest` +- PASS `esm:export CommonCaptcha` +- PASS `construct:RecaptchaV2Request` +- PASS `construct:RecaptchaV2EnterpriseRequest` +- PASS `construct:RecaptchaV3ProxylessRequest` +- PASS `construct:RecaptchaV3EnterpriseRequest` +- PASS `construct:TurnstileRequest` +- PASS `construct:HCaptchaRequest` +- PASS `construct:FunCaptchaRequest` +- PASS `construct:GeeTestRequest` +- PASS `construct:ImageToTextRequest` +- PASS `construct:AmazonRequest` +- PASS `construct:BinanceRequest` +- PASS `construct:ProsopoRequest` +- PASS `construct:YidunRequest` +- PASS `construct:MTCaptchaRequest` +- PASS `construct:DataDomeRequest` +- PASS `construct:ImpervaRequest` +- PASS `construct:TenDIRequest` +- PASS `construct:BasiliskRequest` +- PASS `construct:TemuRequest` +- PASS `construct:CastleRequest` +- PASS `construct:AltchaRequest` +- PASS `construct:FriendlyRequest` +- PASS `construct:TSPDRequest` +- PASS `construct:HuntRequest` +- PASS `construct:AlibabaRequest` +- PASS `construct:ComplexImageRecaptchaRequest` +- PASS `construct:ComplexImageHCaptchaRequest` +- PASS `construct:ComplexImageFunCaptchaRequest` +- PASS `construct:ComplexImageTaskRecognitionRequest` +- PASS `construct:CommonCaptcha` +- PASS `esm:getBalance` +- PASS `esm:Solve RecaptchaV2Request` +- PASS `esm:getBalance error` +- PASS `esm:Solve createTask error` +- PASS `esm:invalid websiteURL` +- PASS `npm run test-unit` +- PASS `npm run test-integr` +- PASS `readme-example-1` +- PASS `readme-example-2` +- PASS `readme-example-3` +- PASS `readme-example-4` + +## Skipped / unverified + +- None from the mechanical runner. + +## Analysis placeholders (fill by agent) + +- [ ] Compare SDK request fields with https://docs.capmonster.cloud/docs/captchas/ +- [ ] Compare createTask / getTaskResult / getBalance with https://docs.capmonster.cloud/docs/methods/ +- [ ] Record missing or stale SDK functionality, with documentation links +- [ ] Record documentation contradictions separately from code defects + +See `.cursor/skills/sdk-self-verify/report-template.md` for the complete report shape. + diff --git a/tools/self-verify/request-fixtures.js b/tools/self-verify/request-fixtures.js new file mode 100644 index 0000000..3e1d93f --- /dev/null +++ b/tools/self-verify/request-fixtures.js @@ -0,0 +1,347 @@ +const PROXY = { + proxyType: 'http', + proxyAddress: '8.8.8.8', + proxyPort: 8080, + proxyLogin: 'proxyLoginHere', + proxyPassword: 'proxyPasswordHere', +}; + +const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; + +/** + * Builds request fixtures from a loaded SDK namespace (CJS or ESM). + */ +function buildRequestFixtures(sdk) { + return [ + { + name: 'RecaptchaV2Request', + docs: 'https://docs.capmonster.cloud/docs/captchas/recaptcha-v2/', + create: () => + new sdk.RecaptchaV2Request({ + websiteURL: 'https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=high', + websiteKey: '6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd', + }), + expected: { type: 'NoCaptchaTask' }, + }, + { + name: 'RecaptchaV2EnterpriseRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/recaptcha-v2-enterprise/', + create: () => + new sdk.RecaptchaV2EnterpriseRequest({ + websiteURL: 'https://example.com', + websiteKey: '6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd', + }), + expected: { type: 'RecaptchaV2EnterpriseTask' }, + }, + { + name: 'RecaptchaV3ProxylessRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/recaptcha-v3/', + create: () => + new sdk.RecaptchaV3ProxylessRequest({ + websiteURL: 'https://example.com', + websiteKey: '6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd', + }), + expected: { type: 'RecaptchaV3TaskProxyless' }, + }, + { + name: 'RecaptchaV3EnterpriseRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/recaptcha-v3-enterprise/', + create: () => + new sdk.RecaptchaV3EnterpriseRequest({ + websiteURL: 'https://example.com', + websiteKey: '6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd', + }), + expected: { type: 'RecaptchaV3EnterpriseTask' }, + }, + { + name: 'TurnstileRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/turnstile-task/', + create: () => + new sdk.TurnstileRequest({ + websiteURL: 'https://tsinvisble.zlsupport.com', + websiteKey: '0x4AAAAAAABUY0VLtOUMAHxE', + }), + expected: { type: 'TurnstileTask' }, + }, + { + name: 'HCaptchaRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/hcaptcha/', + create: () => + new sdk.HCaptchaRequest({ + websiteURL: 'https://example.com', + websiteKey: 'websiteKey', + }), + expected: { type: 'HCaptchaTask' }, + }, + { + name: 'FunCaptchaRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/funcaptcha/', + create: () => + new sdk.FunCaptchaRequest({ + websiteURL: 'https://example.com', + websitePublicKey: '69A21A01-CC7B-B9C6-0F9A-E7FA06677FFC', + proxy: PROXY, + }), + expected: { type: 'FunCaptchaTask' }, + }, + { + name: 'GeeTestRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/geetest/', + create: () => + new sdk.GeeTestRequest({ + websiteURL: 'https://example.com', + gt: '81dc9bdb52d04dc20036dbd8313ed055', + version: 3, + }), + expected: { type: 'GeeTestTask' }, + }, + { + name: 'ImageToTextRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/image-to-text/', + create: () => new sdk.ImageToTextRequest({ body: 'base64image' }), + expected: { type: 'ImageToTextTask' }, + }, + { + name: 'AmazonRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/amazon-task/', + create: () => + new sdk.AmazonRequest({ + websiteURL: 'https://site.com', + websiteKey: '6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd', + challengeScript: 'https://example.com/challenge.js', + captchaScript: 'https://example.com/captcha.js', + context: 'context', + iv: 'iv', + }), + expected: { type: 'AmazonTask' }, + }, + { + name: 'BinanceRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/binance/', + create: () => + new sdk.BinanceRequest({ + websiteURL: 'https://example.com', + websiteKey: 'bizId', + validateId: 'validateId', + }), + expected: { type: 'BinanceTask' }, + }, + { + name: 'ProsopoRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/prosopo/', + create: () => + new sdk.ProsopoRequest({ + websiteURL: 'https://example.com', + websiteKey: 'websiteKey', + }), + expected: { type: 'ProsopoTask' }, + }, + { + name: 'YidunRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/yidun/', + create: () => + new sdk.YidunRequest({ + websiteURL: 'https://example.com', + websiteKey: 'websiteKey', + }), + expected: { type: 'YidunTask' }, + }, + { + name: 'MTCaptchaRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/mtcaptcha/', + create: () => + new sdk.MTCaptchaRequest({ + websiteURL: 'https://example.com', + websiteKey: 'websiteKey', + }), + expected: { type: 'MTCaptchaTask' }, + }, + { + name: 'DataDomeRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/datadome/', + create: () => + new sdk.DataDomeRequest({ + websiteURL: 'https://example.com', + metadata: { captchaUrl: 'https://geo.captcha-delivery.com/captcha/?initialCid=1', datadomeCookie: 'datadome=1' }, + proxy: PROXY, + }), + expected: { type: 'CustomTask', class: 'DataDome' }, + }, + { + name: 'ImpervaRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/incapsula/', + create: () => + new sdk.ImpervaRequest({ + websiteURL: 'https://example.com', + metadata: { incapsulaScriptUrl: '_Incapsula_Resource?SWJIYLWA=1', incapsulaCookies: 'incap_ses=1' }, + proxy: PROXY, + }), + expected: { type: 'CustomTask', class: 'Imperva' }, + }, + { + name: 'TenDIRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/tendi/', + create: () => + new sdk.TenDIRequest({ + websiteURL: 'https://example.com', + websiteKey: 'websiteKey', + }), + expected: { type: 'CustomTask', class: 'TenDI' }, + }, + { + name: 'BasiliskRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/Basilisk-task/', + create: () => + new sdk.BasiliskRequest({ + websiteURL: 'https://example.com', + websiteKey: 'websiteKey', + }), + expected: { type: 'CustomTask', class: 'Basilisk' }, + }, + { + name: 'TemuRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/temu/', + create: () => + new sdk.TemuRequest({ + websiteURL: 'https://example.com', + metadata: { cookie: 'region=1; anti_content=abc' }, + }), + expected: { type: 'CustomTask', class: 'Temu' }, + }, + { + name: 'CastleRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/castle/', + create: () => + new sdk.CastleRequest({ + websiteURL: 'https://example.com/castle', + websiteKey: 'pk_test_123', + metadata: { + wUrl: 'https://s.rsg.sc/auth/js/20251234bgef/build/cw.js', + swUrl: 'https://s.rsg.sc/auth/js/20251213bgef/build/csw.js', + }, + }), + expected: { type: 'CustomTask', class: 'Castle' }, + }, + { + name: 'AltchaRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/altcha-task/', + create: () => + new sdk.AltchaRequest({ + websiteURL: 'https://example.com', + websiteKey: 'websiteKey', + metadata: { challenge: 'c', iterations: '1', salt: 's', signature: 'sig' }, + }), + expected: { type: 'CustomTask', class: 'altcha' }, + }, + { + name: 'FriendlyRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/friendly-captcha/', + create: () => + new sdk.FriendlyRequest({ + websiteURL: 'https://example.com', + websiteKey: 'FFMGEMAD2K3JJ35P', + userAgent: UA, + metadata: { apiGetLib: 'https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.15/widget.module.min.js' }, + }), + expected: { type: 'CustomTask', class: 'friendly' }, + }, + { + name: 'TSPDRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/tspd/', + create: () => + new sdk.TSPDRequest({ + websiteURL: 'https://example.com/tspd', + userAgent: UA, + metadata: { tspdCookie: 'TS386a400d029=example', htmlPageBase64: 'html-base64' }, + proxy: PROXY, + }), + expected: { type: 'CustomTask', class: 'tspd' }, + }, + { + name: 'HuntRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/hunt-task/', + create: () => + new sdk.HuntRequest({ + websiteURL: 'https://example.com/page-with-hunt', + userAgent: UA, + metadata: { + apiGetLib: 'https://example.com/hd-api/external/apps/hash/api.js', + widgetUrl: 'https://captcha.example.com/widget?hash=widget-hash', + }, + proxy: PROXY, + }), + expected: { type: 'CustomTask', class: 'HUNT' }, + extra: ['metadata.widgetUrl'], + }, + { + name: 'AlibabaRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/alibaba-task/', + create: () => + new sdk.AlibabaRequest({ + websiteURL: 'https://www.example.com', + userAgent: UA, + metadata: { + punishUrl: 'https://example.com:443/_____tmd_____/punish?x5secdata=x&x5step=2&action=captcha&pureCaptcha=', + cookieRequired: true, + }, + }), + expected: { type: 'CustomTask', class: 'alibaba' }, + extra: ['metadata.punishUrl', 'metadata.cookieRequired'], + }, + { + name: 'ComplexImageRecaptchaRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/complex-image/', + create: () => + new sdk.ComplexImageRecaptchaRequest({ + imageUrls: ['https://example.com/image.png'], + metaData: { Grid: '3x3', Task: 'Click on traffic lights' }, + }), + expected: { type: 'ComplexImageTask', class: 'recaptcha' }, + }, + { + name: 'ComplexImageHCaptchaRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/complex-image/', + create: () => + new sdk.ComplexImageHCaptchaRequest({ + imageUrls: ['https://example.com/image.png'], + metaData: { Grid: '3x3', Task: 'Please click each image containing a mountain' }, + }), + expected: { type: 'ComplexImageTask', class: 'hcaptcha' }, + }, + { + name: 'ComplexImageFunCaptchaRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/complex-image/', + create: () => + new sdk.ComplexImageFunCaptchaRequest({ + imageUrls: ['https://example.com/image.png'], + metaData: { Task: 'Pick the duck' }, + }), + expected: { type: 'ComplexImageTask', class: 'funcaptcha' }, + }, + { + name: 'ComplexImageTaskRecognitionRequest', + docs: 'https://docs.capmonster.cloud/docs/captchas/complex-image-recognition/', + create: () => + new sdk.ComplexImageTaskRecognitionRequest({ + imagesBase64: ['/9xwee/'], + metaData: { Task: 'oocl_rotate' }, + }), + expected: { type: 'ComplexImageTask', class: 'recognition' }, + }, + { + name: 'CommonCaptcha', + docs: 'https://docs.capmonster.cloud/docs/methods/', + create: () => + new sdk.CommonCaptcha({ + task: { + type: 'RecaptchaV2Task', + websiteURL: 'https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=high', + websiteKey: '6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd', + }, + }), + expected: { type: 'RecaptchaV2Task' }, + }, + ]; +} + +module.exports = { PROXY, UA, buildRequestFixtures }; diff --git a/tools/self-verify/run-module-matrix.js b/tools/self-verify/run-module-matrix.js new file mode 100644 index 0000000..0aa4c02 --- /dev/null +++ b/tools/self-verify/run-module-matrix.js @@ -0,0 +1,276 @@ +const path = require('path'); +const { pathToFileURL } = require('url'); +const { createMockApi } = require('./lib/mock-api'); +const { buildRequestFixtures } = require('./request-fixtures'); + +const ROOT = path.resolve(__dirname, '../..'); +const CJS_ENTRY = path.join(ROOT, 'dist', 'index.js'); +const ESM_ENTRY = path.join(ROOT, 'dist', 'esm', 'index.js'); + +const FAST_TIMEOUTS = { firstRequestDelay: 0, requestsInterval: 0, timeout: 5000 }; + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function collectNamedExports(sdk) { + return Object.keys(sdk) + .filter((key) => key !== 'default' && typeof sdk[key] !== 'undefined') + .sort(); +} + +async function loadSdk(kind) { + if (kind === 'cjs') { + return require(CJS_ENTRY); + } + return import(pathToFileURL(ESM_ENTRY).href); +} + +async function runConstructAndSerialize(sdk) { + const checks = []; + const fixtures = buildRequestFixtures(sdk); + + for (const fixture of fixtures) { + const started = Date.now(); + try { + const task = fixture.create(); + assert(task.type === fixture.expected.type, `${fixture.name}: expected type ${fixture.expected.type}, got ${task.type}`); + if (fixture.expected.class) { + assert(task.class === fixture.expected.class, `${fixture.name}: expected class ${fixture.expected.class}, got ${task.class}`); + } + if (fixture.extra) { + for (const extraPath of fixture.extra) { + const value = extraPath.split('.').reduce((acc, key) => (acc == null ? acc : acc[key]), task); + assert(value !== undefined && value !== null && value !== '', `${fixture.name}: missing ${extraPath}`); + } + } + checks.push({ + name: `construct:${fixture.name}`, + status: 'pass', + kind: 'executed', + docs: fixture.docs, + durationMs: Date.now() - started, + }); + } catch (err) { + checks.push({ + name: `construct:${fixture.name}`, + status: 'fail', + kind: 'executed', + docs: fixture.docs, + durationMs: Date.now() - started, + error: err instanceof Error ? err.message : String(err), + reproduction: `Create ${fixture.name} via ${fixture.name === 'CommonCaptcha' ? 'new CommonCaptcha({ task })' : `new ${fixture.name}({...})`} and inspect task.type / metadata.`, + }); + } + } + + return checks; +} + +async function runClientFlows(sdk, kind) { + const checks = []; + const { CapMonsterCloudClientFactory, ClientOptions, RecaptchaV2Request } = sdk; + + const mock = await createMockApi([ + { responseBody: '{"errorId":0,"balance":12.34}' }, + { responseBody: '{"errorId":0,"taskId":42}' }, + { responseBody: '{"errorId":0,"status":"ready","solution":{"gRecaptchaResponse":"token"}}' }, + { responseBody: '{"errorId":1,"errorCode":"ERROR_KEY_DOES_NOT_EXIST"}' }, + { responseBody: '{"errorId":1,"errorCode":"ERROR_ZERO_BALANCE","errorDescription":"zero","taskId":0}' }, + ]); + + try { + const client = CapMonsterCloudClientFactory.Create(new ClientOptions({ clientKey: 'test-key', serviceUrl: mock.url })); + + const balance = await client.getBalance(); + assert(balance.balance === 12.34, `getBalance expected 12.34, got ${balance.balance}`); + assert( + mock.caughtRequests[0].userAgent && mock.caughtRequests[0].userAgent.includes('Zennolab.CapMonsterCloud.Client.JS'), + 'user-agent header is missing', + ); + checks.push({ + name: `${kind}:getBalance`, + status: 'pass', + kind: 'executed', + docs: 'https://docs.capmonster.cloud/docs/methods/', + }); + + const solved = await client.Solve( + new RecaptchaV2Request({ + websiteURL: 'https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=high', + websiteKey: '6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd', + }), + FAST_TIMEOUTS, + ); + assert(solved.solution && solved.solution.gRecaptchaResponse === 'token', 'Solve did not return expected solution'); + checks.push({ + name: `${kind}:Solve RecaptchaV2Request`, + status: 'pass', + kind: 'executed', + docs: 'https://docs.capmonster.cloud/docs/captchas/recaptcha-v2/', + }); + + try { + await client.getBalance(); + checks.push({ + name: `${kind}:getBalance error`, + status: 'fail', + kind: 'executed', + error: 'Expected GetBalanceError was not thrown', + reproduction: 'Call getBalance() against a mock that returns ERROR_KEY_DOES_NOT_EXIST.', + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + assert(message.includes('KEY_DOES_NOT_EXIST'), `Unexpected getBalance error: ${message}`); + checks.push({ + name: `${kind}:getBalance error`, + status: 'pass', + kind: 'executed', + docs: 'https://docs.capmonster.cloud/docs/methods/', + }); + } + + const failedSolve = await client.Solve( + new RecaptchaV2Request({ + websiteURL: 'https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=high', + websiteKey: '6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd', + }), + FAST_TIMEOUTS, + ); + assert(failedSolve.error === 'ZERO_BALANCE', `Expected ZERO_BALANCE, got ${failedSolve.error}`); + checks.push({ + name: `${kind}:Solve createTask error`, + status: 'pass', + kind: 'executed', + docs: 'https://docs.capmonster.cloud/docs/methods/', + }); + } catch (err) { + checks.push({ + name: `${kind}:client flows`, + status: 'fail', + kind: 'executed', + error: err instanceof Error ? err.message : String(err), + reproduction: `Import the ${kind} build, create CapMonsterCloudClientFactory client against a local mock, then call getBalance() and Solve().`, + }); + } finally { + await mock.close(); + } + + try { + new sdk.RecaptchaV2Request({ websiteURL: 'not-a-url', websiteKey: 'k' }); + checks.push({ + name: `${kind}:invalid websiteURL`, + status: 'fail', + kind: 'executed', + error: 'Expected websiteURL validation error was not thrown', + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + assert(message.includes('websiteURL must be a valid http or https URL'), message); + checks.push({ + name: `${kind}:invalid websiteURL`, + status: 'pass', + kind: 'executed', + }); + } + + return checks; +} + +async function runKind(kind) { + const started = Date.now(); + try { + const sdk = await loadSdk(kind); + const namedExports = collectNamedExports(sdk); + const checks = []; + + checks.push({ + name: `${kind}:load`, + status: 'pass', + kind: 'executed', + detail: `Loaded ${namedExports.length} named exports`, + }); + + const requiredExports = [ + 'CapMonsterCloudClientFactory', + 'ClientOptions', + 'HuntRequest', + 'AlibabaRequest', + 'RecaptchaV2Request', + 'TurnstileRequest', + 'CommonCaptcha', + ]; + for (const exportName of requiredExports) { + checks.push({ + name: `${kind}:export ${exportName}`, + status: typeof sdk[exportName] === 'function' || typeof sdk[exportName] === 'object' ? 'pass' : 'fail', + kind: 'executed', + error: sdk[exportName] ? undefined : `Missing export ${exportName}`, + }); + } + + checks.push(...(await runConstructAndSerialize(sdk))); + checks.push(...(await runClientFlows(sdk, kind))); + + return { + module: kind, + status: checks.some((check) => check.status === 'fail') ? 'fail' : 'pass', + durationMs: Date.now() - started, + namedExports, + checks, + }; + } catch (err) { + return { + module: kind, + status: 'fail', + durationMs: Date.now() - started, + checks: [ + { + name: `${kind}:load`, + status: 'fail', + kind: 'executed', + error: err instanceof Error ? err.stack || err.message : String(err), + reproduction: + kind === 'esm' + ? `node --input-type=module -e "import('${pathToFileURL(ESM_ENTRY).href}')"` + : `node -e "require('${CJS_ENTRY}')"`, + }, + ], + }; + } +} + +async function runModuleMatrix() { + const cjs = await runKind('cjs'); + const esm = await runKind('esm'); + const exportDiff = + cjs.namedExports && esm.namedExports + ? { + onlyCjs: cjs.namedExports.filter((name) => !esm.namedExports.includes(name)), + onlyEsm: esm.namedExports.filter((name) => !cjs.namedExports.includes(name)), + } + : undefined; + + return { + status: cjs.status === 'pass' && esm.status === 'pass' ? 'pass' : 'fail', + cjs, + esm, + exportDiff, + }; +} + +module.exports = { runModuleMatrix, CJS_ENTRY, ESM_ENTRY }; + +if (require.main === module) { + runModuleMatrix() + .then((result) => { + console.log(JSON.stringify(result, null, 2)); + process.exit(result.status === 'pass' ? 0 : 1); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/tools/self-verify/run-readme-examples.js b/tools/self-verify/run-readme-examples.js new file mode 100644 index 0000000..2452f24 --- /dev/null +++ b/tools/self-verify/run-readme-examples.js @@ -0,0 +1,185 @@ +const fs = require('fs'); +const path = require('path'); +const { spawn } = require('child_process'); +const { pathToFileURL } = require('url'); +const { createMockApi } = require('./lib/mock-api'); + +const ROOT = path.resolve(__dirname, '../..'); +const README = path.join(ROOT, 'README.md'); +const TMP = path.join(__dirname, 'tmp', 'readme-examples'); +const CJS_ENTRY = path.join(ROOT, 'dist', 'index.js'); +const ESM_ENTRY = path.join(ROOT, 'dist', 'esm', 'index.js'); + +function extractExamples(markdown) { + const examples = []; + const fence = /```(\w+)\n([\s\S]*?)```/g; + let match; + let index = 0; + while ((match = fence.exec(markdown))) { + const language = match[1]; + const code = match[2].trim(); + if (!['ts', 'typescript', 'js', 'javascript'].includes(language)) { + continue; + } + index += 1; + const isEsm = /\bimport\s+/.test(code) && !/\brequire\s*\(/.test(code); + examples.push({ + id: `readme-example-${index}`, + language, + module: isEsm ? 'esm' : 'cjs', + code, + }); + } + return examples; +} + +function rewriteExample(code, { moduleKind, serviceUrl, live }) { + let next = code.replace( + /from '@zennolab_com\/capmonstercloud-client'/g, + `from '${pathToFileURL(ESM_ENTRY).href}'`, + ); + next = next.replace( + /require\('@zennolab_com\/capmonstercloud-client'\)/g, + `require(${JSON.stringify(CJS_ENTRY)})`, + ); + + if (!live && serviceUrl) { + next = next.replace( + /new ClientOptions\(\{\s*clientKey:\s*'YOUR_API_KEY'\s*,?\s*\}\)/g, + `new ClientOptions({ clientKey: process.env.CMC_CLIENT_KEY || 'test-key', serviceUrl: ${JSON.stringify( + serviceUrl, + )} })`, + ); + } else { + next = next.replace(/'YOUR_API_KEY'/g, "process.env.CMC_CLIENT_KEY || process.env.CAPMONSTER_API_KEY"); + } + + if (moduleKind === 'esm' && !/\bawait\b/.test(next)) { + return next; + } + + return next; +} + +function runNode(filePath, env) { + return new Promise((resolve) => { + const child = spawn(process.execPath, [filePath], { + env: { ...process.env, ...env }, + cwd: ROOT, + }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + child.on('close', (code) => { + resolve({ code, stdout, stderr }); + }); + }); +} + +async function runReadmeExamples(options = {}) { + const live = Boolean(options.live); + const liveSolve = Boolean(options.liveSolve); + const markdown = fs.readFileSync(README, 'utf8'); + const examples = extractExamples(markdown); + fs.mkdirSync(TMP, { recursive: true }); + + const mock = live ? null : await createMockApi(); + + const checks = []; + try { + for (const example of examples) { + const fileName = `${example.id}.${example.module === 'esm' ? 'mjs' : 'cjs'}`; + const filePath = path.join(TMP, fileName); + const rewritten = rewriteExample(example.code, { + moduleKind: example.module, + serviceUrl: mock && mock.url, + live, + }); + fs.writeFileSync(filePath, rewritten); + + const callsSolve = /\.Solve\s*\(/.test(example.code); + if (live && callsSolve && !liveSolve) { + checks.push({ + name: example.id, + status: 'skip', + kind: 'skipped', + reason: + 'Live Solve() is disabled by default to avoid spending API balance. Re-run with --live-solve to execute.', + module: example.module, + language: example.language, + }); + continue; + } + + if (live && !process.env.CMC_CLIENT_KEY && !process.env.CAPMONSTER_API_KEY) { + checks.push({ + name: example.id, + status: 'skip', + kind: 'skipped', + reason: 'No CMC_CLIENT_KEY / CAPMONSTER_API_KEY provided for live execution.', + module: example.module, + language: example.language, + }); + continue; + } + + const result = await runNode(filePath, { + CMC_CLIENT_KEY: process.env.CMC_CLIENT_KEY || process.env.CAPMONSTER_API_KEY || 'test-key', + }); + + const expectedHint = callsSolve ? 'solution' : 'Balance'; + const stdoutMatch = result.stdout.toLowerCase(); + const looksSuccessful = + result.code === 0 && + (callsSolve ? stdoutMatch.includes('token') || stdoutMatch.includes('solution') || stdoutMatch.includes('{') : true); + + checks.push({ + name: example.id, + status: looksSuccessful ? 'pass' : 'fail', + kind: 'executed', + module: example.module, + language: example.language, + file: path.relative(ROOT, filePath), + expected: live + ? `Process exits 0 and prints a ${expectedHint} from the live API.` + : 'Process exits 0 against the local mock API and prints a solution or balance.', + exitCode: result.code, + stdout: result.stdout.trim().slice(0, 2000), + stderr: result.stderr.trim().slice(0, 2000), + error: looksSuccessful ? undefined : result.stderr.trim() || `exit ${result.code}`, + reproduction: `node ${path.relative(ROOT, filePath)}`, + }); + } + } finally { + if (mock) { + await mock.close(); + } + } + + return { + status: checks.some((check) => check.status === 'fail') ? 'fail' : 'pass', + live, + checks, + }; +} + +module.exports = { runReadmeExamples, extractExamples }; + +if (require.main === module) { + const live = process.argv.includes('--live'); + const liveSolve = process.argv.includes('--live-solve'); + runReadmeExamples({ live, liveSolve }) + .then((result) => { + console.log(JSON.stringify(result, null, 2)); + process.exit(result.status === 'pass' ? 0 : 1); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/tools/self-verify/run.js b/tools/self-verify/run.js new file mode 100644 index 0000000..abbfc6a --- /dev/null +++ b/tools/self-verify/run.js @@ -0,0 +1,188 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const { spawn } = require('child_process'); +const { runModuleMatrix } = require('./run-module-matrix'); +const { runReadmeExamples } = require('./run-readme-examples'); + +const ROOT = path.resolve(__dirname, '../..'); +const REPORT_DIR = path.join(__dirname, 'reports'); + +function parseArgs(argv) { + return { + skipBuild: argv.includes('--skip-build'), + skipTests: argv.includes('--skip-tests'), + live: argv.includes('--live'), + liveSolve: argv.includes('--live-solve'), + out: (() => { + const index = argv.indexOf('--out'); + return index >= 0 ? argv[index + 1] : path.join(REPORT_DIR, 'latest.json'); + })(), + }; +} + +function runCommand(command, args) { + return new Promise((resolve) => { + const child = spawn(command, args, { cwd: ROOT, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + child.on('close', (code) => { + resolve({ code, stdout, stderr }); + }); + }); +} + +function npmCommand() { + return process.platform === 'win32' ? 'npm.cmd' : 'npm'; +} + +async function runNpmScript(script) { + const result = await runCommand(npmCommand(), ['run', script]); + return { + name: `npm run ${script}`, + status: result.code === 0 ? 'pass' : 'fail', + kind: 'executed', + exitCode: result.code, + stdout: result.stdout.trim().slice(-4000), + stderr: result.stderr.trim().slice(-4000), + error: result.code === 0 ? undefined : `${script} failed with exit ${result.code}`, + reproduction: `npm run ${script}`, + }; +} + +function renderMarkdown(report) { + const lines = [ + '# SDK self-verify report', + '', + `Generated: ${report.generatedAt}`, + '', + 'This file mixes **executed** checks (commands actually run) and placeholders for **analysis** (API docs comparison done by the agent).', + '', + '## Summary', + '', + `- Overall: **${report.status}**`, + `- Build: ${report.build.status}`, + `- README examples: ${report.readme.status}`, + `- CommonJS/ESM matrix: ${report.modules.status}`, + `- Unit tests: ${report.tests.unit.status}`, + `- Integration tests: ${report.tests.integration.status}`, + '', + '## Executed checks', + '', + ]; + + const executed = []; + const collect = (items) => { + for (const item of items || []) { + if (item && item.kind === 'executed') { + executed.push(item); + } + if (item && item.checks) { + collect(item.checks); + } + } + }; + collect([report.build, report.readme, report.modules.cjs, report.modules.esm, report.tests.unit, report.tests.integration]); + collect(report.readme.checks); + + for (const check of executed) { + lines.push(`- ${check.status === 'pass' ? 'PASS' : 'FAIL'} \`${check.name}\`${check.error ? ` — ${check.error}` : ''}`); + if (check.reproduction && check.status === 'fail') { + lines.push(` - Reproduce: \`${check.reproduction}\``); + } + } + + lines.push('', '## Skipped / unverified', ''); + const skipped = (report.readme.checks || []).filter((check) => check.status === 'skip' || check.kind === 'skipped'); + if (skipped.length === 0) { + lines.push('- None from the mechanical runner.'); + } else { + for (const check of skipped) { + lines.push(`- \`${check.name}\`: ${check.reason || 'skipped'}`); + } + } + + lines.push( + '', + '## Analysis placeholders (fill by agent)', + '', + '- [ ] Compare SDK request fields with https://docs.capmonster.cloud/docs/captchas/', + '- [ ] Compare createTask / getTaskResult / getBalance with https://docs.capmonster.cloud/docs/methods/', + '- [ ] Record missing or stale SDK functionality, with documentation links', + '- [ ] Record documentation contradictions separately from code defects', + '', + 'See `.cursor/skills/sdk-self-verify/report-template.md` for the complete report shape.', + '', + ); + + return `${lines.join('\n')}\n`; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + fs.mkdirSync(path.dirname(args.out), { recursive: true }); + + const report = { + generatedAt: new Date().toISOString(), + kind: 'mechanical-run', + status: 'pass', + build: { name: 'npm run build', status: 'skip', kind: args.skipBuild ? 'skipped' : 'executed' }, + readme: { status: 'skip', checks: [] }, + modules: { status: 'skip' }, + tests: { + unit: { name: 'npm run test-unit', status: 'skip', kind: args.skipTests ? 'skipped' : 'executed' }, + integration: { name: 'npm run test-integr', status: 'skip', kind: args.skipTests ? 'skipped' : 'executed' }, + }, + }; + + if (!args.skipBuild) { + report.build = await runNpmScript('build'); + } else { + report.build = { name: 'npm run build', status: 'skip', kind: 'skipped', reason: '--skip-build' }; + } + + if (report.build.status === 'fail') { + report.status = 'fail'; + report.readme = { status: 'skip', kind: 'skipped', reason: 'build failed', checks: [] }; + report.modules = { status: 'skip', kind: 'skipped', reason: 'build failed' }; + } else { + report.readme = await runReadmeExamples({ live: args.live, liveSolve: args.liveSolve }); + report.modules = await runModuleMatrix(); + } + + if (args.skipTests) { + report.tests.unit = { name: 'npm run test-unit', status: 'skip', kind: 'skipped', reason: '--skip-tests' }; + report.tests.integration = { name: 'npm run test-integr', status: 'skip', kind: 'skipped', reason: '--skip-tests' }; + } else if (report.build.status !== 'fail') { + report.tests.unit = await runNpmScript('test-unit'); + report.tests.integration = await runNpmScript('test-integr'); + } + + const failed = [ + report.build, + report.readme, + report.modules, + report.tests.unit, + report.tests.integration, + ].some((section) => section && section.status === 'fail'); + report.status = failed ? 'fail' : 'pass'; + + fs.writeFileSync(args.out, `${JSON.stringify(report, null, 2)}\n`); + const markdownPath = args.out.replace(/\.json$/i, '.md'); + fs.writeFileSync(markdownPath, renderMarkdown(report)); + + console.log(`Wrote ${args.out}`); + console.log(`Wrote ${markdownPath}`); + process.exit(report.status === 'pass' ? 0 : 1); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});