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,176 @@
|
|
|
1
|
+
// ─── CDP Connector ─────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Connects Leapfrog to an already-running Chrome instance via CDP.
|
|
4
|
+
// User launches Chrome with --remote-debugging-port=9222, and Leapfrog
|
|
5
|
+
// attaches to use real cookies, extensions, and OAuth tokens.
|
|
6
|
+
import { chromium } from "playwright-core";
|
|
7
|
+
import * as fs from "fs/promises";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import * as os from "os";
|
|
10
|
+
import * as http from "http";
|
|
11
|
+
import { logger } from "./logger.js";
|
|
12
|
+
function getPortFileEntries() {
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
const platform = os.platform();
|
|
15
|
+
if (platform === "darwin") {
|
|
16
|
+
const base = path.join(home, "Library", "Application Support");
|
|
17
|
+
return [
|
|
18
|
+
{ path: path.join(base, "Google", "Chrome", "DevToolsActivePort"), browser: "Chrome" },
|
|
19
|
+
{ path: path.join(base, "Google", "Chrome Canary", "DevToolsActivePort"), browser: "Canary" },
|
|
20
|
+
{ path: path.join(base, "BraveSoftware", "Brave-Browser", "DevToolsActivePort"), browser: "Brave" },
|
|
21
|
+
{ path: path.join(base, "Chromium", "DevToolsActivePort"), browser: "Chromium" },
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
if (platform === "linux") {
|
|
25
|
+
return [
|
|
26
|
+
{ path: path.join(home, ".config", "google-chrome", "DevToolsActivePort"), browser: "Chrome" },
|
|
27
|
+
{ path: path.join(home, ".config", "google-chrome-canary", "DevToolsActivePort"), browser: "Canary" },
|
|
28
|
+
{ path: path.join(home, ".config", "BraveSoftware", "Brave-Browser", "DevToolsActivePort"), browser: "Brave" },
|
|
29
|
+
{ path: path.join(home, ".config", "chromium", "DevToolsActivePort"), browser: "Chromium" },
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
if (platform === "win32") {
|
|
33
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local");
|
|
34
|
+
return [
|
|
35
|
+
{ path: path.join(localAppData, "Google", "Chrome", "User Data", "DevToolsActivePort"), browser: "Chrome" },
|
|
36
|
+
{ path: path.join(localAppData, "Google", "Chrome SxS", "User Data", "DevToolsActivePort"), browser: "Canary" },
|
|
37
|
+
{ path: path.join(localAppData, "BraveSoftware", "Brave-Browser", "User Data", "DevToolsActivePort"), browser: "Brave" },
|
|
38
|
+
{ path: path.join(localAppData, "Chromium", "User Data", "DevToolsActivePort"), browser: "Chromium" },
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
44
|
+
/** Parse DevToolsActivePort file: line 1 = port, line 2 = path */
|
|
45
|
+
export function parseDevToolsActivePort(contents) {
|
|
46
|
+
const lines = contents.trim().split("\n");
|
|
47
|
+
if (lines.length < 1)
|
|
48
|
+
return null;
|
|
49
|
+
const port = parseInt(lines[0].trim(), 10);
|
|
50
|
+
return Number.isFinite(port) && port > 0 && port < 65536 ? port : null;
|
|
51
|
+
}
|
|
52
|
+
/** Probe a single port via HTTP GET /json/version with a tight timeout */
|
|
53
|
+
export function probePort(port, timeoutMs = 500) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const req = http.get({ hostname: "127.0.0.1", port, path: "/json/version", timeout: timeoutMs }, (res) => {
|
|
56
|
+
let body = "";
|
|
57
|
+
res.on("data", (chunk) => { body += chunk.toString(); });
|
|
58
|
+
res.on("end", () => {
|
|
59
|
+
try {
|
|
60
|
+
const json = JSON.parse(body);
|
|
61
|
+
resolve(json.Browser ?? "Chrome");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
resolve(null);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
req.on("error", () => resolve(null));
|
|
69
|
+
req.on("timeout", () => { req.destroy(); resolve(null); });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// ─── CDP Connector ─────────────────────────────────────────────────────────
|
|
73
|
+
const SCAN_PORTS = [9222, 9223, 9224, 9225, 9226, 9227, 9228, 9229];
|
|
74
|
+
export class CdpConnector {
|
|
75
|
+
/**
|
|
76
|
+
* Discover available CDP endpoints on this machine.
|
|
77
|
+
* Checks env var, DevToolsActivePort files, and scans common ports in parallel.
|
|
78
|
+
* Total time target: <2s.
|
|
79
|
+
*/
|
|
80
|
+
static async discover() {
|
|
81
|
+
const results = [];
|
|
82
|
+
// 1. Environment variable — highest priority
|
|
83
|
+
const envEndpoint = process.env.LEAP_CDP_ENDPOINT;
|
|
84
|
+
if (envEndpoint) {
|
|
85
|
+
results.push({ endpoint: envEndpoint, browser: "Chrome", source: "env" });
|
|
86
|
+
}
|
|
87
|
+
// 2. Run port-file checks and port scans in parallel
|
|
88
|
+
const [portFileResults, portScanResults] = await Promise.all([
|
|
89
|
+
CdpConnector.discoverFromPortFiles(),
|
|
90
|
+
CdpConnector.discoverFromPortScan(),
|
|
91
|
+
]);
|
|
92
|
+
results.push(...portFileResults, ...portScanResults);
|
|
93
|
+
// Deduplicate by endpoint
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
const deduped = [];
|
|
96
|
+
for (const r of results) {
|
|
97
|
+
const key = r.endpoint.replace(/\/$/, "");
|
|
98
|
+
if (!seen.has(key)) {
|
|
99
|
+
seen.add(key);
|
|
100
|
+
deduped.push(r);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
logger.debug("cdp.discover", { found: deduped.length });
|
|
104
|
+
return deduped;
|
|
105
|
+
}
|
|
106
|
+
/** Check all known DevToolsActivePort file locations */
|
|
107
|
+
static async discoverFromPortFiles() {
|
|
108
|
+
const entries = getPortFileEntries();
|
|
109
|
+
const results = [];
|
|
110
|
+
const checks = entries.map(async (entry) => {
|
|
111
|
+
try {
|
|
112
|
+
const contents = await fs.readFile(entry.path, "utf-8");
|
|
113
|
+
const port = parseDevToolsActivePort(contents);
|
|
114
|
+
if (port) {
|
|
115
|
+
results.push({
|
|
116
|
+
endpoint: `http://localhost:${port}`,
|
|
117
|
+
browser: entry.browser,
|
|
118
|
+
source: "devtools-port-file",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// File doesn't exist or isn't readable — expected
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
await Promise.all(checks);
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
/** Scan common debug ports (9222-9229) for active CDP endpoints */
|
|
130
|
+
static async discoverFromPortScan() {
|
|
131
|
+
const results = [];
|
|
132
|
+
const probes = SCAN_PORTS.map(async (port) => {
|
|
133
|
+
const browserName = await probePort(port);
|
|
134
|
+
if (browserName) {
|
|
135
|
+
results.push({
|
|
136
|
+
endpoint: `http://localhost:${port}`,
|
|
137
|
+
browser: browserName,
|
|
138
|
+
source: "port-scan",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
await Promise.all(probes);
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
/** Connect to a CDP endpoint. Returns a Playwright Browser object. */
|
|
146
|
+
static async connect(endpoint) {
|
|
147
|
+
logger.info("cdp.connect", { endpoint });
|
|
148
|
+
try {
|
|
149
|
+
const browser = await chromium.connectOverCDP(endpoint);
|
|
150
|
+
logger.info("cdp.connected", { endpoint, contexts: browser.contexts().length });
|
|
151
|
+
return browser;
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
155
|
+
logger.error("cdp.connect_failed", { endpoint, error: msg });
|
|
156
|
+
throw new Error(`CDP connection failed to ${endpoint}: ${msg}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get the best available endpoint and connect.
|
|
161
|
+
* Priority: LEAP_CDP_ENDPOINT env > DevToolsActivePort file > port scan
|
|
162
|
+
*/
|
|
163
|
+
static async autoConnect() {
|
|
164
|
+
const results = await CdpConnector.discover();
|
|
165
|
+
if (results.length === 0) {
|
|
166
|
+
throw new Error("No CDP endpoint found. Launch Chrome with --remote-debugging-port=9222 " +
|
|
167
|
+
"or set LEAP_CDP_ENDPOINT environment variable.");
|
|
168
|
+
}
|
|
169
|
+
// Take the first (highest priority) result
|
|
170
|
+
const info = results[0];
|
|
171
|
+
logger.info("cdp.auto_connect", { endpoint: info.endpoint, browser: info.browser, source: info.source });
|
|
172
|
+
const browser = await CdpConnector.connect(info.endpoint);
|
|
173
|
+
return { browser, info };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export default CdpConnector;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ConsentFramework {
|
|
2
|
+
name: string;
|
|
3
|
+
selectors: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare const CONSENT_SELECTORS: ConsentFramework[];
|
|
6
|
+
export declare const TERMS_SELECTORS: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Returns JS to inject via page.evaluate() that auto-checks unchecked
|
|
9
|
+
* terms/privacy/TOS checkboxes. Only targets form checkboxes whose labels
|
|
10
|
+
* or names indicate legal agreements. Returns which selectors matched.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getTermsAutoCheckScript(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Returns JS to inject via page.addInitScript().
|
|
15
|
+
* After page load + 1.5s delay, attempts to dismiss consent dialogs.
|
|
16
|
+
* Sets up a MutationObserver for lazily-loaded banners (auto-disconnects at 10s).
|
|
17
|
+
*/
|
|
18
|
+
export declare function getConsentDismissScript(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Returns JS that checks if a consent dialog is currently visible.
|
|
21
|
+
* Evaluates to `{ detected: boolean, frameworks: string[] }`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getConsentDetectScript(): string;
|
|
24
|
+
/**
|
|
25
|
+
* Returns JS to manually trigger consent dismissal (for retry scenarios).
|
|
26
|
+
* Resets the dismissed flag so the function runs fresh.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getManualDismissScript(): string;
|
|
29
|
+
/**
|
|
30
|
+
* Returns JS to cache a successful selector for a domain.
|
|
31
|
+
* Used when the caller already knows what worked (e.g. from disk persistence).
|
|
32
|
+
*/
|
|
33
|
+
export declare function getCacheSelectorScript(domain: string, selector: string): string;
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// ─── Consent Dismiss ──────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Auto-detects and dismisses cookie/consent dialogs on web pages.
|
|
4
|
+
// Covers the top 10 consent frameworks plus generic text-matching fallback.
|
|
5
|
+
//
|
|
6
|
+
// All functions return JavaScript strings to inject via page.addInitScript()
|
|
7
|
+
// or page.evaluate(). This module has no side effects — the caller decides
|
|
8
|
+
// when and where to inject.
|
|
9
|
+
//
|
|
10
|
+
// Opt-in via LEAP_AUTO_CONSENT=true env var (caller checks, not this module).
|
|
11
|
+
//
|
|
12
|
+
// Per-domain cache: window.__leapfrog_consent_cache stores the winning
|
|
13
|
+
// selector for each domain. On revisit the cached selector fires first
|
|
14
|
+
// (instant dismissal, no 1.5s wait). Disk persistence is handled by
|
|
15
|
+
// domain-knowledge.ts — this module is session-scoped only.
|
|
16
|
+
//
|
|
17
|
+
// ─── Selector Database ────────────────────────────────────────────────────
|
|
18
|
+
export const CONSENT_SELECTORS = [
|
|
19
|
+
{
|
|
20
|
+
name: "OneTrust",
|
|
21
|
+
selectors: [
|
|
22
|
+
"#onetrust-accept-btn-handler",
|
|
23
|
+
".onetrust-close-btn-handler",
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "CookieBot",
|
|
28
|
+
selectors: [
|
|
29
|
+
"#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
|
|
30
|
+
'a[id*="CybotCookiebot"][id*="Allow"]',
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "TrustArc",
|
|
35
|
+
selectors: [
|
|
36
|
+
".truste-consent-required",
|
|
37
|
+
"a.call",
|
|
38
|
+
".pdynamicbutton",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "Quantcast",
|
|
43
|
+
selectors: [
|
|
44
|
+
'.qc-cmp2-summary-buttons button[mode="primary"]',
|
|
45
|
+
".qc-cmp-button",
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "Didomi",
|
|
50
|
+
selectors: [
|
|
51
|
+
"#didomi-notice-agree-button",
|
|
52
|
+
".didomi-continue-without-agreeing",
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "Cookielaw",
|
|
57
|
+
selectors: [
|
|
58
|
+
".cc-compliance .cc-btn",
|
|
59
|
+
".cc-dismiss",
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "Osano",
|
|
64
|
+
selectors: [
|
|
65
|
+
".osano-cm-accept-all",
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "Usercentrics",
|
|
70
|
+
selectors: [
|
|
71
|
+
'button[data-testid="uc-accept-all-button"]',
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "Generic banner",
|
|
76
|
+
selectors: [
|
|
77
|
+
'[class*="cookie"] button',
|
|
78
|
+
'[class*="consent"] button',
|
|
79
|
+
'[id*="cookie"] button',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
// ─── Terms/TOS Checkbox Selectors ────────────────────────────────────────
|
|
84
|
+
//
|
|
85
|
+
// Auto-check terms/privacy checkboxes during form interaction. These fire
|
|
86
|
+
// only when a form is being filled, not on page load (to avoid false positives).
|
|
87
|
+
// Successful selectors are recorded in domain knowledge for instant replay.
|
|
88
|
+
export const TERMS_SELECTORS = [
|
|
89
|
+
'input[type="checkbox"][name*="terms"]',
|
|
90
|
+
'input[type="checkbox"][name*="agree"]',
|
|
91
|
+
'input[type="checkbox"][name*="accept"]',
|
|
92
|
+
'input[type="checkbox"][name*="tos"]',
|
|
93
|
+
'input[type="checkbox"][name*="privacy"]',
|
|
94
|
+
'input[type="checkbox"][id*="terms"]',
|
|
95
|
+
'input[type="checkbox"][id*="agree"]',
|
|
96
|
+
'input[type="checkbox"][id*="accept"]',
|
|
97
|
+
'input[type="checkbox"][id*="tos"]',
|
|
98
|
+
'input[type="checkbox"][id*="policy"]',
|
|
99
|
+
];
|
|
100
|
+
/**
|
|
101
|
+
* Returns JS to inject via page.evaluate() that auto-checks unchecked
|
|
102
|
+
* terms/privacy/TOS checkboxes. Only targets form checkboxes whose labels
|
|
103
|
+
* or names indicate legal agreements. Returns which selectors matched.
|
|
104
|
+
*/
|
|
105
|
+
export function getTermsAutoCheckScript() {
|
|
106
|
+
return `(function() {
|
|
107
|
+
var SELECTORS = ${JSON.stringify(TERMS_SELECTORS)};
|
|
108
|
+
var checked = [];
|
|
109
|
+
for (var i = 0; i < SELECTORS.length; i++) {
|
|
110
|
+
var els = document.querySelectorAll(SELECTORS[i]);
|
|
111
|
+
for (var j = 0; j < els.length; j++) {
|
|
112
|
+
if (!els[j].checked) {
|
|
113
|
+
els[j].checked = true;
|
|
114
|
+
els[j].dispatchEvent(new Event('change', { bubbles: true }));
|
|
115
|
+
checked.push(SELECTORS[i]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { checked: checked.length, selectors: checked };
|
|
120
|
+
})()`;
|
|
121
|
+
}
|
|
122
|
+
// ─── Text-Match Pattern ───────────────────────────────────────────────────
|
|
123
|
+
const TEXT_MATCH_REGEX = "/^\\s*(accept\\s*(all|cookies)?|i\\s*agree|agree|got\\s*it|ok)\\s*$/i";
|
|
124
|
+
// ─── Script Builders ──────────────────────────────────────────────────────
|
|
125
|
+
/**
|
|
126
|
+
* Returns JS to inject via page.addInitScript().
|
|
127
|
+
* After page load + 1.5s delay, attempts to dismiss consent dialogs.
|
|
128
|
+
* Sets up a MutationObserver for lazily-loaded banners (auto-disconnects at 10s).
|
|
129
|
+
*/
|
|
130
|
+
export function getConsentDismissScript() {
|
|
131
|
+
return `(function() {
|
|
132
|
+
if (window.__leapfrog_consent_initialized) return;
|
|
133
|
+
window.__leapfrog_consent_initialized = true;
|
|
134
|
+
|
|
135
|
+
// ── Selector database ───────────────────────────────────────────────
|
|
136
|
+
var FRAMEWORKS = ${JSON.stringify(CONSENT_SELECTORS)};
|
|
137
|
+
var TEXT_RE = ${TEXT_MATCH_REGEX};
|
|
138
|
+
|
|
139
|
+
// ── Cache ───────────────────────────────────────────────────────────
|
|
140
|
+
if (!window.__leapfrog_consent_cache) {
|
|
141
|
+
window.__leapfrog_consent_cache = {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
function isVisible(el) {
|
|
147
|
+
if (!el) return false;
|
|
148
|
+
if (el.getAttribute && el.getAttribute('data-leapfrog') === 'true') return false;
|
|
149
|
+
var rect = el.getBoundingClientRect();
|
|
150
|
+
if (rect.height === 0 || rect.width === 0) return false;
|
|
151
|
+
if (el.offsetParent === null && getComputedStyle(el).position !== 'fixed') return false;
|
|
152
|
+
var inViewport = rect.top < window.innerHeight && rect.bottom > 0
|
|
153
|
+
&& rect.left < window.innerWidth && rect.right > 0;
|
|
154
|
+
return inViewport;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isOnPolicyPage() {
|
|
158
|
+
var url = window.location.href.toLowerCase();
|
|
159
|
+
return url.indexOf('cookie-policy') !== -1 || url.indexOf('privacy') !== -1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function tryClick(el) {
|
|
163
|
+
if (!el || !isVisible(el)) return false;
|
|
164
|
+
el.click();
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Core dismiss function ───────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
window.__leapfrog_dismissConsent = function() {
|
|
171
|
+
if (window.__leapfrog_consent_dismissed) {
|
|
172
|
+
return { dismissed: false, selector: '', framework: 'already-dismissed' };
|
|
173
|
+
}
|
|
174
|
+
if (isOnPolicyPage()) {
|
|
175
|
+
return { dismissed: false, selector: '', framework: 'policy-page-skip' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
var domain = window.location.hostname;
|
|
179
|
+
|
|
180
|
+
// 1. Try cached selector first
|
|
181
|
+
var cached = window.__leapfrog_consent_cache[domain];
|
|
182
|
+
if (cached) {
|
|
183
|
+
var cachedEl = document.querySelector(cached);
|
|
184
|
+
if (tryClick(cachedEl)) {
|
|
185
|
+
window.__leapfrog_consent_dismissed = true;
|
|
186
|
+
return { dismissed: true, selector: cached, framework: 'cached' };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 2. Try framework-specific selectors
|
|
191
|
+
for (var i = 0; i < FRAMEWORKS.length; i++) {
|
|
192
|
+
var fw = FRAMEWORKS[i];
|
|
193
|
+
for (var j = 0; j < fw.selectors.length; j++) {
|
|
194
|
+
var sel = fw.selectors[j];
|
|
195
|
+
try {
|
|
196
|
+
var el = document.querySelector(sel);
|
|
197
|
+
if (tryClick(el)) {
|
|
198
|
+
window.__leapfrog_consent_cache[domain] = sel;
|
|
199
|
+
window.__leapfrog_consent_dismissed = true;
|
|
200
|
+
return { dismissed: true, selector: sel, framework: fw.name };
|
|
201
|
+
}
|
|
202
|
+
} catch (_) { /* invalid selector in this context, skip */ }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 3. Fall back to text matching on visible buttons/links
|
|
207
|
+
// Check both global candidates and elements inside modal dialogs
|
|
208
|
+
// (CNN and similar sites use styled-component modals with <a> links)
|
|
209
|
+
var candidateSelectors = [
|
|
210
|
+
'button', 'a[role="button"]', '[role="button"]', 'a',
|
|
211
|
+
'[role="dialog"] button', '[role="dialog"] a',
|
|
212
|
+
'.modal button', '.modal a',
|
|
213
|
+
'[class*="modal"] button', '[class*="modal"] a'
|
|
214
|
+
];
|
|
215
|
+
var candidates = document.querySelectorAll(candidateSelectors.join(', '));
|
|
216
|
+
for (var k = 0; k < candidates.length; k++) {
|
|
217
|
+
var c = candidates[k];
|
|
218
|
+
if (c.getAttribute && c.getAttribute('data-leapfrog') === 'true') continue;
|
|
219
|
+
var text = (c.textContent || '').trim();
|
|
220
|
+
if (TEXT_RE.test(text) && isVisible(c)) {
|
|
221
|
+
var matchSel = 'text:' + text;
|
|
222
|
+
window.__leapfrog_consent_cache[domain] = matchSel;
|
|
223
|
+
c.click();
|
|
224
|
+
window.__leapfrog_consent_dismissed = true;
|
|
225
|
+
return { dismissed: true, selector: matchSel, framework: 'GDPR Generic' };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { dismissed: false, selector: '', framework: 'none' };
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// ── Auto-run after DOMContentLoaded + 1.5s ─────────────────────────
|
|
233
|
+
|
|
234
|
+
function scheduleRun() {
|
|
235
|
+
setTimeout(function() {
|
|
236
|
+
window.__leapfrog_dismissConsent();
|
|
237
|
+
}, 1500);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (document.readyState === 'loading') {
|
|
241
|
+
document.addEventListener('DOMContentLoaded', scheduleRun);
|
|
242
|
+
} else {
|
|
243
|
+
scheduleRun();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── MutationObserver for lazily-loaded banners ──────────────────────
|
|
247
|
+
|
|
248
|
+
var observer = new MutationObserver(function(mutations) {
|
|
249
|
+
if (window.__leapfrog_consent_dismissed) {
|
|
250
|
+
observer.disconnect();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// Only re-check if nodes were actually added
|
|
254
|
+
for (var m = 0; m < mutations.length; m++) {
|
|
255
|
+
if (mutations[m].addedNodes.length > 0) {
|
|
256
|
+
window.__leapfrog_dismissConsent();
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
function startObserver() {
|
|
263
|
+
observer.observe(document.body || document.documentElement, {
|
|
264
|
+
childList: true,
|
|
265
|
+
subtree: true,
|
|
266
|
+
});
|
|
267
|
+
// Auto-disconnect after 10s to prevent memory leaks
|
|
268
|
+
setTimeout(function() { observer.disconnect(); }, 10000);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (document.body) {
|
|
272
|
+
startObserver();
|
|
273
|
+
} else {
|
|
274
|
+
document.addEventListener('DOMContentLoaded', startObserver);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
})();`;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Returns JS that checks if a consent dialog is currently visible.
|
|
281
|
+
* Evaluates to `{ detected: boolean, frameworks: string[] }`.
|
|
282
|
+
*/
|
|
283
|
+
export function getConsentDetectScript() {
|
|
284
|
+
return `(function() {
|
|
285
|
+
var FRAMEWORKS = ${JSON.stringify(CONSENT_SELECTORS)};
|
|
286
|
+
var TEXT_RE = ${TEXT_MATCH_REGEX};
|
|
287
|
+
var detected = [];
|
|
288
|
+
|
|
289
|
+
function isVisible(el) {
|
|
290
|
+
if (!el) return false;
|
|
291
|
+
if (el.getAttribute && el.getAttribute('data-leapfrog') === 'true') return false;
|
|
292
|
+
var rect = el.getBoundingClientRect();
|
|
293
|
+
if (rect.height === 0 || rect.width === 0) return false;
|
|
294
|
+
if (el.offsetParent === null && getComputedStyle(el).position !== 'fixed') return false;
|
|
295
|
+
return rect.top < window.innerHeight && rect.bottom > 0
|
|
296
|
+
&& rect.left < window.innerWidth && rect.right > 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (var i = 0; i < FRAMEWORKS.length; i++) {
|
|
300
|
+
var fw = FRAMEWORKS[i];
|
|
301
|
+
for (var j = 0; j < fw.selectors.length; j++) {
|
|
302
|
+
try {
|
|
303
|
+
var el = document.querySelector(fw.selectors[j]);
|
|
304
|
+
if (isVisible(el)) {
|
|
305
|
+
detected.push(fw.name);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
} catch (_) {}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Text fallback check — includes <a> tags and modal-scoped elements
|
|
313
|
+
var candidateSelectors = [
|
|
314
|
+
'button', 'a[role="button"]', '[role="button"]', 'a',
|
|
315
|
+
'[role="dialog"] button', '[role="dialog"] a',
|
|
316
|
+
'.modal button', '.modal a',
|
|
317
|
+
'[class*="modal"] button', '[class*="modal"] a'
|
|
318
|
+
];
|
|
319
|
+
var candidates = document.querySelectorAll(candidateSelectors.join(', '));
|
|
320
|
+
for (var k = 0; k < candidates.length; k++) {
|
|
321
|
+
var text = (candidates[k].textContent || '').trim();
|
|
322
|
+
if (TEXT_RE.test(text) && isVisible(candidates[k])) {
|
|
323
|
+
detected.push('GDPR Generic');
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { detected: detected.length > 0, frameworks: detected };
|
|
329
|
+
})()`;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Returns JS to manually trigger consent dismissal (for retry scenarios).
|
|
333
|
+
* Resets the dismissed flag so the function runs fresh.
|
|
334
|
+
*/
|
|
335
|
+
export function getManualDismissScript() {
|
|
336
|
+
return `(function() {
|
|
337
|
+
window.__leapfrog_consent_dismissed = false;
|
|
338
|
+
if (typeof window.__leapfrog_dismissConsent === 'function') {
|
|
339
|
+
return window.__leapfrog_dismissConsent();
|
|
340
|
+
}
|
|
341
|
+
return { dismissed: false, selector: '', framework: 'not-initialized' };
|
|
342
|
+
})()`;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Returns JS to cache a successful selector for a domain.
|
|
346
|
+
* Used when the caller already knows what worked (e.g. from disk persistence).
|
|
347
|
+
*/
|
|
348
|
+
export function getCacheSelectorScript(domain, selector) {
|
|
349
|
+
const safeDomain = domain.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
350
|
+
const safeSelector = selector.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
351
|
+
return `(function() {
|
|
352
|
+
if (!window.__leapfrog_consent_cache) {
|
|
353
|
+
window.__leapfrog_consent_cache = {};
|
|
354
|
+
}
|
|
355
|
+
window.__leapfrog_consent_cache['${safeDomain}'] = '${safeSelector}';
|
|
356
|
+
return true;
|
|
357
|
+
})()`;
|
|
358
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Browser, Page } from "playwright-core";
|
|
2
|
+
import type { Session } from "./types.js";
|
|
3
|
+
export interface HealthCheckResult {
|
|
4
|
+
healthy: boolean;
|
|
5
|
+
reason?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CrashLogEntry {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
url: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
error: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class CrashRecovery {
|
|
14
|
+
/** Crash telemetry — all page crashes during server lifetime */
|
|
15
|
+
private crashLog;
|
|
16
|
+
/** Sessions marked as unhealthy after a page crash (pending recovery) */
|
|
17
|
+
private unhealthySessions;
|
|
18
|
+
/**
|
|
19
|
+
* Attach a disconnect handler to a Playwright Browser instance.
|
|
20
|
+
* On unexpected disconnect (crash, kill, OOM), logs the event and
|
|
21
|
+
* calls onCrash so the SessionManager can clear its state.
|
|
22
|
+
*/
|
|
23
|
+
attachToBrowser(browser: Browser, onCrash: () => void): void;
|
|
24
|
+
/**
|
|
25
|
+
* Attach a page.on('crash') handler to catch Akamai-style browser context
|
|
26
|
+
* crashes. On crash: close the dead page, clean up the pool slot, log the
|
|
27
|
+
* event with the URL that caused the crash, and mark the session unhealthy.
|
|
28
|
+
*
|
|
29
|
+
* @param page The Playwright page to monitor
|
|
30
|
+
* @param session The session owning this page
|
|
31
|
+
* @param onCrash Optional callback when crash is detected (e.g., for cleanup)
|
|
32
|
+
*/
|
|
33
|
+
attachToPage(page: Page, session: Session, onCrash?: (sessionId: string) => void): void;
|
|
34
|
+
/**
|
|
35
|
+
* Check if a session is marked as unhealthy (crashed but not yet recovered).
|
|
36
|
+
*/
|
|
37
|
+
isUnhealthy(sessionId: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Mark a session as recovered after auto-recovery creates a replacement page.
|
|
40
|
+
*/
|
|
41
|
+
markRecovered(sessionId: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Auto-recover a crashed session by creating a replacement page in the same
|
|
44
|
+
* browser context and re-applying stealth init scripts.
|
|
45
|
+
*
|
|
46
|
+
* @param session The session to recover
|
|
47
|
+
* @param applyStealthFn Callback to re-apply stealth to the new page
|
|
48
|
+
* @param rewireNetworkFn Callback to re-wire network intelligence
|
|
49
|
+
* @returns The new replacement page, or null if recovery failed
|
|
50
|
+
*/
|
|
51
|
+
autoRecover(session: Session, applyStealthFn?: (page: Page) => Promise<void>, rewireNetworkFn?: (page: Page, session: Session) => void): Promise<Page | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Quick health check for a single session.
|
|
54
|
+
* Verifies the page is not closed and can still evaluate JavaScript.
|
|
55
|
+
* Never throws — always returns a result object.
|
|
56
|
+
*/
|
|
57
|
+
healthCheck(session: Session): Promise<HealthCheckResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Run health checks on all sessions in parallel.
|
|
60
|
+
* Returns a Map from session ID to health result.
|
|
61
|
+
*/
|
|
62
|
+
healthCheckAll(sessions: Map<string, Session>): Promise<Map<string, HealthCheckResult>>;
|
|
63
|
+
/**
|
|
64
|
+
* Get the crash log — all page crashes during the current server lifetime.
|
|
65
|
+
* Returns an array of {sessionId, url, timestamp, error} entries.
|
|
66
|
+
*/
|
|
67
|
+
getCrashLog(): CrashLogEntry[];
|
|
68
|
+
/**
|
|
69
|
+
* Clear crash log (for testing or after log export).
|
|
70
|
+
*/
|
|
71
|
+
clearCrashLog(): void;
|
|
72
|
+
}
|
|
73
|
+
export declare const crashRecovery: CrashRecovery;
|
|
74
|
+
export default crashRecovery;
|