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,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;