@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.
Files changed (44) hide show
  1. package/README.md +145 -100
  2. package/README.zh.md +81 -36
  3. package/dist/constants.d.ts +1 -1
  4. package/dist/constants.js +3 -1
  5. package/dist/modules/analyzer/QualityAnalyzer.js +1 -1
  6. package/dist/modules/browser/BrowserDiscovery.js +2 -2
  7. package/dist/modules/browser/BrowserModeManager.js +3 -3
  8. package/dist/modules/captcha/AICaptchaDetector.d.ts +12 -16
  9. package/dist/modules/captcha/AICaptchaDetector.js +209 -189
  10. package/dist/modules/captcha/CaptchaDetector.constants.d.ts +2 -0
  11. package/dist/modules/captcha/CaptchaDetector.constants.js +116 -25
  12. package/dist/modules/captcha/CaptchaDetector.d.ts +2 -11
  13. package/dist/modules/captcha/CaptchaDetector.js +102 -51
  14. package/dist/modules/captcha/types.d.ts +46 -0
  15. package/dist/modules/captcha/types.js +52 -0
  16. package/dist/modules/deobfuscator/AdvancedDeobfuscator.d.ts +15 -20
  17. package/dist/modules/deobfuscator/AdvancedDeobfuscator.js +66 -234
  18. package/dist/modules/deobfuscator/Deobfuscator.d.ts +3 -10
  19. package/dist/modules/deobfuscator/Deobfuscator.js +125 -404
  20. package/dist/modules/deobfuscator/webcrack.d.ts +13 -0
  21. package/dist/modules/deobfuscator/webcrack.js +164 -0
  22. package/dist/modules/detector/ObfuscationDetector.d.ts +6 -0
  23. package/dist/modules/detector/ObfuscationDetector.js +53 -2
  24. package/dist/modules/hook/AIHookGenerator.js +1 -1
  25. package/dist/modules/process/memory/writer.js +1 -1
  26. package/dist/server/domains/analysis/definitions.js +223 -2
  27. package/dist/server/domains/analysis/handlers.impl.d.ts +2 -3
  28. package/dist/server/domains/analysis/handlers.impl.js +60 -15
  29. package/dist/server/domains/analysis/manifest.js +2 -5
  30. package/dist/server/domains/browser/definitions.tools.behavior.js +36 -24
  31. package/dist/server/domains/browser/definitions.tools.security.js +13 -10
  32. package/dist/server/domains/browser/handlers/camoufox-flow.js +0 -1
  33. package/dist/server/domains/browser/handlers/captcha-solver.d.ts +1 -1
  34. package/dist/server/domains/browser/handlers/captcha-solver.js +121 -54
  35. package/dist/server/domains/browser/handlers/page-navigation.js +0 -2
  36. package/dist/server/domains/browser/handlers.impl.d.ts +1 -1
  37. package/dist/server/domains/browser/handlers.impl.js +3 -3
  38. package/dist/server/domains/browser/manifest.js +1 -1
  39. package/dist/server/domains/shared/modules.d.ts +1 -0
  40. package/dist/types/deobfuscator.d.ts +43 -1
  41. package/dist/types/index.d.ts +1 -1
  42. package/dist/utils/config.js +19 -10
  43. package/package.json +6 -3
  44. 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
- detectOnly,
290
- aggressiveVM,
291
- timeout,
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
- let finalCode = result.code;
294
- if (useASTOptimization && !detectOnly) {
295
- finalCode = this.astOptimizer.optimize(finalCode);
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
- ...result,
299
- code: finalCode,
300
- astOptimized: useASTOptimization && !detectOnly,
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(ctx.llm);
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 (e.g. Cloudflare, Turnstile).\n\n' +
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
- 'Supports multiple providers through a provider-agnostic interface:\n' +
78
- '- `2captcha` / `anticaptcha` / `capsolver` external solving services\n' +
79
- '- `manual` wait for user to solve manually (fallback)\n\n' +
80
- 'Automatically detects CAPTCHA type (image, reCAPTCHA, hCaptcha) if typeHint is not provided.\n\n' +
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({ provider: "2captcha", apiKey: "..." })',
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
- enum: ['2captcha', 'anticaptcha', 'capsolver', 'manual'],
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: 'Provider API key (default: from CAPTCHA_API_KEY env)' },
92
- typeHint: {
95
+ apiKey: { type: 'string', description: 'External solver API key (default: from CAPTCHA_API_KEY env)' },
96
+ challengeType: {
93
97
  type: 'string',
94
- enum: ['image', 'recaptcha_v2', 'recaptcha_v3', 'hcaptcha', 'funcaptcha', 'turnstile', 'auto'],
95
- description: 'CAPTCHA type hint (default: auto-detect)',
98
+ enum: ['image', 'widget', 'browser_check', 'auto'],
99
+ description: 'Generic challenge type hint (default: auto-detect)',
96
100
  default: 'auto',
97
101
  },
98
- siteKey: { type: 'string', description: 'Site key for reCAPTCHA/hCaptcha/Turnstile (auto-extracted if omitted)' },
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: 'turnstile_solve',
107
- description: 'Solve a Cloudflare Turnstile challenge specifically.\n\n' +
114
+ name: 'widget_challenge_solve',
115
+ description: 'Solve an embedded widget challenge.\n\n' +
108
116
  'Strategy:\n' +
109
- '1. Detect Turnstile widget and extract siteKey\n' +
110
- '2. Send to solving service (or hook window.cf to extract token)\n' +
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 an external solver API key or uses the built-in hook approach.\n\n' +
121
+ 'Requires either external solver credentials or uses the built-in hook approach.\n\n' +
114
122
  'Example:\n' +
115
- ' turnstile_solve({ provider: "capsolver" })',
123
+ ' widget_challenge_solve({ mode: "external_service" })',
116
124
  inputSchema: {
117
125
  type: 'object',
118
126
  properties: {
119
- siteKey: { type: 'string', description: 'Turnstile site key (auto-detected if omitted)' },
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
- enum: ['2captcha', 'anticaptcha', 'capsolver', 'hook', 'manual'],
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: 'Provider API key' },
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
- - reCAPTCHA / hCaptcha
15
- - Cloudflare Challenge
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
- - vendor: vendor name if identified
21
+ - providerHint: broad provider category if identified
22
22
  - confidence: detection confidence (0-100)
23
23
  - reasoning: AI analysis explanation
24
- - screenshot: base64 screenshot (if MCP cannot view images, use external AI)
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 LLM cannot access Vision API directly, the screenshot is provided as base64.
29
- Use an external AI (GPT-4o, Claude 3) to analyze the screenshot.`,
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. Browser switches to headed (visible) mode
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-detect CAPTCHA after page_navigate (default: true)
63
- - autoSwitchHeadless: auto-switch to headed mode when CAPTCHA detected (default: true)
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',
@@ -75,7 +75,6 @@ export async function handleCamoufoxNavigateFlow(context, args) {
75
75
  text: JSON.stringify({
76
76
  success: true,
77
77
  driver: 'camoufox',
78
- captcha_detected: false,
79
78
  url: page.url(),
80
79
  title: await page.title(),
81
80
  }, null, 2),
@@ -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 handleTurnstileSolve(args: Record<string, unknown>, collector: CodeCollector): Promise<unknown>;
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 { CAPTCHA_2CAPTCHA_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';
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 = CAPTCHA_2CAPTCHA_BASE_URL;
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.type === 'turnstile' || params.type === 'recaptcha_v2' || params.type === 'hcaptcha') {
25
- submitBody.method = params.type === 'turnstile' ? 'turnstile' : params.type === 'hcaptcha' ? 'hcaptcha' : 'userrecaptcha';
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
- type: params.type,
65
- provider: '2captcha',
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 provider = args.provider || process.env.CAPTCHA_PROVIDER || 'manual';
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 typeHint = args.typeHint || 'auto';
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 captchaType = typeHint;
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 (captchaType === 'auto') {
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 { type: 'turnstile', siteKey: sk };
94
- if (document.querySelector('.h-captcha'))
95
- return { type: 'hcaptcha', siteKey: sk };
96
- return { type: 'recaptcha_v2', siteKey: sk };
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('iframe[src*="recaptcha"]'))
99
- return { type: 'recaptcha_v2', siteKey: '' };
100
- if (document.querySelector('iframe[src*="hcaptcha"]'))
101
- return { type: 'hcaptcha', siteKey: '' };
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
- captchaType = detected.type;
162
+ challengeType = detected.challengeType;
163
+ taskKind = detected.taskKind;
107
164
  if (!siteKey && detected.siteKey)
108
165
  siteKey = detected.siteKey;
109
166
  }
110
- if (provider === 'manual') {
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
- captchaType,
177
+ challengeType,
115
178
  siteKey: siteKey ?? null,
116
179
  instruction: 'Please solve the CAPTCHA manually in the browser, then continue.',
117
- hint: 'Set CAPTCHA_PROVIDER and CAPTCHA_API_KEY env vars for automatic solving.',
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(`API key required for provider "${provider}". Set CAPTCHA_API_KEY env var.`));
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 (provider === '2captcha') {
190
+ if (externalService === '2captcha') {
128
191
  result = await solveWith2Captcha(apiKey, {
129
- type: captchaType,
192
+ taskKind,
130
193
  siteKey,
131
194
  pageUrl,
132
195
  }, timeoutMs);
133
196
  }
134
- else if (provider === 'anticaptcha' || provider === 'capsolver') {
135
- throw new Error(`Provider "${provider}" is not yet implemented. ` +
136
- `Currently only "2captcha" and "manual" are supported.`);
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(`Unsupported provider: ${provider}`);
202
+ throw new Error('Unsupported external solver service.');
140
203
  }
141
204
  return toTextResponse({
142
205
  success: true,
143
206
  token: result.token,
144
- captchaType: result.type,
145
- provider: result.provider,
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
- captchaType,
157
- provider,
219
+ challengeType,
220
+ mode,
158
221
  maxRetries,
159
- suggestion: 'Try a different provider or solve manually.',
222
+ suggestion: 'Try manual mode or adjust the external solver configuration.',
160
223
  });
161
224
  }
162
- export async function handleTurnstileSolve(args, collector) {
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 provider = args.provider || process.env.CAPTCHA_PROVIDER || 'manual';
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('turnstile_solve', new Error('Could not detect Turnstile siteKey. Provide it manually or ensure the page has a Turnstile widget.'));
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 (provider === 'hook') {
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 turnstile callbacks found. Try provider: "2captcha" instead.'));
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 (provider === 'manual') {
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 Turnstile challenge manually.',
283
+ instruction: 'Please complete the widget challenge manually.',
218
284
  });
219
285
  }
220
- if (provider !== '2captcha') {
221
- return toErrorResponse('turnstile_solve', new Error(`Provider "${provider}" is not yet implemented for Turnstile. ` +
222
- `Currently only "2captcha", "manual", and "hook" are supported.`));
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('turnstile_solve', new Error('API key required'));
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
- type: 'turnstile',
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
- provider: result.provider,
315
+ mode: result.mode,
249
316
  durationMs: result.durationMs,
250
317
  injected: injectToken,
251
318
  });
252
319
  }
253
320
  catch (error) {
254
- return toErrorResponse('turnstile_solve', error, {
321
+ return toErrorResponse('widget_challenge_solve', error, {
255
322
  siteKey,
256
- provider,
257
- suggestion: 'Try provider: "manual" or provider: "hook".',
323
+ mode,
324
+ suggestion: 'Try manual mode or hook mode.',
258
325
  });
259
326
  }
260
327
  }