leapfrog-mcp 0.6.2 → 0.6.3

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 (57) hide show
  1. package/dist/adaptive-wait.d.ts +72 -0
  2. package/dist/adaptive-wait.js +695 -0
  3. package/dist/api-intelligence.d.ts +82 -0
  4. package/dist/api-intelligence.js +575 -0
  5. package/dist/captcha-solver.d.ts +26 -0
  6. package/dist/captcha-solver.js +547 -0
  7. package/dist/cdp-connector.d.ts +33 -0
  8. package/dist/cdp-connector.js +176 -0
  9. package/dist/consent-dismiss.d.ts +33 -0
  10. package/dist/consent-dismiss.js +358 -0
  11. package/dist/crash-recovery.d.ts +74 -0
  12. package/dist/crash-recovery.js +242 -0
  13. package/dist/domain-knowledge.d.ts +149 -0
  14. package/dist/domain-knowledge.js +449 -0
  15. package/dist/harness-intelligence.d.ts +65 -0
  16. package/dist/harness-intelligence.js +432 -0
  17. package/dist/humanize-fingerprint.d.ts +42 -0
  18. package/dist/humanize-fingerprint.js +161 -0
  19. package/dist/humanize-mouse.d.ts +95 -0
  20. package/dist/humanize-mouse.js +275 -0
  21. package/dist/humanize-pause.d.ts +48 -0
  22. package/dist/humanize-pause.js +111 -0
  23. package/dist/humanize-scroll.d.ts +67 -0
  24. package/dist/humanize-scroll.js +185 -0
  25. package/dist/humanize-typing.d.ts +60 -0
  26. package/dist/humanize-typing.js +258 -0
  27. package/dist/humanize-utils.d.ts +62 -0
  28. package/dist/humanize-utils.js +100 -0
  29. package/dist/intervention.d.ts +65 -0
  30. package/dist/intervention.js +591 -0
  31. package/dist/logger.d.ts +13 -0
  32. package/dist/logger.js +47 -0
  33. package/dist/network-intelligence.d.ts +70 -0
  34. package/dist/network-intelligence.js +424 -0
  35. package/dist/page-classifier.d.ts +33 -0
  36. package/dist/page-classifier.js +1000 -0
  37. package/dist/paginate.d.ts +42 -0
  38. package/dist/paginate.js +693 -0
  39. package/dist/recording.d.ts +72 -0
  40. package/dist/recording.js +934 -0
  41. package/dist/script-executor.d.ts +31 -0
  42. package/dist/script-executor.js +249 -0
  43. package/dist/session-hud.d.ts +20 -0
  44. package/dist/session-hud.js +134 -0
  45. package/dist/snapshot-differ.d.ts +26 -0
  46. package/dist/snapshot-differ.js +225 -0
  47. package/dist/ssrf.d.ts +28 -0
  48. package/dist/ssrf.js +290 -0
  49. package/dist/stealth-audit.d.ts +27 -0
  50. package/dist/stealth-audit.js +719 -0
  51. package/dist/stealth.d.ts +195 -0
  52. package/dist/stealth.js +1157 -0
  53. package/dist/tab-manager.d.ts +14 -0
  54. package/dist/tab-manager.js +306 -0
  55. package/dist/tiles-coordinator.d.ts +106 -0
  56. package/dist/tiles-coordinator.js +358 -0
  57. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ export type InterventionType = 'captcha' | 'login' | 'oauth' | 'challenge' | 'manual';
2
+ export interface InterventionEvent {
3
+ type: InterventionType;
4
+ reason: string;
5
+ elementSelector?: string;
6
+ timestamp: number;
7
+ }
8
+ /**
9
+ * Returns JS to inject via page.addInitScript(). Sets up a MutationObserver
10
+ * and a 2s periodic check that detects CAPTCHAs, login walls, OAuth redirects,
11
+ * and Cloudflare challenges. When detected, sets window.__leapfrog_intervention.
12
+ */
13
+ export declare function getDetectionInitScript(): string;
14
+ /**
15
+ * Returns JS for page.evaluate() that runs all detection checks immediately
16
+ * and returns the result (or null if nothing detected).
17
+ */
18
+ export declare function getDetectionCheckScript(): string;
19
+ /**
20
+ * Returns JS to inject the @..@ fullscreen overlay with a reason message
21
+ * and a "Done" button. The overlay uses z-index 2147483647 (max 32-bit).
22
+ */
23
+ export declare function getOverlayScript(reason: string): string;
24
+ /**
25
+ * Returns JS to remove the overlay without marking as resolved.
26
+ * Useful when the caller wants to dismiss programmatically.
27
+ */
28
+ export declare function getDismissScript(): string;
29
+ /**
30
+ * Returns JS for page.evaluate() that checks if the user clicked "Done".
31
+ */
32
+ export declare function getResolutionCheckScript(): string;
33
+ /**
34
+ * Returns JS that requests fullscreen via a callback the caller wires to CDP.
35
+ * The actual CDP call (Browser.setWindowBounds) happens in index.ts, not here.
36
+ */
37
+ export declare function getFullscreenScript(): string;
38
+ /**
39
+ * Returns JS to detect the PerimeterX "Press & Hold" challenge (#px-captcha).
40
+ * Evaluates to { detected: boolean, bounds?: { x, y, width, height } }.
41
+ */
42
+ export declare function getPressAndHoldDetectScript(): string;
43
+ export interface PressAndHoldBounds {
44
+ x: number;
45
+ y: number;
46
+ width: number;
47
+ height: number;
48
+ }
49
+ export interface PressAndHoldDetection {
50
+ detected: boolean;
51
+ bounds?: PressAndHoldBounds;
52
+ }
53
+ /**
54
+ * Solve a PerimeterX "Press & Hold" challenge by moving the mouse to the
55
+ * element center, holding down for ~8 seconds, then releasing.
56
+ *
57
+ * This is a proven technique — tested successfully on Bath & Body Works.
58
+ * The caller passes the Playwright Page and the detected bounds.
59
+ */
60
+ export declare function solvePressAndHold(page: import("playwright-core").Page, bounds: PressAndHoldBounds): Promise<boolean>;
61
+ /**
62
+ * Parse and validate raw output from page.evaluate() of the detection scripts.
63
+ * Returns a typed InterventionEvent or null if the input is invalid / empty.
64
+ */
65
+ export declare function parseDetectionResult(raw: unknown): InterventionEvent | null;
@@ -0,0 +1,591 @@
1
+ // ─── Human Intervention Detection & Overlay ──────────────────────────────
2
+ //
3
+ // Detects when a browser page needs human intervention (CAPTCHAs, login walls,
4
+ // OAuth redirects, Cloudflare challenges) and provides a fullscreen @..@ overlay
5
+ // to guide the user.
6
+ //
7
+ // All functions return JavaScript strings for page.evaluate() or addInitScript().
8
+ // No DOM manipulation happens in Node — everything runs in the browser context.
9
+ // ─── Detection Init Script (MutationObserver) ─────────────────────────────
10
+ /**
11
+ * Returns JS to inject via page.addInitScript(). Sets up a MutationObserver
12
+ * and a 2s periodic check that detects CAPTCHAs, login walls, OAuth redirects,
13
+ * and Cloudflare challenges. When detected, sets window.__leapfrog_intervention.
14
+ */
15
+ export function getDetectionInitScript() {
16
+ return `(() => {
17
+ if (window.__leapfrog_detection_active) return;
18
+ window.__leapfrog_detection_active = true;
19
+ window.__leapfrog_intervention = null;
20
+ window.__leapfrog_intervention_resolved = false;
21
+
22
+ const CAPTCHA_IFRAME_PATTERNS = [
23
+ 'hcaptcha',
24
+ 'challenges.cloudflare.com'
25
+ ];
26
+
27
+ const RECAPTCHA_CHALLENGE_PATTERNS = [
28
+ 'recaptcha/api2/anchor',
29
+ 'recaptcha/api2/bframe'
30
+ ];
31
+
32
+ const TEXT_PATTERNS = [
33
+ /verify.*(human|not.*robot)/i,
34
+ /complete.*captcha/i,
35
+ /security.*check/i
36
+ ];
37
+
38
+ const CHALLENGE_SELECTORS = [
39
+ 'div#challenge-running',
40
+ 'div.cf-browser-verification'
41
+ ];
42
+
43
+ const OAUTH_URL_PATTERNS = [
44
+ '/oauth/',
45
+ '/authorize',
46
+ '/login/oauth'
47
+ ];
48
+
49
+ function setIntervention(type, reason, selector) {
50
+ if (window.__leapfrog_intervention) return;
51
+ window.__leapfrog_intervention = {
52
+ type: type,
53
+ reason: reason,
54
+ elementSelector: selector || null,
55
+ timestamp: Date.now()
56
+ };
57
+ }
58
+
59
+ function isIframeVisible(iframe) {
60
+ return iframe.offsetWidth > 0 && iframe.offsetHeight > 0;
61
+ }
62
+
63
+ function isInvisibleRecaptcha(src) {
64
+ try {
65
+ const url = new URL(src, window.location.href);
66
+ if (url.searchParams.get('size') === 'invisible') return true;
67
+ } catch (e) {}
68
+ return false;
69
+ }
70
+
71
+ function checkCaptchaIframes() {
72
+ const iframes = document.querySelectorAll('iframe[src]');
73
+ for (const iframe of iframes) {
74
+ const src = iframe.getAttribute('src') || '';
75
+ // Check non-recaptcha patterns (hcaptcha, cloudflare)
76
+ for (const pattern of CAPTCHA_IFRAME_PATTERNS) {
77
+ if (src.includes(pattern) && isIframeVisible(iframe)) {
78
+ setIntervention('captcha', 'CAPTCHA detected', 'iframe[src*="' + pattern + '"]');
79
+ return true;
80
+ }
81
+ }
82
+ // Check recaptcha — only match actual challenge iframes, not scoring-only
83
+ for (const pattern of RECAPTCHA_CHALLENGE_PATTERNS) {
84
+ if (src.includes(pattern) && isIframeVisible(iframe) && !isInvisibleRecaptcha(src)) {
85
+ setIntervention('captcha', 'CAPTCHA detected', 'iframe[src*="' + pattern + '"]');
86
+ return true;
87
+ }
88
+ }
89
+ }
90
+ return false;
91
+ }
92
+
93
+ function checkTextPatterns() {
94
+ const body = document.body;
95
+ if (!body) return false;
96
+ const text = body.innerText || '';
97
+ for (const pattern of TEXT_PATTERNS) {
98
+ if (pattern.test(text)) {
99
+ setIntervention('captcha', 'Human verification text detected');
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+
106
+ function checkChallengeElements() {
107
+ for (const sel of CHALLENGE_SELECTORS) {
108
+ if (document.querySelector(sel)) {
109
+ setIntervention('challenge', 'Cloudflare challenge detected', sel);
110
+ return true;
111
+ }
112
+ }
113
+ return false;
114
+ }
115
+
116
+ function checkLoginForms() {
117
+ const passwordInputs = document.querySelectorAll('form input[type="password"]');
118
+ if (passwordInputs.length > 0) {
119
+ const url = window.location.href.toLowerCase();
120
+ const isLoginPage = url.includes('/login') || url.includes('/signin') || url.includes('/sign-in') || url.includes('/auth');
121
+ if (!isLoginPage) {
122
+ setIntervention('login', 'Unexpected login form detected', 'input[type="password"]');
123
+ return true;
124
+ }
125
+ }
126
+ return false;
127
+ }
128
+
129
+ function checkOAuthRedirects() {
130
+ const url = window.location.href;
131
+ for (const pattern of OAUTH_URL_PATTERNS) {
132
+ if (url.includes(pattern)) {
133
+ setIntervention('oauth', 'OAuth redirect detected');
134
+ return true;
135
+ }
136
+ }
137
+ return false;
138
+ }
139
+
140
+ function runAllChecks() {
141
+ if (window.__leapfrog_intervention) return;
142
+ checkCaptchaIframes() ||
143
+ checkChallengeElements() ||
144
+ checkOAuthRedirects() ||
145
+ checkLoginForms() ||
146
+ checkTextPatterns();
147
+ }
148
+
149
+ // Periodic fallback check every 2s
150
+ const interval = setInterval(() => {
151
+ if (window.__leapfrog_intervention_resolved) {
152
+ clearInterval(interval);
153
+ return;
154
+ }
155
+ runAllChecks();
156
+ }, 2000);
157
+
158
+ function checkIframeSrc(iframe) {
159
+ const src = iframe.getAttribute('src') || '';
160
+ for (const pattern of CAPTCHA_IFRAME_PATTERNS) {
161
+ if (src.includes(pattern) && isIframeVisible(iframe)) {
162
+ setIntervention('captcha', 'CAPTCHA detected', 'iframe[src*="' + pattern + '"]');
163
+ return true;
164
+ }
165
+ }
166
+ for (const pattern of RECAPTCHA_CHALLENGE_PATTERNS) {
167
+ if (src.includes(pattern) && isIframeVisible(iframe) && !isInvisibleRecaptcha(src)) {
168
+ setIntervention('captcha', 'CAPTCHA detected', 'iframe[src*="' + pattern + '"]');
169
+ return true;
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+
175
+ // MutationObserver for fast detection of new iframes / challenge elements
176
+ const observer = new MutationObserver((mutations) => {
177
+ if (window.__leapfrog_intervention) return;
178
+ for (const mutation of mutations) {
179
+ for (const node of mutation.addedNodes) {
180
+ if (!(node instanceof HTMLElement)) continue;
181
+ // Check if added node is a captcha iframe
182
+ if (node.tagName === 'IFRAME') {
183
+ if (checkIframeSrc(node)) return;
184
+ }
185
+ // Check if added node matches challenge selectors
186
+ for (const sel of CHALLENGE_SELECTORS) {
187
+ if (node.matches && node.matches(sel)) {
188
+ setIntervention('challenge', 'Cloudflare challenge detected', sel);
189
+ return;
190
+ }
191
+ }
192
+ // Check for captcha iframes inside added subtree
193
+ const nestedIframes = node.querySelectorAll ? node.querySelectorAll('iframe[src]') : [];
194
+ for (const iframe of nestedIframes) {
195
+ if (checkIframeSrc(iframe)) return;
196
+ }
197
+ }
198
+ }
199
+ });
200
+
201
+ if (document.body) {
202
+ observer.observe(document.body, { childList: true, subtree: true });
203
+ } else {
204
+ document.addEventListener('DOMContentLoaded', () => {
205
+ observer.observe(document.body, { childList: true, subtree: true });
206
+ });
207
+ }
208
+
209
+ // Initial sweep
210
+ runAllChecks();
211
+ })();`;
212
+ }
213
+ // ─── On-Demand Detection Check ────────────────────────────────────────────
214
+ /**
215
+ * Returns JS for page.evaluate() that runs all detection checks immediately
216
+ * and returns the result (or null if nothing detected).
217
+ */
218
+ export function getDetectionCheckScript() {
219
+ return `(() => {
220
+ // If already detected by init script, return it
221
+ if (window.__leapfrog_intervention) {
222
+ return window.__leapfrog_intervention;
223
+ }
224
+
225
+ const CAPTCHA_IFRAME_PATTERNS = [
226
+ 'hcaptcha',
227
+ 'challenges.cloudflare.com'
228
+ ];
229
+
230
+ const RECAPTCHA_CHALLENGE_PATTERNS = [
231
+ 'recaptcha/api2/anchor',
232
+ 'recaptcha/api2/bframe'
233
+ ];
234
+
235
+ const TEXT_PATTERNS = [
236
+ /verify.*(human|not.*robot)/i,
237
+ /complete.*captcha/i,
238
+ /security.*check/i
239
+ ];
240
+
241
+ const CHALLENGE_SELECTORS = [
242
+ 'div#challenge-running',
243
+ 'div.cf-browser-verification'
244
+ ];
245
+
246
+ const OAUTH_URL_PATTERNS = [
247
+ '/oauth/',
248
+ '/authorize',
249
+ '/login/oauth'
250
+ ];
251
+
252
+ function isInvisibleRecaptcha(src) {
253
+ try {
254
+ const u = new URL(src, window.location.href);
255
+ if (u.searchParams.get('size') === 'invisible') return true;
256
+ } catch (e) {}
257
+ return false;
258
+ }
259
+
260
+ // Check captcha iframes
261
+ const iframes = document.querySelectorAll('iframe[src]');
262
+ for (const iframe of iframes) {
263
+ const src = iframe.getAttribute('src') || '';
264
+ const visible = iframe.offsetWidth > 0 && iframe.offsetHeight > 0;
265
+ if (!visible) continue;
266
+ for (const pattern of CAPTCHA_IFRAME_PATTERNS) {
267
+ if (src.includes(pattern)) {
268
+ return { type: 'captcha', reason: 'CAPTCHA detected', elementSelector: 'iframe[src*="' + pattern + '"]', timestamp: Date.now() };
269
+ }
270
+ }
271
+ for (const pattern of RECAPTCHA_CHALLENGE_PATTERNS) {
272
+ if (src.includes(pattern) && !isInvisibleRecaptcha(src)) {
273
+ return { type: 'captcha', reason: 'CAPTCHA detected', elementSelector: 'iframe[src*="' + pattern + '"]', timestamp: Date.now() };
274
+ }
275
+ }
276
+ }
277
+
278
+ // Check challenge elements
279
+ for (const sel of CHALLENGE_SELECTORS) {
280
+ if (document.querySelector(sel)) {
281
+ return { type: 'challenge', reason: 'Cloudflare challenge detected', elementSelector: sel, timestamp: Date.now() };
282
+ }
283
+ }
284
+
285
+ // Check OAuth redirects
286
+ const url = window.location.href;
287
+ for (const pattern of OAUTH_URL_PATTERNS) {
288
+ if (url.includes(pattern)) {
289
+ return { type: 'oauth', reason: 'OAuth redirect detected', elementSelector: null, timestamp: Date.now() };
290
+ }
291
+ }
292
+
293
+ // Check login forms on unexpected pages
294
+ const passwordInputs = document.querySelectorAll('form input[type="password"]');
295
+ if (passwordInputs.length > 0) {
296
+ const lower = url.toLowerCase();
297
+ const isLoginPage = lower.includes('/login') || lower.includes('/signin') || lower.includes('/sign-in') || lower.includes('/auth');
298
+ if (!isLoginPage) {
299
+ return { type: 'login', reason: 'Unexpected login form detected', elementSelector: 'input[type="password"]', timestamp: Date.now() };
300
+ }
301
+ }
302
+
303
+ // Check text patterns
304
+ const body = document.body;
305
+ if (body) {
306
+ const text = body.innerText || '';
307
+ const patterns = [
308
+ /verify.*(human|not.*robot)/i,
309
+ /complete.*captcha/i,
310
+ /security.*check/i
311
+ ];
312
+ for (const p of patterns) {
313
+ if (p.test(text)) {
314
+ return { type: 'captcha', reason: 'Human verification text detected', elementSelector: null, timestamp: Date.now() };
315
+ }
316
+ }
317
+ }
318
+
319
+ return null;
320
+ })()`;
321
+ }
322
+ // ─── Overlay ──────────────────────────────────────────────────────────────
323
+ /**
324
+ * Returns JS to inject the @..@ fullscreen overlay with a reason message
325
+ * and a "Done" button. The overlay uses z-index 2147483647 (max 32-bit).
326
+ */
327
+ export function getOverlayScript(reason) {
328
+ // Escape the reason for safe embedding in JS string literal
329
+ const safeReason = reason.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n');
330
+ return `(() => {
331
+ // Remove existing overlay if any
332
+ const existing = document.getElementById('leapfrog-intervention-overlay');
333
+ if (existing) existing.remove();
334
+ const existingBar = document.getElementById('leapfrog-intervention-topbar');
335
+ if (existingBar) existingBar.remove();
336
+
337
+ // BUG-005: Prepend warning to document title for tab visibility
338
+ if (!document.title.startsWith('\\u26a0\\ufe0f NEEDS HUMAN')) {
339
+ document.title = '\\u26a0\\ufe0f NEEDS HUMAN \\u2014 ' + document.title;
340
+ }
341
+
342
+ // BUG-006: Persistent red top bar visible at any tile size
343
+ const topBar = document.createElement('div');
344
+ topBar.id = 'leapfrog-intervention-topbar';
345
+ topBar.setAttribute('data-leapfrog', 'true');
346
+ topBar.textContent = '\\u26a0\\ufe0f NEEDS HUMAN';
347
+ topBar.style.cssText = [
348
+ 'position: fixed',
349
+ 'top: 0',
350
+ 'left: 0',
351
+ 'width: 100%',
352
+ 'height: 32px',
353
+ 'background: #ef4444',
354
+ 'color: white',
355
+ 'z-index: 2147483647',
356
+ 'font-size: 14px',
357
+ 'text-align: center',
358
+ 'line-height: 32px',
359
+ 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
360
+ 'font-weight: 600',
361
+ 'letter-spacing: 0.02em',
362
+ 'pointer-events: none'
363
+ ].join('; ');
364
+ document.body.appendChild(topBar);
365
+ document.body.style.marginTop = '32px';
366
+
367
+ const overlay = document.createElement('div');
368
+ overlay.id = 'leapfrog-intervention-overlay';
369
+ overlay.setAttribute('data-leapfrog', 'true');
370
+ overlay.style.cssText = [
371
+ 'position: fixed',
372
+ 'top: 0',
373
+ 'left: 0',
374
+ 'width: 100vw',
375
+ 'height: 100vh',
376
+ 'background: rgba(0, 0, 0, 0.85)',
377
+ 'z-index: 2147483647',
378
+ 'display: flex',
379
+ 'flex-direction: column',
380
+ 'align-items: center',
381
+ 'justify-content: center',
382
+ 'font-family: "SF Mono", "Fira Code", "Cascadia Code", "JetBrains Mono", Menlo, Monaco, "Courier New", monospace',
383
+ 'color: #22c55e',
384
+ 'pointer-events: auto'
385
+ ].join('; ');
386
+
387
+ // Eyes
388
+ const eyes = document.createElement('div');
389
+ eyes.setAttribute('data-leapfrog', 'true');
390
+ eyes.textContent = '@..@';
391
+ eyes.style.cssText = [
392
+ 'font-size: 64px',
393
+ 'line-height: 1',
394
+ 'letter-spacing: 0.05em',
395
+ 'margin-bottom: 24px',
396
+ 'user-select: none',
397
+ 'text-shadow: 0 0 20px rgba(34, 197, 94, 0.4)'
398
+ ].join('; ');
399
+
400
+ // Arrow pointing down (CSS triangle)
401
+ const arrow = document.createElement('div');
402
+ arrow.setAttribute('data-leapfrog', 'true');
403
+ arrow.style.cssText = [
404
+ 'width: 0',
405
+ 'height: 0',
406
+ 'border-left: 16px solid transparent',
407
+ 'border-right: 16px solid transparent',
408
+ 'border-top: 24px solid #22c55e',
409
+ 'margin-bottom: 24px'
410
+ ].join('; ');
411
+
412
+ // Reason text
413
+ const reasonEl = document.createElement('div');
414
+ reasonEl.setAttribute('data-leapfrog', 'true');
415
+ reasonEl.textContent = '${safeReason}';
416
+ reasonEl.style.cssText = [
417
+ 'font-size: 20px',
418
+ 'color: #e2e8f0',
419
+ 'text-align: center',
420
+ 'max-width: 500px',
421
+ 'line-height: 1.5',
422
+ 'margin-bottom: 40px'
423
+ ].join('; ');
424
+
425
+ // Done button
426
+ const btn = document.createElement('button');
427
+ btn.id = 'leapfrog-intervention-done';
428
+ btn.setAttribute('data-leapfrog', 'true');
429
+ btn.textContent = 'Done';
430
+ btn.style.cssText = [
431
+ 'font-family: inherit',
432
+ 'font-size: 18px',
433
+ 'font-weight: 600',
434
+ 'color: #000',
435
+ 'background: #22c55e',
436
+ 'border: none',
437
+ 'border-radius: 12px',
438
+ 'padding: 14px 48px',
439
+ 'cursor: pointer',
440
+ 'transition: background 0.15s ease, transform 0.1s ease',
441
+ 'outline: none'
442
+ ].join('; ');
443
+
444
+ btn.addEventListener('mouseenter', () => {
445
+ btn.style.background = '#16a34a';
446
+ });
447
+ btn.addEventListener('mouseleave', () => {
448
+ btn.style.background = '#22c55e';
449
+ });
450
+ btn.addEventListener('mousedown', () => {
451
+ btn.style.transform = 'scale(0.97)';
452
+ });
453
+ btn.addEventListener('mouseup', () => {
454
+ btn.style.transform = 'scale(1)';
455
+ });
456
+
457
+ btn.addEventListener('click', () => {
458
+ window.__leapfrog_intervention_resolved = true;
459
+ window.__leapfrog_intervention = null;
460
+ overlay.remove();
461
+ // Clean up top bar
462
+ const bar = document.getElementById('leapfrog-intervention-topbar');
463
+ if (bar) bar.remove();
464
+ document.body.style.marginTop = '';
465
+ // Restore document title
466
+ document.title = document.title.replace(/^\\u26a0\\ufe0f NEEDS HUMAN \\u2014 /, '');
467
+ });
468
+
469
+ overlay.appendChild(eyes);
470
+ overlay.appendChild(arrow);
471
+ overlay.appendChild(reasonEl);
472
+ overlay.appendChild(btn);
473
+ document.body.appendChild(overlay);
474
+ })();`;
475
+ }
476
+ // ─── Dismiss Overlay ──────────────────────────────────────────────────────
477
+ /**
478
+ * Returns JS to remove the overlay without marking as resolved.
479
+ * Useful when the caller wants to dismiss programmatically.
480
+ */
481
+ export function getDismissScript() {
482
+ return `(() => {
483
+ const overlay = document.getElementById('leapfrog-intervention-overlay');
484
+ if (overlay) overlay.remove();
485
+ const bar = document.getElementById('leapfrog-intervention-topbar');
486
+ if (bar) bar.remove();
487
+ document.body.style.marginTop = '';
488
+ document.title = document.title.replace(/^\\u26a0\\ufe0f NEEDS HUMAN \\u2014 /, '');
489
+ })()`;
490
+ }
491
+ // ─── Resolution Check ─────────────────────────────────────────────────────
492
+ /**
493
+ * Returns JS for page.evaluate() that checks if the user clicked "Done".
494
+ */
495
+ export function getResolutionCheckScript() {
496
+ return `(() => {
497
+ return !!window.__leapfrog_intervention_resolved;
498
+ })()`;
499
+ }
500
+ // ─── Fullscreen Takeover ──────────────────────────────────────────────────
501
+ /**
502
+ * Returns JS that requests fullscreen via a callback the caller wires to CDP.
503
+ * The actual CDP call (Browser.setWindowBounds) happens in index.ts, not here.
504
+ */
505
+ export function getFullscreenScript() {
506
+ return `(() => {
507
+ if (typeof window.__leapfrog_requestFullscreen === 'function') {
508
+ window.__leapfrog_requestFullscreen();
509
+ }
510
+ })()`;
511
+ }
512
+ // ─── Press-and-Hold Solver (PerimeterX) ──────────────────────────────────
513
+ /**
514
+ * Returns JS to detect the PerimeterX "Press & Hold" challenge (#px-captcha).
515
+ * Evaluates to { detected: boolean, bounds?: { x, y, width, height } }.
516
+ */
517
+ export function getPressAndHoldDetectScript() {
518
+ return `(() => {
519
+ const el = document.querySelector('#px-captcha');
520
+ if (!el) return { detected: false };
521
+ const rect = el.getBoundingClientRect();
522
+ if (rect.width === 0 || rect.height === 0) return { detected: false };
523
+ return {
524
+ detected: true,
525
+ bounds: {
526
+ x: Math.round(rect.x + rect.width / 2),
527
+ y: Math.round(rect.y + rect.height / 2),
528
+ width: Math.round(rect.width),
529
+ height: Math.round(rect.height),
530
+ },
531
+ };
532
+ })()`;
533
+ }
534
+ /**
535
+ * Solve a PerimeterX "Press & Hold" challenge by moving the mouse to the
536
+ * element center, holding down for ~8 seconds, then releasing.
537
+ *
538
+ * This is a proven technique — tested successfully on Bath & Body Works.
539
+ * The caller passes the Playwright Page and the detected bounds.
540
+ */
541
+ export async function solvePressAndHold(page, bounds) {
542
+ try {
543
+ // Move to element center with a small random offset
544
+ const offsetX = Math.floor(Math.random() * 10) - 5;
545
+ const offsetY = Math.floor(Math.random() * 10) - 5;
546
+ await page.mouse.move(bounds.x + offsetX, bounds.y + offsetY, { steps: 5 });
547
+ // Press and hold for 8-10 seconds (randomized to avoid fingerprinting)
548
+ await page.mouse.down();
549
+ const holdTime = 8000 + Math.floor(Math.random() * 2000);
550
+ await new Promise(r => setTimeout(r, holdTime));
551
+ await page.mouse.up();
552
+ // Wait for challenge to resolve
553
+ await new Promise(r => setTimeout(r, 2000));
554
+ // Check if the challenge element disappeared
555
+ const resolved = await page.evaluate(() => {
556
+ const el = document.querySelector('#px-captcha');
557
+ if (!el)
558
+ return true;
559
+ const rect = el.getBoundingClientRect();
560
+ return rect.width === 0 || rect.height === 0;
561
+ });
562
+ return resolved;
563
+ }
564
+ catch {
565
+ return false;
566
+ }
567
+ }
568
+ // ─── Parse Detection Result ───────────────────────────────────────────────
569
+ const VALID_TYPES = new Set(['captcha', 'login', 'oauth', 'challenge', 'manual']);
570
+ /**
571
+ * Parse and validate raw output from page.evaluate() of the detection scripts.
572
+ * Returns a typed InterventionEvent or null if the input is invalid / empty.
573
+ */
574
+ export function parseDetectionResult(raw) {
575
+ if (!raw || typeof raw !== 'object')
576
+ return null;
577
+ const obj = raw;
578
+ if (typeof obj.type !== 'string' || !VALID_TYPES.has(obj.type)) {
579
+ return null;
580
+ }
581
+ if (typeof obj.reason !== 'string')
582
+ return null;
583
+ if (typeof obj.timestamp !== 'number')
584
+ return null;
585
+ return {
586
+ type: obj.type,
587
+ reason: obj.reason,
588
+ elementSelector: typeof obj.elementSelector === 'string' ? obj.elementSelector : undefined,
589
+ timestamp: obj.timestamp,
590
+ };
591
+ }
@@ -0,0 +1,13 @@
1
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ declare class Logger {
3
+ private minLevel;
4
+ constructor();
5
+ private log;
6
+ debug(event: string, data?: Record<string, unknown>): void;
7
+ info(event: string, data?: Record<string, unknown>): void;
8
+ warn(event: string, data?: Record<string, unknown>): void;
9
+ error(event: string, data?: Record<string, unknown>): void;
10
+ }
11
+ export declare const logger: Logger;
12
+ export type { LogLevel };
13
+ export default logger;