@jshookmcp/jshook 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -100
- package/README.zh.md +81 -36
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +3 -1
- package/dist/modules/analyzer/QualityAnalyzer.js +1 -1
- package/dist/modules/browser/BrowserDiscovery.js +2 -2
- package/dist/modules/browser/BrowserModeManager.js +3 -3
- package/dist/modules/captcha/AICaptchaDetector.d.ts +12 -16
- package/dist/modules/captcha/AICaptchaDetector.js +209 -189
- package/dist/modules/captcha/CaptchaDetector.constants.d.ts +2 -0
- package/dist/modules/captcha/CaptchaDetector.constants.js +116 -25
- package/dist/modules/captcha/CaptchaDetector.d.ts +2 -11
- package/dist/modules/captcha/CaptchaDetector.js +102 -51
- package/dist/modules/captcha/types.d.ts +46 -0
- package/dist/modules/captcha/types.js +52 -0
- package/dist/modules/deobfuscator/AdvancedDeobfuscator.d.ts +15 -20
- package/dist/modules/deobfuscator/AdvancedDeobfuscator.js +66 -234
- package/dist/modules/deobfuscator/Deobfuscator.d.ts +3 -10
- package/dist/modules/deobfuscator/Deobfuscator.js +125 -404
- package/dist/modules/deobfuscator/webcrack.d.ts +13 -0
- package/dist/modules/deobfuscator/webcrack.js +164 -0
- package/dist/modules/detector/ObfuscationDetector.d.ts +6 -0
- package/dist/modules/detector/ObfuscationDetector.js +53 -2
- package/dist/modules/hook/AIHookGenerator.js +1 -1
- package/dist/modules/process/memory/writer.js +1 -1
- package/dist/server/domains/analysis/definitions.js +223 -2
- package/dist/server/domains/analysis/handlers.impl.d.ts +2 -3
- package/dist/server/domains/analysis/handlers.impl.js +60 -15
- package/dist/server/domains/analysis/manifest.js +2 -5
- package/dist/server/domains/browser/definitions.tools.behavior.js +36 -24
- package/dist/server/domains/browser/definitions.tools.security.js +13 -10
- package/dist/server/domains/browser/handlers/camoufox-flow.js +0 -1
- package/dist/server/domains/browser/handlers/captcha-solver.d.ts +1 -1
- package/dist/server/domains/browser/handlers/captcha-solver.js +121 -54
- package/dist/server/domains/browser/handlers/page-navigation.js +0 -2
- package/dist/server/domains/browser/handlers.impl.d.ts +1 -1
- package/dist/server/domains/browser/handlers.impl.js +3 -3
- package/dist/server/domains/browser/manifest.js +1 -1
- package/dist/server/domains/shared/modules.d.ts +1 -0
- package/dist/types/deobfuscator.d.ts +43 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/config.js +19 -10
- package/package.json +6 -3
- package/scripts/postinstall.cjs +37 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { logger } from '../../../utils/logger.js';
|
|
2
2
|
import { asJsonResponse, asTextResponse, serializeError } from '../../domains/shared/response.js';
|
|
3
3
|
import { runSourceMapExtract, runWebpackEnumerate } from '../../domains/analysis/handlers.web-tools.js';
|
|
4
|
+
import { runWebcrack } from '../../../modules/deobfuscator/webcrack.js';
|
|
4
5
|
export class CoreAnalysisHandlers {
|
|
5
6
|
collector;
|
|
6
7
|
scriptManager;
|
|
7
8
|
deobfuscator;
|
|
8
9
|
advancedDeobfuscator;
|
|
9
|
-
astOptimizer;
|
|
10
10
|
obfuscationDetector;
|
|
11
11
|
analyzer;
|
|
12
12
|
cryptoDetector;
|
|
@@ -16,7 +16,6 @@ export class CoreAnalysisHandlers {
|
|
|
16
16
|
this.scriptManager = deps.scriptManager;
|
|
17
17
|
this.deobfuscator = deps.deobfuscator;
|
|
18
18
|
this.advancedDeobfuscator = deps.advancedDeobfuscator;
|
|
19
|
-
this.astOptimizer = deps.astOptimizer;
|
|
20
19
|
this.obfuscationDetector = deps.obfuscationDetector;
|
|
21
20
|
this.analyzer = deps.analyzer;
|
|
22
21
|
this.cryptoDetector = deps.cryptoDetector;
|
|
@@ -30,6 +29,33 @@ export class CoreAnalysisHandlers {
|
|
|
30
29
|
}
|
|
31
30
|
return code;
|
|
32
31
|
}
|
|
32
|
+
extractWebcrackArgs(args) {
|
|
33
|
+
const extracted = {};
|
|
34
|
+
if (typeof args.unpack === 'boolean')
|
|
35
|
+
extracted.unpack = args.unpack;
|
|
36
|
+
if (typeof args.unminify === 'boolean')
|
|
37
|
+
extracted.unminify = args.unminify;
|
|
38
|
+
if (typeof args.jsx === 'boolean')
|
|
39
|
+
extracted.jsx = args.jsx;
|
|
40
|
+
if (typeof args.mangle === 'boolean')
|
|
41
|
+
extracted.mangle = args.mangle;
|
|
42
|
+
if (typeof args.outputDir === 'string' && args.outputDir.trim().length > 0) {
|
|
43
|
+
extracted.outputDir = args.outputDir;
|
|
44
|
+
}
|
|
45
|
+
if (typeof args.forceOutput === 'boolean')
|
|
46
|
+
extracted.forceOutput = args.forceOutput;
|
|
47
|
+
if (typeof args.includeModuleCode === 'boolean')
|
|
48
|
+
extracted.includeModuleCode = args.includeModuleCode;
|
|
49
|
+
if (typeof args.maxBundleModules === 'number')
|
|
50
|
+
extracted.maxBundleModules = args.maxBundleModules;
|
|
51
|
+
if (Array.isArray(args.mappings)) {
|
|
52
|
+
extracted.mappings = args.mappings.filter((item) => typeof item === 'object' &&
|
|
53
|
+
item !== null &&
|
|
54
|
+
typeof item.path === 'string' &&
|
|
55
|
+
typeof item.pattern === 'string');
|
|
56
|
+
}
|
|
57
|
+
return extracted;
|
|
58
|
+
}
|
|
33
59
|
async handleCollectCode(args) {
|
|
34
60
|
const returnSummaryOnly = args.returnSummaryOnly ?? false;
|
|
35
61
|
let smartMode = args.smartMode;
|
|
@@ -200,6 +226,7 @@ export class CoreAnalysisHandlers {
|
|
|
200
226
|
code,
|
|
201
227
|
llm: args.llm,
|
|
202
228
|
aggressive: args.aggressive,
|
|
229
|
+
...this.extractWebcrackArgs(args),
|
|
203
230
|
});
|
|
204
231
|
return asJsonResponse(result);
|
|
205
232
|
}
|
|
@@ -280,24 +307,42 @@ export class CoreAnalysisHandlers {
|
|
|
280
307
|
error: 'code is required and must be a non-empty string',
|
|
281
308
|
});
|
|
282
309
|
}
|
|
283
|
-
const detectOnly = args.detectOnly ?? false;
|
|
284
|
-
const aggressiveVM = args.aggressiveVM ?? false;
|
|
285
|
-
const useASTOptimization = args.useASTOptimization ?? true;
|
|
286
|
-
const timeout = args.timeout ?? 60000;
|
|
287
310
|
const result = await this.advancedDeobfuscator.deobfuscate({
|
|
288
311
|
code,
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
312
|
+
...this.extractWebcrackArgs(args),
|
|
313
|
+
...(typeof args.detectOnly === 'boolean' ? { detectOnly: args.detectOnly } : {}),
|
|
314
|
+
...(typeof args.aggressiveVM === 'boolean' ? { aggressiveVM: args.aggressiveVM } : {}),
|
|
315
|
+
...(typeof args.useASTOptimization === 'boolean'
|
|
316
|
+
? { useASTOptimization: args.useASTOptimization }
|
|
317
|
+
: {}),
|
|
318
|
+
...(typeof args.timeout === 'number' ? { timeout: args.timeout } : {}),
|
|
292
319
|
});
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
return asJsonResponse(result);
|
|
321
|
+
}
|
|
322
|
+
async handleWebcrackUnpack(args) {
|
|
323
|
+
const code = this.requireCodeArg(args, 'webcrack_unpack');
|
|
324
|
+
if (!code) {
|
|
325
|
+
return asJsonResponse({
|
|
326
|
+
success: false,
|
|
327
|
+
error: 'code is required and must be a non-empty string',
|
|
328
|
+
});
|
|
296
329
|
}
|
|
330
|
+
const result = await runWebcrack(code, {
|
|
331
|
+
unpack: args.unpack ?? true,
|
|
332
|
+
unminify: args.unminify ?? true,
|
|
333
|
+
jsx: args.jsx ?? true,
|
|
334
|
+
mangle: args.mangle ?? false,
|
|
335
|
+
...this.extractWebcrackArgs(args),
|
|
336
|
+
});
|
|
297
337
|
return asJsonResponse({
|
|
298
|
-
|
|
299
|
-
code:
|
|
300
|
-
|
|
338
|
+
success: result.applied,
|
|
339
|
+
code: result.code,
|
|
340
|
+
bundle: result.bundle,
|
|
341
|
+
savedTo: result.savedTo,
|
|
342
|
+
savedArtifacts: result.savedArtifacts,
|
|
343
|
+
optionsUsed: result.optionsUsed,
|
|
344
|
+
warning: result.reason,
|
|
345
|
+
engine: 'webcrack',
|
|
301
346
|
});
|
|
302
347
|
}
|
|
303
348
|
async handleWebpackEnumerate(args) {
|
|
@@ -3,7 +3,6 @@ import { coreTools } from '../../domains/analysis/definitions.js';
|
|
|
3
3
|
import { CoreAnalysisHandlers } from '../../domains/analysis/index.js';
|
|
4
4
|
import { Deobfuscator } from '../../domains/shared/modules.js';
|
|
5
5
|
import { AdvancedDeobfuscator } from '../../domains/shared/modules.js';
|
|
6
|
-
import { ASTOptimizer } from '../../domains/shared/modules.js';
|
|
7
6
|
import { ObfuscationDetector } from '../../domains/shared/modules.js';
|
|
8
7
|
import { CodeAnalyzer } from '../../domains/shared/modules.js';
|
|
9
8
|
import { CryptoDetector } from '../../domains/shared/modules.js';
|
|
@@ -17,9 +16,7 @@ function ensure(ctx) {
|
|
|
17
16
|
if (!ctx.deobfuscator)
|
|
18
17
|
ctx.deobfuscator = new Deobfuscator(ctx.llm);
|
|
19
18
|
if (!ctx.advancedDeobfuscator)
|
|
20
|
-
ctx.advancedDeobfuscator = new AdvancedDeobfuscator(
|
|
21
|
-
if (!ctx.astOptimizer)
|
|
22
|
-
ctx.astOptimizer = new ASTOptimizer();
|
|
19
|
+
ctx.advancedDeobfuscator = new AdvancedDeobfuscator();
|
|
23
20
|
if (!ctx.obfuscationDetector)
|
|
24
21
|
ctx.obfuscationDetector = new ObfuscationDetector();
|
|
25
22
|
if (!ctx.analyzer)
|
|
@@ -34,7 +31,6 @@ function ensure(ctx) {
|
|
|
34
31
|
scriptManager: ctx.scriptManager,
|
|
35
32
|
deobfuscator: ctx.deobfuscator,
|
|
36
33
|
advancedDeobfuscator: ctx.advancedDeobfuscator,
|
|
37
|
-
astOptimizer: ctx.astOptimizer,
|
|
38
34
|
obfuscationDetector: ctx.obfuscationDetector,
|
|
39
35
|
analyzer: ctx.analyzer,
|
|
40
36
|
cryptoDetector: ctx.cryptoDetector,
|
|
@@ -60,6 +56,7 @@ const manifest = {
|
|
|
60
56
|
{ tool: t('manage_hooks'), domain: DOMAIN, bind: b((h, a) => h.handleManageHooks(a)) },
|
|
61
57
|
{ tool: t('detect_obfuscation'), domain: DOMAIN, bind: b((h, a) => h.handleDetectObfuscation(a)) },
|
|
62
58
|
{ tool: t('advanced_deobfuscate'), domain: DOMAIN, bind: b((h, a) => h.handleAdvancedDeobfuscate(a)) },
|
|
59
|
+
{ tool: t('webcrack_unpack'), domain: DOMAIN, bind: b((h, a) => h.handleWebcrackUnpack(a)) },
|
|
63
60
|
{ tool: t('clear_collected_data'), domain: DOMAIN, bind: b((h) => h.handleClearCollectedData()) },
|
|
64
61
|
{ tool: t('get_collection_stats'), domain: DOMAIN, bind: b((h) => h.handleGetCollectionStats()) },
|
|
65
62
|
{ tool: t('webpack_enumerate'), domain: DOMAIN, bind: b((h, a) => h.handleWebpackEnumerate(a)) },
|
|
@@ -6,7 +6,7 @@ export const behaviorTools = [
|
|
|
6
6
|
'- Non-linear speed (ease-in-out)\n' +
|
|
7
7
|
'- Configurable jitter/noise\n' +
|
|
8
8
|
'- Viewport-clamped trajectory\n\n' +
|
|
9
|
-
'Use this before page_click for anti-bot bypass
|
|
9
|
+
'Use this before page_click for anti-bot bypass on browser-check or widget challenges.\n\n' +
|
|
10
10
|
'Example:\n' +
|
|
11
11
|
' human_mouse({ toX: 500, toY: 300, durationMs: 800 })',
|
|
12
12
|
inputSchema: {
|
|
@@ -74,28 +74,36 @@ export const behaviorTools = [
|
|
|
74
74
|
{
|
|
75
75
|
name: 'captcha_vision_solve',
|
|
76
76
|
description: 'Attempt to solve a CAPTCHA using an external solving service or AI vision.\n\n' +
|
|
77
|
-
'
|
|
78
|
-
'- `
|
|
79
|
-
'- `manual`
|
|
80
|
-
'Automatically detects
|
|
77
|
+
'Public contract:\n' +
|
|
78
|
+
'- `mode: "external_service"` routes to the configured solver backend\n' +
|
|
79
|
+
'- `mode: "manual"` waits for the user to solve manually\n\n' +
|
|
80
|
+
'Automatically detects the challenge class (`image` or `widget`) if `challengeType` is omitted.\n\n' +
|
|
81
81
|
'Example:\n' +
|
|
82
|
-
' captcha_vision_solve({
|
|
82
|
+
' captcha_vision_solve({ mode: "external_service", apiKey: "..." })',
|
|
83
83
|
inputSchema: {
|
|
84
84
|
type: 'object',
|
|
85
85
|
properties: {
|
|
86
|
+
mode: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
enum: ['external_service', 'manual'],
|
|
89
|
+
description: 'Solver mode (default: from config or "manual")',
|
|
90
|
+
},
|
|
86
91
|
provider: {
|
|
87
92
|
type: 'string',
|
|
88
|
-
|
|
89
|
-
description: 'Solving service provider (default: from config or "manual")',
|
|
93
|
+
description: 'Deprecated legacy external-service override; avoid in new callers',
|
|
90
94
|
},
|
|
91
|
-
apiKey: { type: 'string', description: '
|
|
92
|
-
|
|
95
|
+
apiKey: { type: 'string', description: 'External solver API key (default: from CAPTCHA_API_KEY env)' },
|
|
96
|
+
challengeType: {
|
|
93
97
|
type: 'string',
|
|
94
|
-
enum: ['image', '
|
|
95
|
-
description: '
|
|
98
|
+
enum: ['image', 'widget', 'browser_check', 'auto'],
|
|
99
|
+
description: 'Generic challenge type hint (default: auto-detect)',
|
|
96
100
|
default: 'auto',
|
|
97
101
|
},
|
|
98
|
-
|
|
102
|
+
typeHint: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
description: 'Deprecated legacy alias for challengeType; avoid in new callers',
|
|
105
|
+
},
|
|
106
|
+
siteKey: { type: 'string', description: 'Widget site key (auto-extracted if omitted)' },
|
|
99
107
|
pageUrl: { type: 'string', description: 'Page URL for context (auto-detected if omitted)' },
|
|
100
108
|
timeoutMs: { type: 'number', description: 'Max solve time in ms (default: 180000)', default: 180000 },
|
|
101
109
|
maxRetries: { type: 'integer', description: 'Max retry attempts (default: 2)', default: 2 },
|
|
@@ -103,27 +111,31 @@ export const behaviorTools = [
|
|
|
103
111
|
},
|
|
104
112
|
},
|
|
105
113
|
{
|
|
106
|
-
name: '
|
|
107
|
-
description: 'Solve
|
|
114
|
+
name: 'widget_challenge_solve',
|
|
115
|
+
description: 'Solve an embedded widget challenge.\n\n' +
|
|
108
116
|
'Strategy:\n' +
|
|
109
|
-
'1. Detect
|
|
110
|
-
'2. Send to
|
|
111
|
-
'3. Inject solved token back into the page\n' +
|
|
117
|
+
'1. Detect the widget and extract siteKey\n' +
|
|
118
|
+
'2. Send to the configured external solver service (or hook the page callback to extract token)\n' +
|
|
119
|
+
'3. Inject the solved token back into the page\n' +
|
|
112
120
|
'4. Trigger callback to proceed\n\n' +
|
|
113
|
-
'Requires either
|
|
121
|
+
'Requires either external solver credentials or uses the built-in hook approach.\n\n' +
|
|
114
122
|
'Example:\n' +
|
|
115
|
-
'
|
|
123
|
+
' widget_challenge_solve({ mode: "external_service" })',
|
|
116
124
|
inputSchema: {
|
|
117
125
|
type: 'object',
|
|
118
126
|
properties: {
|
|
119
|
-
siteKey: { type: 'string', description: '
|
|
127
|
+
siteKey: { type: 'string', description: 'Widget site key (auto-detected if omitted)' },
|
|
120
128
|
pageUrl: { type: 'string', description: 'Page URL (auto-detected if omitted)' },
|
|
129
|
+
mode: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
enum: ['external_service', 'hook', 'manual'],
|
|
132
|
+
description: 'Solving mode (default: from config or "manual")',
|
|
133
|
+
},
|
|
121
134
|
provider: {
|
|
122
135
|
type: 'string',
|
|
123
|
-
|
|
124
|
-
description: 'Solving method (default: from config or "manual")',
|
|
136
|
+
description: 'Deprecated legacy external-service override; avoid in new callers',
|
|
125
137
|
},
|
|
126
|
-
apiKey: { type: 'string', description: '
|
|
138
|
+
apiKey: { type: 'string', description: 'External solver API key' },
|
|
127
139
|
timeoutMs: { type: 'number', description: 'Max solve time in ms (default: 120000)', default: 120000 },
|
|
128
140
|
injectToken: { type: 'boolean', description: 'Auto-inject solved token into page (default: true)', default: true },
|
|
129
141
|
},
|
|
@@ -11,22 +11,23 @@ Detection process:
|
|
|
11
11
|
Supported CAPTCHA types:
|
|
12
12
|
- Slider CAPTCHA: drag-to-verify style challenges
|
|
13
13
|
- Image CAPTCHA: select-images challenges
|
|
14
|
-
-
|
|
15
|
-
-
|
|
14
|
+
- Widget CAPTCHA: embedded checkbox or iframe-based challenges
|
|
15
|
+
- Browser Check: interstitial or automatic integrity checks
|
|
16
16
|
- Custom CAPTCHA implementations
|
|
17
17
|
|
|
18
18
|
Response fields:
|
|
19
19
|
- detected: whether CAPTCHA was found
|
|
20
20
|
- type: CAPTCHA type identifier
|
|
21
|
-
-
|
|
21
|
+
- providerHint: broad provider category if identified
|
|
22
22
|
- confidence: detection confidence (0-100)
|
|
23
23
|
- reasoning: AI analysis explanation
|
|
24
|
-
-
|
|
24
|
+
- screenshotPath: saved screenshot path when a vision-capable model is unavailable
|
|
25
25
|
- suggestions: recommended next steps
|
|
26
26
|
|
|
27
27
|
Note:
|
|
28
|
-
When the MCP
|
|
29
|
-
|
|
28
|
+
When the configured MCP model cannot access vision directly, the detector saves a screenshot
|
|
29
|
+
to disk and returns screenshotPath together with prompt guidance in the reasoning field.
|
|
30
|
+
Use an external AI (GPT-4o, Claude 3) to analyze the saved screenshot if needed.`,
|
|
30
31
|
inputSchema: {
|
|
31
32
|
type: 'object',
|
|
32
33
|
properties: {},
|
|
@@ -38,10 +39,12 @@ Use an external AI (GPT-4o, Claude 3) to analyze the screenshot.`,
|
|
|
38
39
|
|
|
39
40
|
Steps:
|
|
40
41
|
1. CAPTCHA is detected on the page
|
|
41
|
-
2.
|
|
42
|
-
3. User solves the CAPTCHA manually
|
|
42
|
+
2. This tool polls the current page until the CAPTCHA is no longer detected
|
|
43
|
+
3. User solves the CAPTCHA manually in the active browser/page
|
|
43
44
|
4. Script resumes automatically after detection
|
|
44
45
|
|
|
46
|
+
Note: this tool does not switch browser modes on its own.
|
|
47
|
+
|
|
45
48
|
Timeout: default 300000ms (5 minutes)`,
|
|
46
49
|
inputSchema: {
|
|
47
50
|
type: 'object',
|
|
@@ -59,8 +62,8 @@ Timeout: default 300000ms (5 minutes)`,
|
|
|
59
62
|
description: `Configure CAPTCHA detection behavior.
|
|
60
63
|
|
|
61
64
|
Parameters:
|
|
62
|
-
- autoDetectCaptcha: auto-
|
|
63
|
-
- autoSwitchHeadless:
|
|
65
|
+
- autoDetectCaptcha: enable CAPTCHA auto-handling for browser-mode integrations that use these settings
|
|
66
|
+
- autoSwitchHeadless: allow supported integrations to switch to headed mode when CAPTCHA is detected
|
|
64
67
|
- captchaTimeout: timeout for waiting user to solve CAPTCHA in ms (default: 300000)`,
|
|
65
68
|
inputSchema: {
|
|
66
69
|
type: 'object',
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { CodeCollector } from '../../../domains/shared/modules.js';
|
|
2
2
|
export declare function handleCaptchaVisionSolve(args: Record<string, unknown>, collector: CodeCollector): Promise<unknown>;
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function handleWidgetChallengeSolve(args: Record<string, unknown>, collector: CodeCollector): Promise<unknown>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from '../../../../utils/logger.js';
|
|
2
|
-
import {
|
|
2
|
+
import { CAPTCHA_SOLVER_BASE_URL, CAPTCHA_SUBMIT_TIMEOUT_MS, CAPTCHA_POLL_INTERVAL_MS, CAPTCHA_RESULT_TIMEOUT_MS, CAPTCHA_DEFAULT_TIMEOUT_MS, CAPTCHA_MIN_TIMEOUT_MS, CAPTCHA_MAX_TIMEOUT_MS, CAPTCHA_MAX_RETRIES, CAPTCHA_DEFAULT_RETRIES, } from '../../../../constants.js';
|
|
3
3
|
function sleep(ms) {
|
|
4
4
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
5
5
|
}
|
|
@@ -14,15 +14,64 @@ function toErrorResponse(tool, error, extra = {}) {
|
|
|
14
14
|
...extra,
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
+
function normalizeSolverMode(rawMode) {
|
|
18
|
+
const value = typeof rawMode === 'string' ? rawMode.toLowerCase() : '';
|
|
19
|
+
if (value === 'hook')
|
|
20
|
+
return 'hook';
|
|
21
|
+
if (value === 'external_service')
|
|
22
|
+
return 'external_service';
|
|
23
|
+
if (value === '2captcha' || value === 'anticaptcha' || value === 'capsolver') {
|
|
24
|
+
return 'external_service';
|
|
25
|
+
}
|
|
26
|
+
return 'manual';
|
|
27
|
+
}
|
|
28
|
+
function normalizeChallengeTypeHint(rawType) {
|
|
29
|
+
const value = typeof rawType === 'string' ? rawType.toLowerCase() : '';
|
|
30
|
+
if (value === 'image')
|
|
31
|
+
return 'image';
|
|
32
|
+
if (value === 'widget' ||
|
|
33
|
+
value === 'recaptcha_v2' ||
|
|
34
|
+
value === 'recaptcha_v3' ||
|
|
35
|
+
value === 'hcaptcha' ||
|
|
36
|
+
value === 'funcaptcha' ||
|
|
37
|
+
value === 'turnstile') {
|
|
38
|
+
return 'widget';
|
|
39
|
+
}
|
|
40
|
+
if (value === 'browser_check' || value === 'managed_widget') {
|
|
41
|
+
return 'browser_check';
|
|
42
|
+
}
|
|
43
|
+
return 'auto';
|
|
44
|
+
}
|
|
45
|
+
function resolveLegacyServiceOverride(rawProvider) {
|
|
46
|
+
if (typeof rawProvider !== 'string' || !rawProvider.trim()) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return rawProvider.trim().toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
function resolveExternalServiceName(args) {
|
|
52
|
+
const legacyOverride = resolveLegacyServiceOverride(args.provider);
|
|
53
|
+
const configured = (process.env.CAPTCHA_PROVIDER || '').trim().toLowerCase();
|
|
54
|
+
return legacyOverride || configured || '2captcha';
|
|
55
|
+
}
|
|
17
56
|
async function solveWith2Captcha(apiKey, params, timeoutMs) {
|
|
18
57
|
const start = Date.now();
|
|
19
|
-
const baseUrl =
|
|
58
|
+
const baseUrl = CAPTCHA_SOLVER_BASE_URL;
|
|
59
|
+
if (!baseUrl) {
|
|
60
|
+
throw new Error('CAPTCHA_SOLVER_BASE_URL must be configured before using external_service mode.');
|
|
61
|
+
}
|
|
20
62
|
const submitBody = {
|
|
21
63
|
key: apiKey,
|
|
22
64
|
json: 1,
|
|
23
65
|
};
|
|
24
|
-
if (params.
|
|
25
|
-
|
|
66
|
+
if (params.taskKind === 'turnstile' ||
|
|
67
|
+
params.taskKind === 'recaptcha_v2' ||
|
|
68
|
+
params.taskKind === 'hcaptcha') {
|
|
69
|
+
submitBody.method =
|
|
70
|
+
params.taskKind === 'turnstile'
|
|
71
|
+
? 'turnstile'
|
|
72
|
+
: params.taskKind === 'hcaptcha'
|
|
73
|
+
? 'hcaptcha'
|
|
74
|
+
: 'userrecaptcha';
|
|
26
75
|
submitBody.sitekey = params.siteKey;
|
|
27
76
|
submitBody.pageurl = params.pageUrl;
|
|
28
77
|
}
|
|
@@ -61,8 +110,8 @@ async function solveWith2Captcha(apiKey, params, timeoutMs) {
|
|
|
61
110
|
if (resultData.status === 1) {
|
|
62
111
|
return {
|
|
63
112
|
token: resultData.request,
|
|
64
|
-
|
|
65
|
-
|
|
113
|
+
challengeType: params.taskKind === 'image' ? 'image' : 'widget',
|
|
114
|
+
mode: 'external_service',
|
|
66
115
|
durationMs: Date.now() - start,
|
|
67
116
|
};
|
|
68
117
|
}
|
|
@@ -76,73 +125,87 @@ export async function handleCaptchaVisionSolve(args, collector) {
|
|
|
76
125
|
const page = await collector.getActivePage();
|
|
77
126
|
if (!page)
|
|
78
127
|
throw new Error('No active page.');
|
|
79
|
-
const
|
|
128
|
+
const mode = normalizeSolverMode(args.mode ?? args.provider ?? process.env.CAPTCHA_PROVIDER);
|
|
129
|
+
const externalService = resolveExternalServiceName(args);
|
|
80
130
|
const apiKey = args.apiKey || process.env.CAPTCHA_API_KEY || '';
|
|
81
|
-
const
|
|
131
|
+
const challengeTypeHint = normalizeChallengeTypeHint(args.challengeType ?? args.typeHint);
|
|
82
132
|
const timeoutMs = Math.min(Math.max(args.timeoutMs ?? CAPTCHA_DEFAULT_TIMEOUT_MS, CAPTCHA_MIN_TIMEOUT_MS), CAPTCHA_MAX_TIMEOUT_MS);
|
|
83
133
|
const maxRetries = Math.min(Math.max(args.maxRetries ?? CAPTCHA_DEFAULT_RETRIES, 0), CAPTCHA_MAX_RETRIES);
|
|
84
|
-
let
|
|
134
|
+
let challengeType = challengeTypeHint;
|
|
135
|
+
let taskKind = challengeTypeHint === 'image' ? 'image' : 'recaptcha_v2';
|
|
85
136
|
let siteKey = args.siteKey;
|
|
86
137
|
const pageUrl = args.pageUrl || page.url();
|
|
87
|
-
if (
|
|
138
|
+
if (challengeType === 'auto') {
|
|
88
139
|
const detected = await page.evaluate(() => {
|
|
89
140
|
if (document.querySelector('[data-sitekey]')) {
|
|
90
141
|
const el = document.querySelector('[data-sitekey]');
|
|
91
142
|
const sk = el?.getAttribute('data-sitekey') || '';
|
|
92
|
-
if (document.querySelector('.cf-turnstile'))
|
|
93
|
-
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
143
|
+
if (document.querySelector('.cf-turnstile')) {
|
|
144
|
+
return { challengeType: 'widget', taskKind: 'turnstile', siteKey: sk };
|
|
145
|
+
}
|
|
146
|
+
if (document.querySelector('.h-captcha')) {
|
|
147
|
+
return { challengeType: 'widget', taskKind: 'hcaptcha', siteKey: sk };
|
|
148
|
+
}
|
|
149
|
+
return { challengeType: 'widget', taskKind: 'recaptcha_v2', siteKey: sk };
|
|
150
|
+
}
|
|
151
|
+
if (document.querySelector('iframe[src*="recaptcha"]')) {
|
|
152
|
+
return { challengeType: 'widget', taskKind: 'recaptcha_v2', siteKey: '' };
|
|
153
|
+
}
|
|
154
|
+
if (document.querySelector('iframe[src*="hcaptcha"]')) {
|
|
155
|
+
return { challengeType: 'widget', taskKind: 'hcaptcha', siteKey: '' };
|
|
97
156
|
}
|
|
98
|
-
if (document.querySelector('
|
|
99
|
-
return {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (document.querySelector('.cf-turnstile'))
|
|
103
|
-
return { type: 'turnstile', siteKey: '' };
|
|
104
|
-
return { type: 'image', siteKey: '' };
|
|
157
|
+
if (document.querySelector('.cf-turnstile')) {
|
|
158
|
+
return { challengeType: 'widget', taskKind: 'turnstile', siteKey: '' };
|
|
159
|
+
}
|
|
160
|
+
return { challengeType: 'image', taskKind: 'image', siteKey: '' };
|
|
105
161
|
});
|
|
106
|
-
|
|
162
|
+
challengeType = detected.challengeType;
|
|
163
|
+
taskKind = detected.taskKind;
|
|
107
164
|
if (!siteKey && detected.siteKey)
|
|
108
165
|
siteKey = detected.siteKey;
|
|
109
166
|
}
|
|
110
|
-
if (
|
|
167
|
+
else if (challengeType === 'image') {
|
|
168
|
+
taskKind = 'image';
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
taskKind = 'recaptcha_v2';
|
|
172
|
+
}
|
|
173
|
+
if (mode === 'manual') {
|
|
111
174
|
return toTextResponse({
|
|
112
175
|
success: true,
|
|
113
176
|
mode: 'manual',
|
|
114
|
-
|
|
177
|
+
challengeType,
|
|
115
178
|
siteKey: siteKey ?? null,
|
|
116
179
|
instruction: 'Please solve the CAPTCHA manually in the browser, then continue.',
|
|
117
|
-
hint: '
|
|
180
|
+
hint: 'Configure an external solver service and CAPTCHA_API_KEY to automate this flow.',
|
|
118
181
|
});
|
|
119
182
|
}
|
|
120
183
|
if (!apiKey) {
|
|
121
|
-
return toErrorResponse('captcha_vision_solve', new Error(
|
|
184
|
+
return toErrorResponse('captcha_vision_solve', new Error('External solver credentials are required. Set CAPTCHA_API_KEY.'));
|
|
122
185
|
}
|
|
123
186
|
let lastError = null;
|
|
124
187
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
125
188
|
try {
|
|
126
189
|
let result;
|
|
127
|
-
if (
|
|
190
|
+
if (externalService === '2captcha') {
|
|
128
191
|
result = await solveWith2Captcha(apiKey, {
|
|
129
|
-
|
|
192
|
+
taskKind,
|
|
130
193
|
siteKey,
|
|
131
194
|
pageUrl,
|
|
132
195
|
}, timeoutMs);
|
|
133
196
|
}
|
|
134
|
-
else if (
|
|
135
|
-
throw new Error(
|
|
136
|
-
|
|
197
|
+
else if (externalService === 'anticaptcha' || externalService === 'capsolver') {
|
|
198
|
+
throw new Error('The selected external solver service is not yet implemented. ' +
|
|
199
|
+
'Currently only the configured primary service and manual mode are supported.');
|
|
137
200
|
}
|
|
138
201
|
else {
|
|
139
|
-
throw new Error(
|
|
202
|
+
throw new Error('Unsupported external solver service.');
|
|
140
203
|
}
|
|
141
204
|
return toTextResponse({
|
|
142
205
|
success: true,
|
|
143
206
|
token: result.token,
|
|
144
|
-
|
|
145
|
-
|
|
207
|
+
challengeType: result.challengeType,
|
|
208
|
+
mode: result.mode,
|
|
146
209
|
durationMs: result.durationMs,
|
|
147
210
|
attempt: attempt + 1,
|
|
148
211
|
});
|
|
@@ -153,17 +216,18 @@ export async function handleCaptchaVisionSolve(args, collector) {
|
|
|
153
216
|
}
|
|
154
217
|
}
|
|
155
218
|
return toErrorResponse('captcha_vision_solve', lastError ?? new Error('All attempts failed'), {
|
|
156
|
-
|
|
157
|
-
|
|
219
|
+
challengeType,
|
|
220
|
+
mode,
|
|
158
221
|
maxRetries,
|
|
159
|
-
suggestion: 'Try
|
|
222
|
+
suggestion: 'Try manual mode or adjust the external solver configuration.',
|
|
160
223
|
});
|
|
161
224
|
}
|
|
162
|
-
export async function
|
|
225
|
+
export async function handleWidgetChallengeSolve(args, collector) {
|
|
163
226
|
const page = await collector.getActivePage();
|
|
164
227
|
if (!page)
|
|
165
228
|
throw new Error('No active page.');
|
|
166
|
-
const
|
|
229
|
+
const mode = normalizeSolverMode(args.mode ?? args.provider ?? process.env.CAPTCHA_PROVIDER);
|
|
230
|
+
const externalService = resolveExternalServiceName(args);
|
|
167
231
|
const apiKey = args.apiKey || process.env.CAPTCHA_API_KEY || '';
|
|
168
232
|
const timeoutMs = Math.min(Math.max(args.timeoutMs ?? 120_000, 5_000), 600_000);
|
|
169
233
|
const injectToken = args.injectToken ?? true;
|
|
@@ -176,9 +240,9 @@ export async function handleTurnstileSolve(args, collector) {
|
|
|
176
240
|
}) || undefined;
|
|
177
241
|
}
|
|
178
242
|
if (!siteKey) {
|
|
179
|
-
return toErrorResponse('
|
|
243
|
+
return toErrorResponse('widget_challenge_solve', new Error('Could not detect the widget siteKey. Provide it manually or ensure the page exposes a site key.'));
|
|
180
244
|
}
|
|
181
|
-
if (
|
|
245
|
+
if (mode === 'hook') {
|
|
182
246
|
const hookTimeoutMs = Math.min(timeoutMs, 30_000);
|
|
183
247
|
const token = await page.evaluate((hookTimeout) => {
|
|
184
248
|
return new Promise((resolve, reject) => {
|
|
@@ -195,7 +259,7 @@ export async function handleTurnstileSolve(args, collector) {
|
|
|
195
259
|
}
|
|
196
260
|
else {
|
|
197
261
|
clearTimeout(timeout);
|
|
198
|
-
reject(new Error('No
|
|
262
|
+
reject(new Error('No widget callbacks found. Try external_service mode instead.'));
|
|
199
263
|
}
|
|
200
264
|
});
|
|
201
265
|
}, hookTimeoutMs).catch(() => null);
|
|
@@ -204,29 +268,31 @@ export async function handleTurnstileSolve(args, collector) {
|
|
|
204
268
|
success: true,
|
|
205
269
|
token,
|
|
206
270
|
method: 'hook',
|
|
271
|
+
challengeType: 'widget',
|
|
207
272
|
siteKey,
|
|
208
273
|
});
|
|
209
274
|
}
|
|
210
275
|
}
|
|
211
|
-
if (
|
|
276
|
+
if (mode === 'manual') {
|
|
212
277
|
return toTextResponse({
|
|
213
278
|
success: true,
|
|
214
279
|
mode: 'manual',
|
|
280
|
+
challengeType: 'widget',
|
|
215
281
|
siteKey,
|
|
216
282
|
pageUrl,
|
|
217
|
-
instruction: 'Please complete the
|
|
283
|
+
instruction: 'Please complete the widget challenge manually.',
|
|
218
284
|
});
|
|
219
285
|
}
|
|
220
|
-
if (
|
|
221
|
-
return toErrorResponse('
|
|
222
|
-
|
|
286
|
+
if (externalService !== '2captcha') {
|
|
287
|
+
return toErrorResponse('widget_challenge_solve', new Error('The selected external solver service is not implemented for this widget flow. ' +
|
|
288
|
+
'Currently only the configured primary service, manual mode, and hook mode are supported.'));
|
|
223
289
|
}
|
|
224
290
|
if (!apiKey) {
|
|
225
|
-
return toErrorResponse('
|
|
291
|
+
return toErrorResponse('widget_challenge_solve', new Error('External solver credentials are required.'));
|
|
226
292
|
}
|
|
227
293
|
try {
|
|
228
294
|
const result = await solveWith2Captcha(apiKey, {
|
|
229
|
-
|
|
295
|
+
taskKind: 'turnstile',
|
|
230
296
|
siteKey,
|
|
231
297
|
pageUrl,
|
|
232
298
|
}, timeoutMs);
|
|
@@ -244,17 +310,18 @@ export async function handleTurnstileSolve(args, collector) {
|
|
|
244
310
|
return toTextResponse({
|
|
245
311
|
success: true,
|
|
246
312
|
token: result.token,
|
|
313
|
+
challengeType: result.challengeType,
|
|
247
314
|
siteKey,
|
|
248
|
-
|
|
315
|
+
mode: result.mode,
|
|
249
316
|
durationMs: result.durationMs,
|
|
250
317
|
injected: injectToken,
|
|
251
318
|
});
|
|
252
319
|
}
|
|
253
320
|
catch (error) {
|
|
254
|
-
return toErrorResponse('
|
|
321
|
+
return toErrorResponse('widget_challenge_solve', error, {
|
|
255
322
|
siteKey,
|
|
256
|
-
|
|
257
|
-
suggestion: 'Try
|
|
323
|
+
mode,
|
|
324
|
+
suggestion: 'Try manual mode or hook mode.',
|
|
258
325
|
});
|
|
259
326
|
}
|
|
260
327
|
}
|