@nuanu-ai/agentbrowse 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +104 -0
  2. package/dist/agentpay-gateway.d.ts +8 -0
  3. package/dist/agentpay-gateway.d.ts.map +1 -0
  4. package/dist/agentpay-gateway.js +58 -0
  5. package/dist/commands/act.d.ts +5 -0
  6. package/dist/commands/act.d.ts.map +1 -0
  7. package/dist/commands/act.js +30 -0
  8. package/dist/commands/captcha-solve.d.ts +6 -0
  9. package/dist/commands/captcha-solve.d.ts.map +1 -0
  10. package/dist/commands/captcha-solve.js +36 -0
  11. package/dist/commands/close.d.ts +5 -0
  12. package/dist/commands/close.d.ts.map +1 -0
  13. package/dist/commands/close.js +32 -0
  14. package/dist/commands/extract.d.ts +5 -0
  15. package/dist/commands/extract.d.ts.map +1 -0
  16. package/dist/commands/extract.js +59 -0
  17. package/dist/commands/launch.d.ts +10 -0
  18. package/dist/commands/launch.d.ts.map +1 -0
  19. package/dist/commands/launch.js +132 -0
  20. package/dist/commands/navigate.d.ts +5 -0
  21. package/dist/commands/navigate.d.ts.map +1 -0
  22. package/dist/commands/navigate.js +26 -0
  23. package/dist/commands/observe.d.ts +5 -0
  24. package/dist/commands/observe.d.ts.map +1 -0
  25. package/dist/commands/observe.js +36 -0
  26. package/dist/commands/screenshot.d.ts +5 -0
  27. package/dist/commands/screenshot.d.ts.map +1 -0
  28. package/dist/commands/screenshot.js +27 -0
  29. package/dist/commands/status.d.ts +5 -0
  30. package/dist/commands/status.d.ts.map +1 -0
  31. package/dist/commands/status.js +47 -0
  32. package/dist/index.d.ts +3 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +247 -0
  35. package/dist/output.d.ts +20 -0
  36. package/dist/output.d.ts.map +1 -0
  37. package/dist/output.js +48 -0
  38. package/dist/session.d.ts +22 -0
  39. package/dist/session.d.ts.map +1 -0
  40. package/dist/session.js +63 -0
  41. package/dist/solver/browser-launcher.d.ts +12 -0
  42. package/dist/solver/browser-launcher.d.ts.map +1 -0
  43. package/dist/solver/browser-launcher.js +265 -0
  44. package/dist/solver/captcha-detector.d.ts +22 -0
  45. package/dist/solver/captcha-detector.d.ts.map +1 -0
  46. package/dist/solver/captcha-detector.js +463 -0
  47. package/dist/solver/captcha-runtime.d.ts +18 -0
  48. package/dist/solver/captcha-runtime.d.ts.map +1 -0
  49. package/dist/solver/captcha-runtime.js +152 -0
  50. package/dist/solver/captcha-solver.d.ts +24 -0
  51. package/dist/solver/captcha-solver.d.ts.map +1 -0
  52. package/dist/solver/captcha-solver.js +88 -0
  53. package/dist/solver/config.d.ts +12 -0
  54. package/dist/solver/config.d.ts.map +1 -0
  55. package/dist/solver/config.js +67 -0
  56. package/dist/solver/fingerprint.d.ts +9 -0
  57. package/dist/solver/fingerprint.d.ts.map +1 -0
  58. package/dist/solver/fingerprint.js +96 -0
  59. package/dist/solver/profile-manager.d.ts +8 -0
  60. package/dist/solver/profile-manager.d.ts.map +1 -0
  61. package/dist/solver/profile-manager.js +74 -0
  62. package/dist/solver/types.d.ts +66 -0
  63. package/dist/solver/types.d.ts.map +1 -0
  64. package/dist/solver/types.js +1 -0
  65. package/dist/stagehand.d.ts +20 -0
  66. package/dist/stagehand.d.ts.map +1 -0
  67. package/dist/stagehand.js +46 -0
  68. package/package.json +66 -0
@@ -0,0 +1,463 @@
1
+ const DEFAULT_SOLVE_TIMEOUT_MS = 90000;
2
+ const SOLVE_TIMEOUT = Symbol('solve-timeout');
3
+ export async function detectCaptchas(page) {
4
+ const pageUrl = page.url();
5
+ return page.evaluate((url) => {
6
+ const found = [];
7
+ const recaptchaEl = document.querySelector('.g-recaptcha[data-sitekey], div[data-sitekey]:not([data-hcaptcha-widget-id])');
8
+ if (recaptchaEl) {
9
+ const key = recaptchaEl.getAttribute('data-sitekey');
10
+ if (key)
11
+ found.push({ type: 'recaptcha-v2', siteKey: key, pageUrl: url });
12
+ }
13
+ const recaptchaIframe = document.querySelector('iframe[src*="recaptcha/api2"], iframe[src*="recaptcha/enterprise"]');
14
+ if (recaptchaIframe && found.length === 0) {
15
+ const match = recaptchaIframe.src.match(/[?&]k=([^&]+)/);
16
+ if (match?.[1])
17
+ found.push({ type: 'recaptcha-v2', siteKey: match[1], pageUrl: url });
18
+ }
19
+ const hcaptchaEl = document.querySelector('.h-captcha[data-sitekey], div[data-hcaptcha-widget-id][data-sitekey]');
20
+ if (hcaptchaEl) {
21
+ const key = hcaptchaEl.getAttribute('data-sitekey');
22
+ if (key)
23
+ found.push({ type: 'hcaptcha', siteKey: key, pageUrl: url });
24
+ }
25
+ const turnstileEl = document.querySelector('.cf-turnstile[data-sitekey]');
26
+ if (turnstileEl) {
27
+ const key = turnstileEl.getAttribute('data-sitekey');
28
+ if (key)
29
+ found.push({ type: 'turnstile', siteKey: key, pageUrl: url });
30
+ }
31
+ return found;
32
+ }, pageUrl);
33
+ }
34
+ export async function solveVisibleCaptchas(page, solver) {
35
+ const outcomes = await solveVisibleCaptchasWithOptions(page, solver, undefined);
36
+ return outcomes.filter((outcome) => outcome.injected).length;
37
+ }
38
+ export async function solveVisibleCaptchasWithOptions(page, solver, opts) {
39
+ const captchas = await detectCaptchas(page);
40
+ const outcomes = [];
41
+ const timeoutMs = opts?.solveTimeoutMs ?? DEFAULT_SOLVE_TIMEOUT_MS;
42
+ for (const captcha of captchas) {
43
+ if (opts?.skipCaptcha?.(captcha)) {
44
+ continue;
45
+ }
46
+ opts?.onProgress?.(`[captcha] detected ${captcha.type} on ${captcha.pageUrl}, sending to solver (timeout ${Math.ceil(timeoutMs / 1000)}s)`);
47
+ try {
48
+ const maybeToken = await Promise.race([
49
+ solver.solve(captcha.type, captcha.siteKey, captcha.pageUrl),
50
+ sleep(timeoutMs).then(() => SOLVE_TIMEOUT),
51
+ ]);
52
+ if (maybeToken === SOLVE_TIMEOUT) {
53
+ opts?.onProgress?.(`[captcha] solver timed out for ${captcha.type}`);
54
+ const timeoutOutcome = {
55
+ captcha,
56
+ injected: false,
57
+ clicked: false,
58
+ verified: false,
59
+ error: 'solver-timeout',
60
+ };
61
+ outcomes.push(timeoutOutcome);
62
+ opts?.onCaptchaResult?.(timeoutOutcome);
63
+ continue;
64
+ }
65
+ const token = maybeToken;
66
+ await injectCaptchaToken(page, captcha, token);
67
+ let verified = await verifyCaptchaResolved(page, captcha);
68
+ let clicked = false;
69
+ // Click only as last resort — it triggers a new Google challenge flow,
70
+ // which ignores the injected token. Token + callback is the primary path.
71
+ if (!verified) {
72
+ clicked = await bestEffortClickCaptchaCheckbox(page, captcha, opts?.onProgress);
73
+ if (clicked) {
74
+ verified = await verifyCaptchaResolved(page, captcha);
75
+ }
76
+ }
77
+ const outcome = {
78
+ captcha,
79
+ injected: true,
80
+ clicked,
81
+ verified,
82
+ };
83
+ outcomes.push(outcome);
84
+ opts?.onCaptchaResult?.(outcome);
85
+ if (verified) {
86
+ opts?.onSolvedCaptcha?.(captcha);
87
+ opts?.onProgress?.(`[captcha] solved ${captcha.type}, token injected${clicked ? ' + click' : ''}, verified`);
88
+ }
89
+ else {
90
+ opts?.onProgress?.(`[captcha] solved ${captcha.type}, token injected${clicked ? ' + click' : ''}, unresolved`);
91
+ }
92
+ }
93
+ catch (error) {
94
+ const reason = formatSolveError(error);
95
+ opts?.onProgress?.(`[captcha] solver failed for ${captcha.type}: ${reason}`);
96
+ const failOutcome = {
97
+ captcha,
98
+ injected: false,
99
+ clicked: false,
100
+ verified: false,
101
+ error: 'solver-failed',
102
+ };
103
+ outcomes.push(failOutcome);
104
+ opts?.onCaptchaResult?.(failOutcome);
105
+ // Continue with remaining captcha widgets.
106
+ }
107
+ }
108
+ return outcomes;
109
+ }
110
+ async function injectCaptchaToken(page, captcha, token) {
111
+ await page.evaluate((info) => {
112
+ if (info.type === 'recaptcha-v2') {
113
+ const selectors = [
114
+ '#g-recaptcha-response',
115
+ "textarea[name='g-recaptcha-response']",
116
+ "textarea[id*='g-recaptcha-response']",
117
+ "input[name='g-recaptcha-response']",
118
+ ];
119
+ for (const selector of selectors) {
120
+ const nodes = document.querySelectorAll(selector);
121
+ for (const node of Array.from(nodes)) {
122
+ node.value = info.token;
123
+ node.dispatchEvent(new Event('input', { bubbles: true }));
124
+ node.dispatchEvent(new Event('change', { bubbles: true }));
125
+ }
126
+ }
127
+ // Invoke reCAPTCHA success callback
128
+ let callbackInvoked = false;
129
+ // Method 1: data-callback attribute (implicit rendering)
130
+ const el = document.querySelector('.g-recaptcha[data-callback], div[data-sitekey][data-callback]');
131
+ const callbackName = el?.getAttribute('data-callback');
132
+ if (callbackName) {
133
+ const cb = window[callbackName];
134
+ if (typeof cb === 'function') {
135
+ cb(info.token);
136
+ callbackInvoked = true;
137
+ }
138
+ }
139
+ // Method 2: ___grecaptcha_cfg.clients (explicit rendering / no data-callback)
140
+ if (!callbackInvoked) {
141
+ /* eslint-disable -- plain JS in browser evaluate context, no TS helpers available */
142
+ const win = window;
143
+ const cfg = win.___grecaptcha_cfg;
144
+ if (cfg && cfg.clients) {
145
+ const queue = [];
146
+ let found = null;
147
+ for (const client of Object.values(cfg.clients)) {
148
+ queue.push([client, 0]);
149
+ }
150
+ while (queue.length > 0 && !found) {
151
+ const pair = queue.shift();
152
+ const obj = pair[0];
153
+ const depth = pair[1];
154
+ if (depth > 5 || !obj || typeof obj !== 'object')
155
+ continue;
156
+ if (typeof obj.callback === 'function') {
157
+ found = obj.callback;
158
+ break;
159
+ }
160
+ for (const val of Object.values(obj)) {
161
+ if (typeof val === 'object' && val !== null) {
162
+ queue.push([val, depth + 1]);
163
+ }
164
+ }
165
+ }
166
+ if (found)
167
+ found(info.token);
168
+ }
169
+ /* eslint-enable */
170
+ }
171
+ const submitBtn = document.querySelector("button[type='submit'], input[type='submit']");
172
+ if (submitBtn)
173
+ submitBtn.removeAttribute('disabled');
174
+ }
175
+ else if (info.type === 'hcaptcha') {
176
+ const selectors = [
177
+ "textarea[name='h-captcha-response']",
178
+ "textarea[id*='h-captcha-response']",
179
+ "textarea[name*='hcaptcha-response']",
180
+ "textarea[id*='hcaptcha-response']",
181
+ "input[name='h-captcha-response']",
182
+ "input[name*='hcaptcha-response']",
183
+ ];
184
+ for (const selector of selectors) {
185
+ const nodes = document.querySelectorAll(selector);
186
+ for (const node of Array.from(nodes)) {
187
+ node.value = info.token;
188
+ node.dispatchEvent(new Event('input', { bubbles: true }));
189
+ node.dispatchEvent(new Event('change', { bubbles: true }));
190
+ }
191
+ }
192
+ }
193
+ else if (info.type === 'turnstile') {
194
+ const selectors = [
195
+ "input[name='cf-turnstile-response']",
196
+ "textarea[name='cf-turnstile-response']",
197
+ "[id*='turnstile-response']",
198
+ ];
199
+ for (const selector of selectors) {
200
+ const nodes = document.querySelectorAll(selector);
201
+ for (const node of Array.from(nodes)) {
202
+ node.value = info.token;
203
+ node.dispatchEvent(new Event('input', { bubbles: true }));
204
+ node.dispatchEvent(new Event('change', { bubbles: true }));
205
+ }
206
+ }
207
+ }
208
+ }, { type: captcha.type, token });
209
+ }
210
+ function recaptchaAnchorFrames(page, siteKey) {
211
+ const all = page.frames().filter((frame) => frame.url().includes('/recaptcha/api2/anchor'));
212
+ const keyed = all.filter((frame) => {
213
+ try {
214
+ return new URL(frame.url()).searchParams.get('k') === siteKey;
215
+ }
216
+ catch {
217
+ return false;
218
+ }
219
+ });
220
+ return keyed.length > 0 ? keyed : all;
221
+ }
222
+ async function bestEffortClickCaptchaCheckbox(page, captcha, onProgress) {
223
+ if (captcha.type !== 'recaptcha-v2') {
224
+ return false;
225
+ }
226
+ const topology = await inspectRecaptchaTopology(page, captcha.siteKey);
227
+ if (topology.anchorCount === 0) {
228
+ onProgress?.('[captcha] best-effort click skipped: no anchor frames found');
229
+ return false;
230
+ }
231
+ if (topology.hasMultipleSiteKeys || topology.anchorCount > 1) {
232
+ onProgress?.(`[captcha] recaptcha topology: ${topology.anchorCount} anchor(s), ${topology.hasMultipleSiteKeys ? 'multiple' : 'single'} siteKey(s) — proceeding with targeted click`);
233
+ }
234
+ if (await isVisibleRecaptchaChallenge(page, captcha.siteKey)) {
235
+ onProgress?.('[captcha] best-effort click skipped: recaptcha image challenge already visible');
236
+ return false;
237
+ }
238
+ const frames = recaptchaAnchorFrames(page, captcha.siteKey);
239
+ for (const frame of frames) {
240
+ try {
241
+ const state = await frame.evaluate(() => {
242
+ const anchor = document.querySelector('#recaptcha-anchor');
243
+ if (!anchor) {
244
+ return { present: false, checked: false };
245
+ }
246
+ return {
247
+ present: true,
248
+ checked: anchor.getAttribute('aria-checked') === 'true',
249
+ };
250
+ });
251
+ if (!state.present || state.checked) {
252
+ continue;
253
+ }
254
+ const anchor = await frame.$('#recaptcha-anchor');
255
+ if (!anchor) {
256
+ continue;
257
+ }
258
+ await anchor.click({ delay: 50 });
259
+ await anchor.dispose();
260
+ onProgress?.('[captcha] best-effort click: recaptcha checkbox');
261
+ await sleep(700);
262
+ return true;
263
+ }
264
+ catch {
265
+ // Continue with other matching frames.
266
+ }
267
+ }
268
+ return false;
269
+ }
270
+ async function inspectRecaptchaTopology(page, siteKey) {
271
+ try {
272
+ return await page.evaluate((key) => {
273
+ const anchors = Array.from(document.querySelectorAll('iframe[src*="recaptcha/api2/anchor"]'));
274
+ const siteKeys = new Set();
275
+ let anchorCount = 0;
276
+ for (const anchor of anchors) {
277
+ try {
278
+ const k = new URL(anchor.src).searchParams.get('k');
279
+ if (k)
280
+ siteKeys.add(k);
281
+ if (k === key)
282
+ anchorCount += 1;
283
+ }
284
+ catch {
285
+ // ignore malformed URL
286
+ }
287
+ }
288
+ return {
289
+ anchorCount,
290
+ hasMultipleSiteKeys: siteKeys.size > 1,
291
+ };
292
+ }, siteKey);
293
+ }
294
+ catch {
295
+ return { anchorCount: 0, hasMultipleSiteKeys: false };
296
+ }
297
+ }
298
+ async function isVisibleRecaptchaChallenge(page, siteKey) {
299
+ try {
300
+ return await page.evaluate((key) => {
301
+ const frames = Array.from(document.querySelectorAll('iframe[src*="recaptcha/api2/bframe"]'));
302
+ for (const frame of frames) {
303
+ try {
304
+ const url = new URL(frame.src);
305
+ const k = url.searchParams.get('k');
306
+ if (k && k !== key)
307
+ continue;
308
+ }
309
+ catch {
310
+ // ignore malformed frame URL
311
+ }
312
+ const rect = frame.getBoundingClientRect();
313
+ if (rect.width <= 0 || rect.height <= 0) {
314
+ continue;
315
+ }
316
+ if (rect.bottom < 0 || rect.top > window.innerHeight) {
317
+ continue;
318
+ }
319
+ const style = window.getComputedStyle(frame);
320
+ if (style.visibility !== 'hidden' && style.display !== 'none' && style.opacity !== '0') {
321
+ return true;
322
+ }
323
+ }
324
+ return false;
325
+ }, siteKey);
326
+ }
327
+ catch {
328
+ return false;
329
+ }
330
+ }
331
+ async function hasTokenInAnyFrame(page, selectors) {
332
+ for (const frame of page.frames()) {
333
+ try {
334
+ const hasToken = await frame.evaluate((selList) => {
335
+ for (const selector of selList) {
336
+ const nodes = document.querySelectorAll(selector);
337
+ for (const node of Array.from(nodes)) {
338
+ const input = node;
339
+ const direct = typeof input.value === 'string' ? input.value : '';
340
+ const value = (direct || node.textContent || '').trim();
341
+ if (value.length > 20) {
342
+ return true;
343
+ }
344
+ }
345
+ }
346
+ return false;
347
+ }, selectors);
348
+ if (hasToken)
349
+ return true;
350
+ }
351
+ catch {
352
+ // Some frames are inaccessible/transient; ignore.
353
+ }
354
+ }
355
+ return false;
356
+ }
357
+ async function verifyRecaptchaResolved(page, siteKey) {
358
+ const frames = recaptchaAnchorFrames(page, siteKey);
359
+ if (frames.length > 0) {
360
+ let sawAnchor = false;
361
+ for (const frame of frames) {
362
+ try {
363
+ const checked = await frame.evaluate(() => {
364
+ const anchor = document.querySelector('#recaptcha-anchor');
365
+ if (!anchor)
366
+ return null;
367
+ return anchor.getAttribute('aria-checked') === 'true';
368
+ });
369
+ if (checked === null)
370
+ continue;
371
+ sawAnchor = true;
372
+ if (checked)
373
+ return true;
374
+ }
375
+ catch {
376
+ // Ignore transient frame access errors.
377
+ }
378
+ }
379
+ }
380
+ // Token in response field is sufficient — injection + callback handles site-side verification;
381
+ // aria-checked is a visual indicator inside Google's iframe, not required for form submission.
382
+ return hasTokenInAnyFrame(page, [
383
+ '#g-recaptcha-response',
384
+ "textarea[name='g-recaptcha-response']",
385
+ "textarea[id*='g-recaptcha-response']",
386
+ "input[name='g-recaptcha-response']",
387
+ ]);
388
+ }
389
+ async function verifyCaptchaResolved(page, captcha) {
390
+ switch (captcha.type) {
391
+ case 'recaptcha-v2':
392
+ return verifyRecaptchaResolved(page, captcha.siteKey);
393
+ case 'hcaptcha':
394
+ return hasTokenInAnyFrame(page, [
395
+ "textarea[name='h-captcha-response']",
396
+ "textarea[id*='h-captcha-response']",
397
+ "textarea[name*='hcaptcha-response']",
398
+ "textarea[id*='hcaptcha-response']",
399
+ "input[name='h-captcha-response']",
400
+ "input[name*='hcaptcha-response']",
401
+ ]);
402
+ case 'turnstile':
403
+ return hasTokenInAnyFrame(page, [
404
+ "input[name='cf-turnstile-response']",
405
+ "textarea[name='cf-turnstile-response']",
406
+ "[id*='turnstile-response']",
407
+ ]);
408
+ }
409
+ }
410
+ export function setupCaptchaMonitor(page, solver) {
411
+ let solving = false;
412
+ const checkAndSolve = async () => {
413
+ if (solving)
414
+ return;
415
+ solving = true;
416
+ try {
417
+ await solveVisibleCaptchasWithOptions(page, solver);
418
+ }
419
+ catch {
420
+ // Page may navigate/close during scan.
421
+ }
422
+ finally {
423
+ solving = false;
424
+ }
425
+ };
426
+ page.on('domcontentloaded', () => {
427
+ void checkAndSolve();
428
+ });
429
+ const interval = setInterval(() => {
430
+ if (page.isClosed()) {
431
+ clearInterval(interval);
432
+ return;
433
+ }
434
+ void checkAndSolve();
435
+ }, 3000);
436
+ page.once('close', () => clearInterval(interval));
437
+ }
438
+ function sleep(ms) {
439
+ return new Promise((resolve) => setTimeout(resolve, ms));
440
+ }
441
+ function formatSolveError(error) {
442
+ if (error == null)
443
+ return 'unknown-error';
444
+ if (typeof error === 'string')
445
+ return error;
446
+ if (error instanceof Error) {
447
+ const withErr = error;
448
+ const details = [];
449
+ if (withErr.message)
450
+ details.push(withErr.message);
451
+ if (withErr.err)
452
+ details.push(String(withErr.err));
453
+ if (withErr.code != null)
454
+ details.push(`code=${String(withErr.code)}`);
455
+ return details.length > 0 ? details.join(' | ') : error.name;
456
+ }
457
+ try {
458
+ return JSON.stringify(error);
459
+ }
460
+ catch {
461
+ return String(error);
462
+ }
463
+ }
@@ -0,0 +1,18 @@
1
+ export type SolveCaptchasByCdpOptions = {
2
+ timeoutMs?: number;
3
+ pollIntervalMs?: number;
4
+ solveTimeoutMs?: number;
5
+ connectTimeoutMs?: number;
6
+ apiUrl?: string;
7
+ onProgress?: (message: string) => void;
8
+ };
9
+ export type SolveCaptchasByCdpResult = {
10
+ solved: number;
11
+ verified: number;
12
+ unresolved: number;
13
+ unresolvedCaptchas: string[];
14
+ detected: number;
15
+ timedOut: boolean;
16
+ };
17
+ export declare function solveCaptchasByCdp(cdpUrl: string, apiKey: string, opts?: SolveCaptchasByCdpOptions): Promise<SolveCaptchasByCdpResult>;
18
+ //# sourceMappingURL=captcha-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captcha-runtime.d.ts","sourceRoot":"","sources":["../../src/solver/captcha-runtime.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAMF,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,yBAAyB,GAC/B,OAAO,CAAC,wBAAwB,CAAC,CA8GnC"}
@@ -0,0 +1,152 @@
1
+ import puppeteer from 'puppeteer';
2
+ import { CaptchaSolver } from './captcha-solver.js';
3
+ import { detectCaptchas, solveVisibleCaptchasWithOptions } from './captcha-detector.js';
4
+ const DEFAULT_TIMEOUT_MS = 90000;
5
+ const DEFAULT_POLL_MS = 1500;
6
+ const DEFAULT_CONNECT_TIMEOUT_MS = 15000;
7
+ export async function solveCaptchasByCdp(cdpUrl, apiKey, opts) {
8
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
9
+ const pollIntervalMs = opts?.pollIntervalMs ?? DEFAULT_POLL_MS;
10
+ const connectTimeoutMs = opts?.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
11
+ const deadline = Date.now() + timeoutMs;
12
+ const onProgress = opts?.onProgress;
13
+ onProgress?.('[captcha] connecting to browser...');
14
+ const browser = await withTimeout(puppeteer.connect(buildConnectOptions(cdpUrl)), connectTimeoutMs, `Failed to connect to browser within ${Math.ceil(connectTimeoutMs / 1000)}s`);
15
+ try {
16
+ const solver = new CaptchaSolver({
17
+ apiKey,
18
+ apiUrl: opts?.apiUrl,
19
+ taskTimeoutMs: opts?.solveTimeoutMs,
20
+ });
21
+ let solved = 0;
22
+ const detectedKeys = new Set();
23
+ const solvedKeys = new Set();
24
+ const verifiedKeys = new Set();
25
+ let sawCaptcha = false;
26
+ let lastWaitLogAt = 0;
27
+ onProgress?.(`[captcha] connected to browser, waiting for captcha up to ${Math.ceil(timeoutMs / 1000)}s`);
28
+ while (Date.now() < deadline) {
29
+ const pages = await getCandidatePages(browser);
30
+ if (pages.length === 0) {
31
+ if (Date.now() - lastWaitLogAt > 10000) {
32
+ onProgress?.('[captcha] waiting for active page...');
33
+ lastWaitLogAt = Date.now();
34
+ }
35
+ await sleep(pollIntervalMs);
36
+ continue;
37
+ }
38
+ let foundAnyCaptcha = false;
39
+ for (const page of pages) {
40
+ const captchas = await detectCaptchas(page);
41
+ if (captchas.length === 0)
42
+ continue;
43
+ foundAnyCaptcha = true;
44
+ if (!sawCaptcha) {
45
+ onProgress?.(`[captcha] captcha detected on ${stripQuery(page.url()) || 'current tab'} (${captchas.length})`);
46
+ }
47
+ for (const captcha of captchas) {
48
+ const key = `${captcha.type}:${captcha.siteKey}:${captcha.pageUrl}`;
49
+ detectedKeys.add(key);
50
+ }
51
+ sawCaptcha = true;
52
+ const unsolvedBefore = captchas.filter((captcha) => !solvedKeys.has(captchaKey(captcha)));
53
+ if (unsolvedBefore.length === 0) {
54
+ return finalizeResult(solved, detectedKeys, verifiedKeys, false, onProgress);
55
+ }
56
+ const outcomes = await solveVisibleCaptchasWithOptions(page, solver, {
57
+ solveTimeoutMs: opts?.solveTimeoutMs,
58
+ onProgress,
59
+ skipCaptcha: (captcha) => solvedKeys.has(captchaKey(captcha)),
60
+ });
61
+ for (const outcome of outcomes) {
62
+ const key = captchaKey(outcome.captcha);
63
+ if (outcome.injected || outcome.verified) {
64
+ solved += 1;
65
+ solvedKeys.add(key);
66
+ }
67
+ if (outcome.verified) {
68
+ verifiedKeys.add(key);
69
+ }
70
+ }
71
+ const unsolvedAfter = unsolvedBefore.filter((captcha) => !solvedKeys.has(captchaKey(captcha)));
72
+ if (unsolvedAfter.length === 0) {
73
+ return finalizeResult(solved, detectedKeys, verifiedKeys, false, onProgress);
74
+ }
75
+ }
76
+ if (foundAnyCaptcha) {
77
+ await sleep(1000);
78
+ continue;
79
+ }
80
+ if (sawCaptcha) {
81
+ return finalizeResult(solved, detectedKeys, verifiedKeys, false, onProgress);
82
+ }
83
+ if (Date.now() - lastWaitLogAt > 10000) {
84
+ onProgress?.('[captcha] waiting for captcha...');
85
+ lastWaitLogAt = Date.now();
86
+ }
87
+ await sleep(pollIntervalMs);
88
+ }
89
+ onProgress?.('[captcha] timeout reached');
90
+ return finalizeResult(solved, detectedKeys, verifiedKeys, true, onProgress);
91
+ }
92
+ finally {
93
+ await browser.disconnect();
94
+ }
95
+ }
96
+ function finalizeResult(solved, detectedKeys, verifiedKeys, timedOut, onProgress) {
97
+ const detected = detectedKeys.size;
98
+ const unresolvedCaptchas = Array.from(detectedKeys).filter((key) => !verifiedKeys.has(key));
99
+ const verified = verifiedKeys.size;
100
+ const unresolved = unresolvedCaptchas.length;
101
+ onProgress?.(`[captcha] done: solved ${solved}, verified ${verified}, unresolved ${unresolved}, detected ${detected}`);
102
+ return {
103
+ solved,
104
+ verified,
105
+ unresolved,
106
+ unresolvedCaptchas,
107
+ detected,
108
+ timedOut,
109
+ };
110
+ }
111
+ function captchaKey(captcha) {
112
+ return `${captcha.type}:${captcha.siteKey}:${captcha.pageUrl}`;
113
+ }
114
+ async function getCandidatePages(browser) {
115
+ const pages = await browser.pages();
116
+ return pages.filter((page) => !page.url().startsWith('devtools://'));
117
+ }
118
+ function buildConnectOptions(cdpUrl) {
119
+ try {
120
+ const parsed = new URL(cdpUrl);
121
+ if (parsed.protocol === 'ws:' || parsed.protocol === 'wss:') {
122
+ return {
123
+ browserURL: `${parsed.protocol === 'wss:' ? 'https:' : 'http:'}//${parsed.host}`,
124
+ };
125
+ }
126
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
127
+ return { browserURL: `${parsed.protocol}//${parsed.host}` };
128
+ }
129
+ }
130
+ catch {
131
+ // Fallback to raw WS endpoint below.
132
+ }
133
+ return { browserWSEndpoint: cdpUrl };
134
+ }
135
+ async function withTimeout(promise, timeoutMs, message) {
136
+ const timeoutPromise = sleep(timeoutMs).then(() => {
137
+ throw new Error(message);
138
+ });
139
+ return Promise.race([promise, timeoutPromise]);
140
+ }
141
+ function stripQuery(url) {
142
+ try {
143
+ const parsed = new URL(url);
144
+ return `${parsed.origin}${parsed.pathname}`;
145
+ }
146
+ catch {
147
+ return url;
148
+ }
149
+ }
150
+ function sleep(ms) {
151
+ return new Promise((resolve) => setTimeout(resolve, ms));
152
+ }
@@ -0,0 +1,24 @@
1
+ export type CaptchaType = 'recaptcha-v2' | 'hcaptcha' | 'turnstile';
2
+ export type CaptchaSolverConfig = {
3
+ apiKey: string;
4
+ apiUrl?: string;
5
+ requestTimeoutMs?: number;
6
+ taskPollIntervalMs?: number;
7
+ taskTimeoutMs?: number;
8
+ };
9
+ /**
10
+ * Backend CAPTCHA gateway client. No direct provider keys are used in CLI.
11
+ */
12
+ export declare class CaptchaSolver {
13
+ private readonly apiKey;
14
+ private readonly apiUrl;
15
+ private readonly requestTimeoutMs;
16
+ private readonly taskPollIntervalMs;
17
+ private readonly taskTimeoutMs;
18
+ constructor(config: CaptchaSolverConfig);
19
+ solve(type: CaptchaType, siteKey: string, pageUrl: string): Promise<string>;
20
+ private createTask;
21
+ private getTaskStatus;
22
+ private request;
23
+ }
24
+ //# sourceMappingURL=captcha-solver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captcha-solver.d.ts","sourceRoot":"","sources":["../../src/solver/captcha-solver.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,CAAC;AAEpE,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAoBF;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,MAAM,EAAE,mBAAmB;IAejC,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAkBnE,UAAU;YAYV,aAAa;YAOb,OAAO;CAsCtB"}