From 99308b2e21b77f546585888c35301ea31d5aae5d Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Sun, 25 Jan 2026 10:12:23 -0800 Subject: [PATCH] Pagecontrol hook for evaluateJs --- src/agent-runtime.ts | 7 +++ src/captcha/types.ts | 7 +++ src/extension/injected_api.js | 35 ++++++++++-- tests/agent-runtime-captcha-context.test.ts | 61 +++++++++++++++++++++ 4 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 tests/agent-runtime-captcha-context.test.ts diff --git a/src/agent-runtime.ts b/src/agent-runtime.ts index 2cc911c..c624d4d 100644 --- a/src/agent-runtime.ts +++ b/src/agent-runtime.ts @@ -809,6 +809,13 @@ export class AgentRuntime { url: snapshot.url, source, captcha: snapshot.diagnostics?.captcha ?? null, + evaluateJs: async (code: string) => { + const result = await this.evaluateJs({ code }); + if (!result.ok) { + throw new Error(result.error ?? 'evaluateJs failed'); + } + return result.value; + }, }; } diff --git a/src/captcha/types.ts b/src/captcha/types.ts index e57b46a..eb12202 100644 --- a/src/captcha/types.ts +++ b/src/captcha/types.ts @@ -16,6 +16,8 @@ export interface CaptchaContext { snapshotPath?: string; liveSessionUrl?: string; meta?: Record; + evaluateJs?: (code: string) => Promise; + pageControl?: PageControlHook; } export interface CaptchaResolution { @@ -26,6 +28,11 @@ export interface CaptchaResolution { pollMs?: number; } +export interface PageControlHook { + evaluateJs: (code: string) => Promise; + getUrl?: () => Promise; +} + export type CaptchaHandler = ( ctx: CaptchaContext ) => CaptchaResolution | Promise; diff --git a/src/extension/injected_api.js b/src/extension/injected_api.js index c1e305e..c959b5b 100644 --- a/src/extension/injected_api.js +++ b/src/extension/injected_api.js @@ -99,14 +99,36 @@ arkose: 0, awswaf: 0 }; + function isVisibleElement(el) { + try { + if (!el) return !1; + const style = window.getComputedStyle(el); + if ("none" === style.display || "hidden" === style.visibility) return !1; + const opacity = parseFloat(style.opacity || "1"); + if (!Number.isNaN(opacity) && opacity <= .01) return !1; + if (!el.getClientRects || 0 === el.getClientRects().length) return !1; + const rect = el.getBoundingClientRect(); + if (rect.width < 8 || rect.height < 8) return !1; + const vw = window.innerWidth || document.documentElement.clientWidth || 0; + const vh = window.innerHeight || document.documentElement.clientHeight || 0; + if (vw && vh) { + if (rect.bottom <= 0 || rect.right <= 0 || rect.top >= vh || rect.left >= vw) return !1; + } + return !0; + } catch (e) { + return !1; + } + } try { const iframes = document.querySelectorAll("iframe"); for (const iframe of iframes) { const src = iframe.getAttribute("src") || "", title = iframe.getAttribute("title") || ""; - if (src) for (const [provider, hints] of Object.entries(CAPTCHA_IFRAME_HINTS)) matchHints(src, hints) && (hasIframeHit = !0, - providerSignals[provider] += 1, addEvidence(evidence.iframe_src_hits, truncateText(src, 120))); - if (title && matchHints(title, [ "captcha", "recaptcha" ]) && (hasContainerHit = !0, - addEvidence(evidence.selector_hits, 'iframe[title*="captcha"]')), evidence.iframe_src_hits.length >= 5) break; + if (src) for (const [provider, hints] of Object.entries(CAPTCHA_IFRAME_HINTS)) if (matchHints(src, hints)) { + addEvidence(evidence.iframe_src_hits, truncateText(src, 120)); + isVisibleElement(iframe) && (hasIframeHit = !0, providerSignals[provider] += 1); + } + if (title && matchHints(title, [ "captcha", "recaptcha" ]) && (addEvidence(evidence.selector_hits, 'iframe[title*="captcha"]'), + isVisibleElement(iframe) && (hasContainerHit = !0)), evidence.iframe_src_hits.length >= 5) break; } } catch (e) {} try { @@ -121,8 +143,9 @@ } } catch (e) {} for (const {selector: selector, provider: provider} of CAPTCHA_CONTAINER_SELECTORS) try { - document.querySelector(selector) && (hasContainerHit = !0, addEvidence(evidence.selector_hits, selector), - "unknown" !== provider && (providerSignals[provider] += 1)); + const el = document.querySelector(selector); + el && (addEvidence(evidence.selector_hits, selector), isVisibleElement(el) && (hasContainerHit = !0, + "unknown" !== provider && (providerSignals[provider] += 1))); } catch (e) {} const textSnippet = function() { try { diff --git a/tests/agent-runtime-captcha-context.test.ts b/tests/agent-runtime-captcha-context.test.ts new file mode 100644 index 0000000..f23c5e8 --- /dev/null +++ b/tests/agent-runtime-captcha-context.test.ts @@ -0,0 +1,61 @@ +import { AgentRuntime } from '../src/agent-runtime'; +import { Tracer } from '../src/tracing/tracer'; +import { TraceSink } from '../src/tracing/sink'; +import { CaptchaDiagnostics, Snapshot } from '../src/types'; +import { MockPage } from './mocks/browser-mock'; + +class MockSink extends TraceSink { + public events: any[] = []; + emit(event: Record): void { + this.events.push(event); + } + async close(): Promise { + // no-op + } + getSinkType(): string { + return 'MockSink'; + } +} + +describe('AgentRuntime captcha context', () => { + it('exposes evaluateJs hook to captcha handlers', async () => { + const sink = new MockSink(); + const tracer = new Tracer('test-run', sink); + const page = new MockPage('https://example.com') as any; + page.evaluate = jest.fn().mockResolvedValue('ok'); + + const captcha: CaptchaDiagnostics = { + detected: true, + confidence: 0.9, + provider_hint: 'recaptcha', + evidence: { + iframe_src_hits: ['https://www.google.com/recaptcha/api2/anchor'], + selector_hits: [], + text_hits: [], + url_hits: [], + }, + }; + + const snapshot: Snapshot = { + status: 'success', + url: 'https://example.com', + elements: [], + diagnostics: { captcha }, + timestamp: 't1', + }; + + const browserLike = { + snapshot: async () => snapshot, + }; + + const runtime = new AgentRuntime(browserLike as any, page as any, tracer); + runtime.beginStep('captcha_test'); + + const ctx = (runtime as any).buildCaptchaContext(snapshot, 'gateway'); + expect(typeof ctx.evaluateJs).toBe('function'); + + const result = await ctx.evaluateJs('1+1'); + expect(result).toBe('ok'); + expect(page.evaluate).toHaveBeenCalled(); + }); +});