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 f4728d4..8d50639 100644 --- a/src/extension/injected_api.js +++ b/src/extension/injected_api.js @@ -115,6 +115,26 @@ 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) { 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(); + }); +});