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.
- package/dist/adaptive-wait.d.ts +72 -0
- package/dist/adaptive-wait.js +695 -0
- package/dist/api-intelligence.d.ts +82 -0
- package/dist/api-intelligence.js +575 -0
- package/dist/captcha-solver.d.ts +26 -0
- package/dist/captcha-solver.js +547 -0
- package/dist/cdp-connector.d.ts +33 -0
- package/dist/cdp-connector.js +176 -0
- package/dist/consent-dismiss.d.ts +33 -0
- package/dist/consent-dismiss.js +358 -0
- package/dist/crash-recovery.d.ts +74 -0
- package/dist/crash-recovery.js +242 -0
- package/dist/domain-knowledge.d.ts +149 -0
- package/dist/domain-knowledge.js +449 -0
- package/dist/harness-intelligence.d.ts +65 -0
- package/dist/harness-intelligence.js +432 -0
- package/dist/humanize-fingerprint.d.ts +42 -0
- package/dist/humanize-fingerprint.js +161 -0
- package/dist/humanize-mouse.d.ts +95 -0
- package/dist/humanize-mouse.js +275 -0
- package/dist/humanize-pause.d.ts +48 -0
- package/dist/humanize-pause.js +111 -0
- package/dist/humanize-scroll.d.ts +67 -0
- package/dist/humanize-scroll.js +185 -0
- package/dist/humanize-typing.d.ts +60 -0
- package/dist/humanize-typing.js +258 -0
- package/dist/humanize-utils.d.ts +62 -0
- package/dist/humanize-utils.js +100 -0
- package/dist/intervention.d.ts +65 -0
- package/dist/intervention.js +591 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +47 -0
- package/dist/network-intelligence.d.ts +70 -0
- package/dist/network-intelligence.js +424 -0
- package/dist/page-classifier.d.ts +33 -0
- package/dist/page-classifier.js +1000 -0
- package/dist/paginate.d.ts +42 -0
- package/dist/paginate.js +693 -0
- package/dist/recording.d.ts +72 -0
- package/dist/recording.js +934 -0
- package/dist/script-executor.d.ts +31 -0
- package/dist/script-executor.js +249 -0
- package/dist/session-hud.d.ts +20 -0
- package/dist/session-hud.js +134 -0
- package/dist/snapshot-differ.d.ts +26 -0
- package/dist/snapshot-differ.js +225 -0
- package/dist/ssrf.d.ts +28 -0
- package/dist/ssrf.js +290 -0
- package/dist/stealth-audit.d.ts +27 -0
- package/dist/stealth-audit.js +719 -0
- package/dist/stealth.d.ts +195 -0
- package/dist/stealth.js +1157 -0
- package/dist/tab-manager.d.ts +14 -0
- package/dist/tab-manager.js +306 -0
- package/dist/tiles-coordinator.d.ts +106 -0
- package/dist/tiles-coordinator.js +358 -0
- 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
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -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;
|