@monoes/monobrowse 1.0.0
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/src/browser/action-builder/analyzer.d.ts +11 -0
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -0
- package/dist/src/browser/action-builder/analyzer.js +93 -0
- package/dist/src/browser/action-builder/analyzer.js.map +1 -0
- package/dist/src/browser/action-builder/types.d.ts +46 -0
- package/dist/src/browser/action-builder/types.d.ts.map +1 -0
- package/dist/src/browser/action-builder/types.js +2 -0
- package/dist/src/browser/action-builder/types.js.map +1 -0
- package/dist/src/browser/actions.d.ts +29 -0
- package/dist/src/browser/actions.d.ts.map +1 -0
- package/dist/src/browser/actions.js +398 -0
- package/dist/src/browser/actions.js.map +1 -0
- package/dist/src/browser/adapters/gemini.d.ts +3 -0
- package/dist/src/browser/adapters/gemini.d.ts.map +1 -0
- package/dist/src/browser/adapters/gemini.js +13 -0
- package/dist/src/browser/adapters/gemini.js.map +1 -0
- package/dist/src/browser/adapters/google.d.ts +3 -0
- package/dist/src/browser/adapters/google.d.ts.map +1 -0
- package/dist/src/browser/adapters/google.js +17 -0
- package/dist/src/browser/adapters/google.js.map +1 -0
- package/dist/src/browser/adapters/index.d.ts +15 -0
- package/dist/src/browser/adapters/index.d.ts.map +1 -0
- package/dist/src/browser/adapters/index.js +18 -0
- package/dist/src/browser/adapters/index.js.map +1 -0
- package/dist/src/browser/adapters/instagram.d.ts +3 -0
- package/dist/src/browser/adapters/instagram.d.ts.map +1 -0
- package/dist/src/browser/adapters/instagram.js +14 -0
- package/dist/src/browser/adapters/instagram.js.map +1 -0
- package/dist/src/browser/adapters/linkedin.d.ts +3 -0
- package/dist/src/browser/adapters/linkedin.d.ts.map +1 -0
- package/dist/src/browser/adapters/linkedin.js +13 -0
- package/dist/src/browser/adapters/linkedin.js.map +1 -0
- package/dist/src/browser/adapters/microsoft.d.ts +3 -0
- package/dist/src/browser/adapters/microsoft.d.ts.map +1 -0
- package/dist/src/browser/adapters/microsoft.js +16 -0
- package/dist/src/browser/adapters/microsoft.js.map +1 -0
- package/dist/src/browser/adapters/x.d.ts +3 -0
- package/dist/src/browser/adapters/x.d.ts.map +1 -0
- package/dist/src/browser/adapters/x.js +13 -0
- package/dist/src/browser/adapters/x.js.map +1 -0
- package/dist/src/browser/batch.d.ts +5 -0
- package/dist/src/browser/batch.d.ts.map +1 -0
- package/dist/src/browser/batch.js +2 -0
- package/dist/src/browser/batch.js.map +1 -0
- package/dist/src/browser/browser.d.ts +15 -0
- package/dist/src/browser/browser.d.ts.map +1 -0
- package/dist/src/browser/browser.js +256 -0
- package/dist/src/browser/browser.js.map +1 -0
- package/dist/src/browser/cdp.d.ts +18 -0
- package/dist/src/browser/cdp.d.ts.map +1 -0
- package/dist/src/browser/cdp.js +153 -0
- package/dist/src/browser/cdp.js.map +1 -0
- package/dist/src/browser/console-log.d.ts +23 -0
- package/dist/src/browser/console-log.d.ts.map +1 -0
- package/dist/src/browser/console-log.js +104 -0
- package/dist/src/browser/console-log.js.map +1 -0
- package/dist/src/browser/dashboard/api-types.d.ts +50 -0
- package/dist/src/browser/dashboard/api-types.d.ts.map +1 -0
- package/dist/src/browser/dashboard/api-types.js +14 -0
- package/dist/src/browser/dashboard/api-types.js.map +1 -0
- package/dist/src/browser/dashboard/server.d.ts +11 -0
- package/dist/src/browser/dashboard/server.d.ts.map +1 -0
- package/dist/src/browser/dashboard/server.js +191 -0
- package/dist/src/browser/dashboard/server.js.map +1 -0
- package/dist/src/browser/dialog.d.ts +12 -0
- package/dist/src/browser/dialog.d.ts.map +1 -0
- package/dist/src/browser/dialog.js +53 -0
- package/dist/src/browser/dialog.js.map +1 -0
- package/dist/src/browser/emulation.d.ts +15 -0
- package/dist/src/browser/emulation.d.ts.map +1 -0
- package/dist/src/browser/emulation.js +62 -0
- package/dist/src/browser/emulation.js.map +1 -0
- package/dist/src/browser/find.d.ts +22 -0
- package/dist/src/browser/find.d.ts.map +1 -0
- package/dist/src/browser/find.js +169 -0
- package/dist/src/browser/find.js.map +1 -0
- package/dist/src/browser/har.d.ts +27 -0
- package/dist/src/browser/har.d.ts.map +1 -0
- package/dist/src/browser/har.js +161 -0
- package/dist/src/browser/har.js.map +1 -0
- package/dist/src/browser/index.d.ts +23 -0
- package/dist/src/browser/index.d.ts.map +1 -0
- package/dist/src/browser/index.js +23 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/browser/network.d.ts +32 -0
- package/dist/src/browser/network.d.ts.map +1 -0
- package/dist/src/browser/network.js +214 -0
- package/dist/src/browser/network.js.map +1 -0
- package/dist/src/browser/pdf.d.ts +15 -0
- package/dist/src/browser/pdf.d.ts.map +1 -0
- package/dist/src/browser/pdf.js +27 -0
- package/dist/src/browser/pdf.js.map +1 -0
- package/dist/src/browser/profiler.d.ts +10 -0
- package/dist/src/browser/profiler.d.ts.map +1 -0
- package/dist/src/browser/profiler.js +77 -0
- package/dist/src/browser/profiler.js.map +1 -0
- package/dist/src/browser/record.d.ts +21 -0
- package/dist/src/browser/record.d.ts.map +1 -0
- package/dist/src/browser/record.js +59 -0
- package/dist/src/browser/record.js.map +1 -0
- package/dist/src/browser/screenshot.d.ts +15 -0
- package/dist/src/browser/screenshot.d.ts.map +1 -0
- package/dist/src/browser/screenshot.js +38 -0
- package/dist/src/browser/screenshot.js.map +1 -0
- package/dist/src/browser/session.d.ts +8 -0
- package/dist/src/browser/session.d.ts.map +1 -0
- package/dist/src/browser/session.js +91 -0
- package/dist/src/browser/session.js.map +1 -0
- package/dist/src/browser/snapshot.d.ts +12 -0
- package/dist/src/browser/snapshot.d.ts.map +1 -0
- package/dist/src/browser/snapshot.js +179 -0
- package/dist/src/browser/snapshot.js.map +1 -0
- package/dist/src/browser/storage.d.ts +12 -0
- package/dist/src/browser/storage.d.ts.map +1 -0
- package/dist/src/browser/storage.js +46 -0
- package/dist/src/browser/storage.js.map +1 -0
- package/dist/src/browser/tabs.d.ts +8 -0
- package/dist/src/browser/tabs.d.ts.map +1 -0
- package/dist/src/browser/tabs.js +25 -0
- package/dist/src/browser/tabs.js.map +1 -0
- package/dist/src/browser/trace.d.ts +10 -0
- package/dist/src/browser/trace.d.ts.map +1 -0
- package/dist/src/browser/trace.js +87 -0
- package/dist/src/browser/trace.js.map +1 -0
- package/dist/src/browser/types.d.ts +111 -0
- package/dist/src/browser/types.d.ts.map +1 -0
- package/dist/src/browser/types.js +16 -0
- package/dist/src/browser/types.js.map +1 -0
- package/dist/src/browser/vitals.d.ts +15 -0
- package/dist/src/browser/vitals.d.ts.map +1 -0
- package/dist/src/browser/vitals.js +116 -0
- package/dist/src/browser/vitals.js.map +1 -0
- package/dist/src/browser/wait.d.ts +4 -0
- package/dist/src/browser/wait.d.ts.map +1 -0
- package/dist/src/browser/wait.js +146 -0
- package/dist/src/browser/wait.js.map +1 -0
- package/dist/src/browser/workflow/builtin-handlers.d.ts +3 -0
- package/dist/src/browser/workflow/builtin-handlers.d.ts.map +1 -0
- package/dist/src/browser/workflow/builtin-handlers.js +343 -0
- package/dist/src/browser/workflow/builtin-handlers.js.map +1 -0
- package/dist/src/browser/workflow/engine.d.ts +10 -0
- package/dist/src/browser/workflow/engine.d.ts.map +1 -0
- package/dist/src/browser/workflow/engine.js +174 -0
- package/dist/src/browser/workflow/engine.js.map +1 -0
- package/dist/src/browser/workflow/expression.d.ts +4 -0
- package/dist/src/browser/workflow/expression.d.ts.map +1 -0
- package/dist/src/browser/workflow/expression.js +89 -0
- package/dist/src/browser/workflow/expression.js.map +1 -0
- package/dist/src/browser/workflow/index.d.ts +6 -0
- package/dist/src/browser/workflow/index.d.ts.map +1 -0
- package/dist/src/browser/workflow/index.js +6 -0
- package/dist/src/browser/workflow/index.js.map +1 -0
- package/dist/src/browser/workflow/store.d.ts +8 -0
- package/dist/src/browser/workflow/store.d.ts.map +1 -0
- package/dist/src/browser/workflow/store.js +87 -0
- package/dist/src/browser/workflow/store.js.map +1 -0
- package/dist/src/browser/workflow/types.d.ts +62 -0
- package/dist/src/browser/workflow/types.d.ts.map +1 -0
- package/dist/src/browser/workflow/types.js +2 -0
- package/dist/src/browser/workflow/types.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +56 -0
- package/src/browser/action-builder/analyzer.ts +127 -0
- package/src/browser/action-builder/types.ts +17 -0
- package/src/browser/actions.ts +518 -0
- package/src/browser/adapters/gemini.ts +15 -0
- package/src/browser/adapters/google.ts +20 -0
- package/src/browser/adapters/index.ts +33 -0
- package/src/browser/adapters/instagram.ts +16 -0
- package/src/browser/adapters/linkedin.ts +15 -0
- package/src/browser/adapters/microsoft.ts +19 -0
- package/src/browser/adapters/x.ts +15 -0
- package/src/browser/batch.ts +4 -0
- package/src/browser/browser.ts +265 -0
- package/src/browser/cdp.ts +156 -0
- package/src/browser/console-log.ts +120 -0
- package/src/browser/dashboard/api-types.ts +71 -0
- package/src/browser/dashboard/server.ts +206 -0
- package/src/browser/dashboard/ui.html +444 -0
- package/src/browser/dialog.ts +63 -0
- package/src/browser/emulation.ts +94 -0
- package/src/browser/find.ts +225 -0
- package/src/browser/har.ts +202 -0
- package/src/browser/index.ts +22 -0
- package/src/browser/network.ts +244 -0
- package/src/browser/pdf.ts +41 -0
- package/src/browser/profiler.ts +96 -0
- package/src/browser/record.ts +92 -0
- package/src/browser/screenshot.ts +67 -0
- package/src/browser/session.ts +118 -0
- package/src/browser/snapshot.ts +248 -0
- package/src/browser/storage.ts +54 -0
- package/src/browser/tabs.ts +37 -0
- package/src/browser/trace.ts +106 -0
- package/src/browser/types.ts +135 -0
- package/src/browser/vitals.ts +122 -0
- package/src/browser/wait.ts +178 -0
- package/src/browser/workflow/builtin-handlers.ts +379 -0
- package/src/browser/workflow/engine.ts +202 -0
- package/src/browser/workflow/expression.ts +107 -0
- package/src/browser/workflow/index.ts +5 -0
- package/src/browser/workflow/store.ts +87 -0
- package/src/browser/workflow/types.ts +62 -0
- package/src/index.ts +6 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/browser/adapters/google.ts
|
|
2
|
+
import type { PlatformAdapter, PageInterface } from './index.js';
|
|
3
|
+
|
|
4
|
+
export const googleAdapter: PlatformAdapter = {
|
|
5
|
+
platform: 'google',
|
|
6
|
+
baseURL: 'https://accounts.google.com',
|
|
7
|
+
reservedPaths: ['/signin', '/oauth', '/o/oauth2'],
|
|
8
|
+
loginURL: () => 'https://accounts.google.com/signin/v2/identifier',
|
|
9
|
+
async isLoggedIn(page: PageInterface): Promise<boolean> {
|
|
10
|
+
const url = await page.url();
|
|
11
|
+
// Logged in if we're on a Google service page (not the sign-in page)
|
|
12
|
+
if (url.includes('accounts.google.com/signin')) return false;
|
|
13
|
+
return page.evaluate<boolean>("!!(document.querySelector('[aria-label*=\"Google Account\"]') || document.querySelector('.gb_A') || document.cookie.includes('SSID'))");
|
|
14
|
+
},
|
|
15
|
+
async extractUsername(page: PageInterface): Promise<string> {
|
|
16
|
+
return page.evaluate<string>(
|
|
17
|
+
"document.querySelector('[data-email]')?.getAttribute('data-email') ?? document.querySelector('.gb_A.gb_Sa')?.textContent?.trim() ?? ''"
|
|
18
|
+
);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/browser/adapters/index.ts
|
|
2
|
+
|
|
3
|
+
export interface PageInterface {
|
|
4
|
+
evaluate<T>(expression: string): Promise<T>;
|
|
5
|
+
url(): Promise<string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PlatformAdapter {
|
|
9
|
+
platform: string;
|
|
10
|
+
baseURL: string;
|
|
11
|
+
reservedPaths: string[];
|
|
12
|
+
isLoggedIn(page: PageInterface): Promise<boolean>;
|
|
13
|
+
loginURL(): string;
|
|
14
|
+
extractUsername(page: PageInterface): Promise<string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
import { linkedinAdapter } from './linkedin.js';
|
|
18
|
+
import { instagramAdapter } from './instagram.js';
|
|
19
|
+
import { xAdapter } from './x.js';
|
|
20
|
+
import { geminiAdapter } from './gemini.js';
|
|
21
|
+
|
|
22
|
+
export const adapters: Map<string, PlatformAdapter> = new Map([
|
|
23
|
+
['linkedin', linkedinAdapter],
|
|
24
|
+
['instagram', instagramAdapter],
|
|
25
|
+
['x', xAdapter],
|
|
26
|
+
['gemini', geminiAdapter],
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export function getAdapter(platform: string): PlatformAdapter {
|
|
30
|
+
const adapter = adapters.get(platform);
|
|
31
|
+
if (!adapter) throw new Error(`Unknown platform: ${platform}. Supported: ${[...adapters.keys()].join(', ')}`);
|
|
32
|
+
return adapter;
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/browser/adapters/instagram.ts
|
|
2
|
+
import type { PlatformAdapter, PageInterface } from './index.js';
|
|
3
|
+
|
|
4
|
+
export const instagramAdapter: PlatformAdapter = {
|
|
5
|
+
platform: 'instagram',
|
|
6
|
+
baseURL: 'https://www.instagram.com',
|
|
7
|
+
reservedPaths: ['/direct', '/explore', '/reels', '/stories'],
|
|
8
|
+
loginURL: () => 'https://www.instagram.com/accounts/login/',
|
|
9
|
+
async isLoggedIn(page: PageInterface): Promise<boolean> {
|
|
10
|
+
return page.evaluate<boolean>('!!document.querySelector(\'svg[aria-label="Home"]\')');
|
|
11
|
+
},
|
|
12
|
+
async extractUsername(page: PageInterface): Promise<string> {
|
|
13
|
+
// Use getAttribute to get the raw path-relative href (not the resolved absolute URL)
|
|
14
|
+
return page.evaluate<string>("document.querySelector('a[href^=\"/\"]')?.getAttribute('href')?.split('/').filter(Boolean)[0] ?? ''");
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/browser/adapters/linkedin.ts
|
|
2
|
+
import type { PlatformAdapter, PageInterface } from './index.js';
|
|
3
|
+
|
|
4
|
+
export const linkedinAdapter: PlatformAdapter = {
|
|
5
|
+
platform: 'linkedin',
|
|
6
|
+
baseURL: 'https://www.linkedin.com',
|
|
7
|
+
reservedPaths: ['/feed', '/jobs', '/messaging', '/notifications', '/me'],
|
|
8
|
+
loginURL: () => 'https://www.linkedin.com/login',
|
|
9
|
+
async isLoggedIn(page: PageInterface): Promise<boolean> {
|
|
10
|
+
return page.evaluate<boolean>("!!document.querySelector('.global-nav__me-photo')");
|
|
11
|
+
},
|
|
12
|
+
async extractUsername(page: PageInterface): Promise<string> {
|
|
13
|
+
return page.evaluate<string>("document.querySelector('.profile-rail-card__actor-link')?.textContent?.trim() ?? ''");
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/browser/adapters/microsoft.ts
|
|
2
|
+
import type { PlatformAdapter, PageInterface } from './index.js';
|
|
3
|
+
|
|
4
|
+
export const microsoftAdapter: PlatformAdapter = {
|
|
5
|
+
platform: 'microsoft',
|
|
6
|
+
baseURL: 'https://login.microsoftonline.com',
|
|
7
|
+
reservedPaths: ['/oauth2', '/common/oauth2', '/kmsi'],
|
|
8
|
+
loginURL: () => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=common&response_type=code&scope=openid+profile+email',
|
|
9
|
+
async isLoggedIn(page: PageInterface): Promise<boolean> {
|
|
10
|
+
const url = await page.url();
|
|
11
|
+
if (url.includes('login.microsoftonline.com') || url.includes('login.microsoft.com')) return false;
|
|
12
|
+
return page.evaluate<boolean>("!!(document.querySelector('[data-tid=\"user-email\"]') || document.querySelector('.mectrl_currentAccount') || document.cookie.includes('ESTSAUTH'))");
|
|
13
|
+
},
|
|
14
|
+
async extractUsername(page: PageInterface): Promise<string> {
|
|
15
|
+
return page.evaluate<string>(
|
|
16
|
+
"document.querySelector('[data-tid=\"user-email\"]')?.textContent?.trim() ?? document.querySelector('.mectrl_headerEmail')?.textContent?.trim() ?? ''"
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/browser/adapters/x.ts
|
|
2
|
+
import type { PlatformAdapter, PageInterface } from './index.js';
|
|
3
|
+
|
|
4
|
+
export const xAdapter: PlatformAdapter = {
|
|
5
|
+
platform: 'x',
|
|
6
|
+
baseURL: 'https://x.com',
|
|
7
|
+
reservedPaths: ['/home', '/explore', '/notifications', '/messages'],
|
|
8
|
+
loginURL: () => 'https://x.com/i/flow/login',
|
|
9
|
+
async isLoggedIn(page: PageInterface): Promise<boolean> {
|
|
10
|
+
return page.evaluate<boolean>('!!document.querySelector(\'[data-testid="SideNav_AccountSwitcher_Button"]\')');
|
|
11
|
+
},
|
|
12
|
+
async extractUsername(page: PageInterface): Promise<string> {
|
|
13
|
+
return page.evaluate<string>("document.querySelector('[data-testid=\"SideNav_AccountSwitcher_Button\"] span')?.textContent?.trim() ?? ''");
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { spawn, execSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { CdpClient, fetchTargets, fetchNewTarget } from './cdp.js';
|
|
6
|
+
import type { BrowserConfig, CdpTarget } from './types.js';
|
|
7
|
+
import { CHROME_EXECUTABLES } from './types.js';
|
|
8
|
+
import { enableConsoleCapture, setupConsoleCapture } from './console-log.js';
|
|
9
|
+
import { setupDialogAutoHandling } from './dialog.js';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PORT = 9222;
|
|
12
|
+
const LAUNCH_TIMEOUT = 10_000;
|
|
13
|
+
const POLL_INTERVAL = 200;
|
|
14
|
+
|
|
15
|
+
function findChrome(executablePath?: string): string {
|
|
16
|
+
if (executablePath) {
|
|
17
|
+
if (existsSync(executablePath)) return executablePath;
|
|
18
|
+
throw new Error(`Chrome executable not found: ${executablePath}`);
|
|
19
|
+
}
|
|
20
|
+
for (const candidate of CHROME_EXECUTABLES) {
|
|
21
|
+
if (existsSync(candidate)) return candidate;
|
|
22
|
+
}
|
|
23
|
+
// Try PATH
|
|
24
|
+
try {
|
|
25
|
+
const result = execSync('which google-chrome chromium-browser chromium 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
26
|
+
const first = result.split('\n')[0];
|
|
27
|
+
if (first) return first;
|
|
28
|
+
} catch {
|
|
29
|
+
// ignore
|
|
30
|
+
}
|
|
31
|
+
throw new Error(
|
|
32
|
+
'Chrome/Chromium not found. Install Google Chrome or pass executablePath in BrowserConfig.'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function isPortOpen(port: number): Promise<boolean> {
|
|
37
|
+
try {
|
|
38
|
+
await fetchTargets(port);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function launchBrowser(config: BrowserConfig = {}): Promise<number> {
|
|
46
|
+
const rawPort = config.port ?? DEFAULT_PORT;
|
|
47
|
+
// Validate port is in a safe range for localhost CDP debugging
|
|
48
|
+
if (!Number.isInteger(rawPort) || rawPort < 1024 || rawPort > 65535) {
|
|
49
|
+
throw new Error(`Invalid port: ${rawPort}. Must be an integer between 1024 and 65535.`);
|
|
50
|
+
}
|
|
51
|
+
const port = rawPort;
|
|
52
|
+
|
|
53
|
+
if (await isPortOpen(port)) {
|
|
54
|
+
return port;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const chromePath = findChrome(config.executablePath);
|
|
58
|
+
const userDataDir = config.userDataDir ?? join(tmpdir(), `monomind-browser-${port}`);
|
|
59
|
+
|
|
60
|
+
const defaultArgs = [
|
|
61
|
+
`--remote-debugging-port=${port}`,
|
|
62
|
+
`--user-data-dir=${userDataDir}`,
|
|
63
|
+
'--no-first-run',
|
|
64
|
+
'--no-default-browser-check',
|
|
65
|
+
'--disable-background-networking',
|
|
66
|
+
'--disable-client-side-phishing-detection',
|
|
67
|
+
'--disable-default-apps',
|
|
68
|
+
'--disable-extensions',
|
|
69
|
+
'--disable-hang-monitor',
|
|
70
|
+
'--disable-popup-blocking',
|
|
71
|
+
'--disable-prompt-on-repost',
|
|
72
|
+
'--disable-sync',
|
|
73
|
+
'--disable-translate',
|
|
74
|
+
'--metrics-recording-only',
|
|
75
|
+
'--safebrowsing-disable-auto-update',
|
|
76
|
+
'--password-store=basic',
|
|
77
|
+
'--use-mock-keychain',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
if (config.headless !== false) {
|
|
81
|
+
defaultArgs.push('--headless=new');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cap caller-supplied args to prevent memory exhaustion via huge argument arrays
|
|
85
|
+
const callerArgs = (config.args ?? []).slice(0, 50);
|
|
86
|
+
const args = [...defaultArgs, ...callerArgs];
|
|
87
|
+
const child = spawn(chromePath, args, {
|
|
88
|
+
detached: true,
|
|
89
|
+
stdio: 'ignore',
|
|
90
|
+
});
|
|
91
|
+
child.unref();
|
|
92
|
+
|
|
93
|
+
const deadline = Date.now() + LAUNCH_TIMEOUT;
|
|
94
|
+
while (Date.now() < deadline) {
|
|
95
|
+
await sleep(POLL_INTERVAL);
|
|
96
|
+
if (await isPortOpen(port)) return port;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new Error(`Chrome failed to start on port ${port} within ${LAUNCH_TIMEOUT}ms`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function enableSessionDomains(client: CdpClient, sessionId: string): Promise<void> {
|
|
103
|
+
await Promise.all([
|
|
104
|
+
client.send('Page.enable', {}, sessionId),
|
|
105
|
+
client.send('Runtime.enable', {}, sessionId),
|
|
106
|
+
client.send('Network.enable', {}, sessionId),
|
|
107
|
+
client.send('DOM.enable', {}, sessionId),
|
|
108
|
+
client.send('Accessibility.enable', {}, sessionId),
|
|
109
|
+
]);
|
|
110
|
+
setupConsoleCapture(client, sessionId);
|
|
111
|
+
await enableConsoleCapture(client, sessionId);
|
|
112
|
+
setupDialogAutoHandling(client, sessionId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function connectToTarget(port: number, targetId?: string): Promise<{ client: CdpClient; target: CdpTarget; sessionId: string }> {
|
|
116
|
+
const targets = await fetchTargets(port);
|
|
117
|
+
const pageTargets = targets.filter((t) => t.type === 'page');
|
|
118
|
+
|
|
119
|
+
let target: CdpTarget;
|
|
120
|
+
if (targetId) {
|
|
121
|
+
const found = pageTargets.find((t) => t.id === targetId);
|
|
122
|
+
if (!found) throw new Error(`Target ${targetId} not found`);
|
|
123
|
+
target = found;
|
|
124
|
+
} else if (pageTargets.length > 0) {
|
|
125
|
+
target = pageTargets[0];
|
|
126
|
+
} else {
|
|
127
|
+
target = await fetchNewTarget(port, 'about:blank');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const wsUrl = target.webSocketDebuggerUrl ?? `ws://127.0.0.1:${port}/devtools/page/${target.id}`;
|
|
131
|
+
const client = new CdpClient();
|
|
132
|
+
await client.connect(wsUrl);
|
|
133
|
+
|
|
134
|
+
const { sessionId } = await client.send<{ sessionId: string }>('Target.attachToTarget', {
|
|
135
|
+
targetId: target.id,
|
|
136
|
+
flatten: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await enableSessionDomains(client, sessionId);
|
|
140
|
+
return { client, target, sessionId };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function openUrl(client: CdpClient, sessionId: string, url: string): Promise<void> {
|
|
144
|
+
// Cap to 2 MB to prevent OOM in CDP message serializer (e.g. data: URI attacks)
|
|
145
|
+
if (url.length > 2_097_152) throw new Error('URL exceeds 2 MB limit');
|
|
146
|
+
await client.send('Page.navigate', { url }, sessionId);
|
|
147
|
+
await waitForNetworkIdle(client, sessionId, 500, 30_000);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function waitForLoad(
|
|
151
|
+
client: CdpClient,
|
|
152
|
+
sessionId: string,
|
|
153
|
+
condition: 'load' | 'networkidle' | 'domcontentloaded' = 'load',
|
|
154
|
+
timeout = 30_000
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
if (condition === 'load' || condition === 'domcontentloaded') {
|
|
157
|
+
// Guard against race where the page loads before the listener is registered
|
|
158
|
+
const readyExpr = condition === 'load' ? 'document.readyState === "complete"' : 'document.readyState !== "loading"';
|
|
159
|
+
const readyCheck = await client.send<{ result: { value?: boolean } }>('Runtime.evaluate', {
|
|
160
|
+
expression: readyExpr, returnByValue: true,
|
|
161
|
+
}, sessionId).catch(() => ({ result: { value: false } }));
|
|
162
|
+
if (readyCheck.result?.value) return;
|
|
163
|
+
|
|
164
|
+
const event = condition === 'load' ? 'Page.loadEventFired' : 'Page.domContentEventFired';
|
|
165
|
+
const [eventPromise, cancelOnce] = client.onceWithOff(event, sessionId);
|
|
166
|
+
let timedOut = false;
|
|
167
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
168
|
+
const timeoutPromise = new Promise<void>((resolve) => {
|
|
169
|
+
timeoutHandle = setTimeout(() => { timedOut = true; resolve(); }, timeout);
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
await Promise.race([eventPromise, timeoutPromise]);
|
|
173
|
+
if (timedOut) throw new Error(`Timeout waiting for ${condition}`);
|
|
174
|
+
} finally {
|
|
175
|
+
cancelOnce();
|
|
176
|
+
clearTimeout(timeoutHandle);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// networkidle: no network requests for 500ms
|
|
182
|
+
await waitForNetworkIdle(client, sessionId, 500, timeout);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function waitForNetworkIdle(
|
|
186
|
+
client: CdpClient,
|
|
187
|
+
sessionId: string,
|
|
188
|
+
idleMs: number,
|
|
189
|
+
timeout: number
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
let pending = 0;
|
|
193
|
+
const inflight = new Set<string>();
|
|
194
|
+
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
195
|
+
const killTimer = setTimeout(() => {
|
|
196
|
+
cleanup();
|
|
197
|
+
reject(new Error('Timeout waiting for networkidle'));
|
|
198
|
+
}, timeout);
|
|
199
|
+
|
|
200
|
+
const cleanup = () => {
|
|
201
|
+
if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
|
|
202
|
+
clearTimeout(killTimer);
|
|
203
|
+
offReq(); offResp(); offFail(); offCache(); offResp2();
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const settle = () => {
|
|
207
|
+
cleanup();
|
|
208
|
+
resolve();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const check = () => {
|
|
212
|
+
if (pending === 0) {
|
|
213
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
214
|
+
idleTimer = setTimeout(settle, idleMs);
|
|
215
|
+
} else {
|
|
216
|
+
if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const offReq = client.on('Network.requestWillBeSent', (params, sid) => {
|
|
221
|
+
if (sid !== sessionId) return;
|
|
222
|
+
const id = params.requestId as string;
|
|
223
|
+
if (!inflight.has(id)) { inflight.add(id); pending++; check(); }
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const decrement = (params: Record<string, unknown>, sid?: string) => {
|
|
227
|
+
if (sid !== sessionId) return;
|
|
228
|
+
const id = params.requestId as string;
|
|
229
|
+
if (inflight.delete(id)) { pending = Math.max(0, pending - 1); check(); }
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const offResp = client.on('Network.loadingFinished', decrement);
|
|
233
|
+
const offFail = client.on('Network.loadingFailed', decrement);
|
|
234
|
+
const offCache = client.on('Network.requestServedFromCache', decrement);
|
|
235
|
+
// Guard against requests that never fire loadingFinished/loadingFailed (e.g. data: URLs)
|
|
236
|
+
// Skip 3xx redirect responses — the request continues under the same requestId
|
|
237
|
+
const offResp2 = client.on('Network.responseReceived', (params, sid) => {
|
|
238
|
+
const p = params as { requestId: string; response: { status: number } };
|
|
239
|
+
if (p.response.status >= 300 && p.response.status < 400) return;
|
|
240
|
+
decrement(params, sid);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
check();
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function getCurrentUrl(client: CdpClient, sessionId: string): Promise<string> {
|
|
248
|
+
const result = await client.send<{ result: { value: string } }>('Runtime.evaluate', {
|
|
249
|
+
expression: 'location.href',
|
|
250
|
+
returnByValue: true,
|
|
251
|
+
}, sessionId);
|
|
252
|
+
return result.result?.value ?? '';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function getCurrentTitle(client: CdpClient, sessionId: string): Promise<string> {
|
|
256
|
+
const result = await client.send<{ result: { value: string } }>('Runtime.evaluate', {
|
|
257
|
+
expression: 'document.title',
|
|
258
|
+
returnByValue: true,
|
|
259
|
+
}, sessionId);
|
|
260
|
+
return result.result?.value ?? '';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function sleep(ms: number): Promise<void> {
|
|
264
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
265
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import type { CdpCommand, CdpResponse, CdpTarget } from './types.js';
|
|
3
|
+
|
|
4
|
+
export class CdpClient {
|
|
5
|
+
private ws: WebSocket | null = null;
|
|
6
|
+
private pendingCommands = new Map<number, { resolve: (r: CdpResponse) => void; reject: (e: Error) => void }>();
|
|
7
|
+
private eventListeners = new Map<string, Set<(params: Record<string, unknown>, sessionId?: string) => void>>();
|
|
8
|
+
private nextId = 1;
|
|
9
|
+
private connected = false;
|
|
10
|
+
|
|
11
|
+
async connect(wsUrl: string): Promise<void> {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
this.ws = new WebSocket(wsUrl);
|
|
14
|
+
|
|
15
|
+
this.ws.on('open', () => {
|
|
16
|
+
this.connected = true;
|
|
17
|
+
resolve();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
this.ws.on('error', (err) => {
|
|
21
|
+
if (!this.connected) {
|
|
22
|
+
reject(err);
|
|
23
|
+
} else {
|
|
24
|
+
// Post-connect: flush all pending commands — 'close' may not fire on all platforms
|
|
25
|
+
this.connected = false;
|
|
26
|
+
for (const { reject: r } of this.pendingCommands.values()) r(err);
|
|
27
|
+
this.pendingCommands.clear();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
this.ws.on('close', () => {
|
|
32
|
+
this.connected = false;
|
|
33
|
+
for (const { reject: r } of this.pendingCommands.values()) {
|
|
34
|
+
r(new Error('CDP connection closed'));
|
|
35
|
+
}
|
|
36
|
+
this.pendingCommands.clear();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this.ws.on('message', (data) => {
|
|
40
|
+
try {
|
|
41
|
+
const msg: CdpResponse = JSON.parse(data.toString());
|
|
42
|
+
if (msg.id !== undefined && this.pendingCommands.has(msg.id)) {
|
|
43
|
+
const handler = this.pendingCommands.get(msg.id)!;
|
|
44
|
+
this.pendingCommands.delete(msg.id);
|
|
45
|
+
if (msg.error) {
|
|
46
|
+
handler.reject(new Error(`CDP error ${msg.error.code}: ${msg.error.message}`));
|
|
47
|
+
} else {
|
|
48
|
+
handler.resolve(msg);
|
|
49
|
+
}
|
|
50
|
+
} else if (msg.method) {
|
|
51
|
+
const listeners = this.eventListeners.get(msg.method);
|
|
52
|
+
if (listeners) {
|
|
53
|
+
for (const fn of listeners) {
|
|
54
|
+
try { fn(msg.params ?? {}, msg.sessionId); } catch { /* isolate per-listener errors */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// ignore malformed messages
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
send<T = Record<string, unknown>>(method: string, params?: Record<string, unknown>, sessionId?: string): Promise<T> {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
if (!this.ws || !this.connected) {
|
|
68
|
+
reject(new Error('CDP not connected'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Cap in-flight commands to prevent unbounded Map growth
|
|
72
|
+
if (this.pendingCommands.size >= 1000) {
|
|
73
|
+
reject(new Error('CDP command queue full (>1000 in-flight commands)'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const id = this.nextId++;
|
|
77
|
+
const cmd: CdpCommand = { id, method, params };
|
|
78
|
+
if (sessionId) cmd.sessionId = sessionId;
|
|
79
|
+
this.pendingCommands.set(id, {
|
|
80
|
+
resolve: (r) => resolve((r.result ?? {}) as T),
|
|
81
|
+
reject,
|
|
82
|
+
});
|
|
83
|
+
this.ws.send(JSON.stringify(cmd), (err) => {
|
|
84
|
+
if (err) {
|
|
85
|
+
this.pendingCommands.delete(id);
|
|
86
|
+
reject(err);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
on(event: string, fn: (params: Record<string, unknown>, sessionId?: string) => void): () => void {
|
|
93
|
+
if (!this.eventListeners.has(event)) {
|
|
94
|
+
this.eventListeners.set(event, new Set());
|
|
95
|
+
}
|
|
96
|
+
const listeners = this.eventListeners.get(event)!;
|
|
97
|
+
// Cap listeners per event to prevent unbounded Set growth
|
|
98
|
+
if (listeners.size >= 100) {
|
|
99
|
+
throw new Error(`CDP event listener limit reached for event: ${event}`);
|
|
100
|
+
}
|
|
101
|
+
listeners.add(fn);
|
|
102
|
+
return () => this.eventListeners.get(event)?.delete(fn);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
once(event: string, sessionId?: string): Promise<Record<string, unknown>> {
|
|
106
|
+
const [promise] = this.onceWithOff(event, sessionId);
|
|
107
|
+
return promise;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
onceWithOff(event: string, sessionId?: string): [Promise<Record<string, unknown>>, () => void] {
|
|
111
|
+
let off: () => void = () => {};
|
|
112
|
+
const promise = new Promise<Record<string, unknown>>((resolve) => {
|
|
113
|
+
off = this.on(event, (params, sid) => {
|
|
114
|
+
if (sessionId !== undefined && sid !== sessionId) return;
|
|
115
|
+
off();
|
|
116
|
+
resolve(params);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
return [promise, () => off()];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
close(): void {
|
|
123
|
+
this.connected = false;
|
|
124
|
+
for (const { reject: r } of this.pendingCommands.values()) r(new Error('CDP connection closed'));
|
|
125
|
+
this.pendingCommands.clear();
|
|
126
|
+
this.ws?.close();
|
|
127
|
+
this.ws = null;
|
|
128
|
+
this.eventListeners.clear();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
isConnected(): boolean {
|
|
132
|
+
return this.connected;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const CDP_RESPONSE_SIZE_LIMIT = 10 * 1024 * 1024; // 10 MB
|
|
137
|
+
|
|
138
|
+
async function readCdpJson(res: Response): Promise<unknown> {
|
|
139
|
+
const text = await res.text();
|
|
140
|
+
if (text.length > CDP_RESPONSE_SIZE_LIMIT) {
|
|
141
|
+
throw new Error(`CDP response too large: ${text.length} bytes`);
|
|
142
|
+
}
|
|
143
|
+
return JSON.parse(text);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function fetchTargets(port: number): Promise<CdpTarget[]> {
|
|
147
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/list`);
|
|
148
|
+
if (!res.ok) throw new Error(`Failed to fetch targets: ${res.statusText}`);
|
|
149
|
+
return readCdpJson(res) as Promise<CdpTarget[]>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function fetchNewTarget(port: number, url: string): Promise<CdpTarget> {
|
|
153
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/new?${url}`);
|
|
154
|
+
if (!res.ok) throw new Error(`Failed to create target: ${res.statusText}`);
|
|
155
|
+
return readCdpJson(res) as Promise<CdpTarget>;
|
|
156
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
|
|
3
|
+
export interface ConsoleMessage {
|
|
4
|
+
type: 'log' | 'error' | 'warn' | 'info' | 'debug' | 'verbose';
|
|
5
|
+
text: string;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
url?: string;
|
|
8
|
+
lineNumber?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PageError {
|
|
12
|
+
text: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
lineNumber?: number;
|
|
15
|
+
columnNumber?: number;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const _consoleMessages = new Map<string, ConsoleMessage[]>();
|
|
20
|
+
const _pageErrors = new Map<string, PageError[]>();
|
|
21
|
+
const _consoleListeners = new Map<string, Array<() => void>>();
|
|
22
|
+
|
|
23
|
+
function messagesFor(sessionId: string): ConsoleMessage[] {
|
|
24
|
+
if (!_consoleMessages.has(sessionId)) _consoleMessages.set(sessionId, []);
|
|
25
|
+
return _consoleMessages.get(sessionId)!;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function errorsFor(sessionId: string): PageError[] {
|
|
29
|
+
if (!_pageErrors.has(sessionId)) _pageErrors.set(sessionId, []);
|
|
30
|
+
return _pageErrors.get(sessionId)!;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function setupConsoleCapture(client: CdpClient, sessionId: string): void {
|
|
34
|
+
// Remove stale listeners from any prior connection on this sessionId
|
|
35
|
+
const prevOffs = _consoleListeners.get(sessionId);
|
|
36
|
+
if (prevOffs) { for (const off of prevOffs) off(); _consoleListeners.delete(sessionId); }
|
|
37
|
+
|
|
38
|
+
_consoleMessages.set(sessionId, []);
|
|
39
|
+
_pageErrors.set(sessionId, []);
|
|
40
|
+
|
|
41
|
+
const off1 = client.on('Runtime.consoleAPICalled', (params, sid) => {
|
|
42
|
+
if (sid !== sessionId) return;
|
|
43
|
+
const args = (params.args as Array<{ value?: unknown; description?: string }>) ?? [];
|
|
44
|
+
const text = args.map((a) => a.description ?? String(a.value ?? '')).join(' ');
|
|
45
|
+
const rawType = (params.type as string) === 'warning' ? 'warn' : (params.type as string);
|
|
46
|
+
messagesFor(sessionId).push({
|
|
47
|
+
type: (rawType as ConsoleMessage['type']) ?? 'log',
|
|
48
|
+
text,
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const off2 = client.on('Log.entryAdded', (params, sid) => {
|
|
54
|
+
if (sid !== sessionId) return;
|
|
55
|
+
const entry = params.entry as { level?: string; text?: string; url?: string; lineNumber?: number };
|
|
56
|
+
// CDP uses 'warning' but ConsoleMessage type uses 'warn'
|
|
57
|
+
const rawLevel = entry.level === 'warning' ? 'warn' : entry.level;
|
|
58
|
+
messagesFor(sessionId).push({
|
|
59
|
+
type: (rawLevel as ConsoleMessage['type']) ?? 'log',
|
|
60
|
+
text: entry.text ?? '',
|
|
61
|
+
timestamp: Date.now(),
|
|
62
|
+
url: entry.url,
|
|
63
|
+
lineNumber: entry.lineNumber,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const off3 = client.on('Runtime.exceptionThrown', (params, sid) => {
|
|
68
|
+
if (sid !== sessionId) return;
|
|
69
|
+
const detail = params.exceptionDetails as {
|
|
70
|
+
text?: string;
|
|
71
|
+
url?: string;
|
|
72
|
+
lineNumber?: number;
|
|
73
|
+
columnNumber?: number;
|
|
74
|
+
exception?: { description?: string; value?: unknown };
|
|
75
|
+
};
|
|
76
|
+
const message = detail.exception?.description ?? detail.text ?? 'Unknown error';
|
|
77
|
+
errorsFor(sessionId).push({
|
|
78
|
+
text: message,
|
|
79
|
+
url: detail.url,
|
|
80
|
+
lineNumber: detail.lineNumber,
|
|
81
|
+
columnNumber: detail.columnNumber,
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
_consoleListeners.set(sessionId, [off1, off2, off3]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function enableConsoleCapture(client: CdpClient, sessionId: string): Promise<void> {
|
|
90
|
+
await client.send('Runtime.enable', {}, sessionId);
|
|
91
|
+
await client.send('Log.enable', {}, sessionId);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getConsoleMessages(sessionId?: string): ConsoleMessage[] {
|
|
95
|
+
if (sessionId) return [...(messagesFor(sessionId))];
|
|
96
|
+
// Fallback: return all messages across all sessions (legacy callers)
|
|
97
|
+
return [..._consoleMessages.values()].flat();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function clearConsoleMessages(sessionId?: string): void {
|
|
101
|
+
if (sessionId) { _consoleMessages.set(sessionId, []); return; }
|
|
102
|
+
_consoleMessages.clear();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getPageErrors(sessionId?: string): PageError[] {
|
|
106
|
+
if (sessionId) return [...(errorsFor(sessionId))];
|
|
107
|
+
return [..._pageErrors.values()].flat();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function clearPageErrors(sessionId?: string): void {
|
|
111
|
+
if (sessionId) { _pageErrors.set(sessionId, []); return; }
|
|
112
|
+
_pageErrors.clear();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function teardownConsoleCapture(sessionId: string): void {
|
|
116
|
+
const offs = _consoleListeners.get(sessionId);
|
|
117
|
+
if (offs) { for (const off of offs) off(); _consoleListeners.delete(sessionId); }
|
|
118
|
+
_consoleMessages.delete(sessionId);
|
|
119
|
+
_pageErrors.delete(sessionId);
|
|
120
|
+
}
|