veil-browser 0.4.0 → 0.4.1
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/README.md +70 -0
- package/dist/browser.d.ts +21 -3
- package/dist/browser.js +314 -54
- package/dist/index.js +102 -41
- package/dist/mcp.d.ts +13 -1
- package/dist/mcp.js +537 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -80,6 +80,36 @@ veil shot page.png
|
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
83
|
+
## Browser Selection
|
|
84
|
+
|
|
85
|
+
veil can use Playwright's bundled Chromium, an installed Chrome-compatible browser, or an already-running browser exposed over CDP.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Installed Google Chrome
|
|
89
|
+
veil login reddit --browser chrome
|
|
90
|
+
|
|
91
|
+
# Installed Dia
|
|
92
|
+
veil login reddit --browser dia
|
|
93
|
+
|
|
94
|
+
# Attach to a manually started browser over CDP
|
|
95
|
+
veil login reddit --cdp-url http://127.0.0.1:9222
|
|
96
|
+
|
|
97
|
+
# Launch a dedicated persistent automation profile
|
|
98
|
+
veil login reddit --browser chrome --user-data-dir "$HOME/.veil/chrome-profile"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Supported shared browser flags:
|
|
102
|
+
|
|
103
|
+
- `--browser playwright|chrome|dia`
|
|
104
|
+
- `--browser-path /absolute/path/to/browser`
|
|
105
|
+
- `--cdp-url http://127.0.0.1:9222`
|
|
106
|
+
- `--user-data-dir /path/to/profile`
|
|
107
|
+
- `--timeout-ms 45000`
|
|
108
|
+
|
|
109
|
+
Use a dedicated automation profile for `--user-data-dir`. Do not point veil at your live default Chrome profile while that browser is open.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
83
113
|
## All Commands
|
|
84
114
|
|
|
85
115
|
### Session
|
|
@@ -89,6 +119,7 @@ veil shot page.png
|
|
|
89
119
|
| `veil open <platform>` | Restore session and navigate to platform home |
|
|
90
120
|
| `veil sessions` | List saved sessions |
|
|
91
121
|
| `veil close` | Close browser |
|
|
122
|
+
| `veil serve` | Start the Streamable HTTP MCP server |
|
|
92
123
|
|
|
93
124
|
**Supported platforms:** `x`, `linkedin`, `reddit`, `bluesky` (or any URL)
|
|
94
125
|
|
|
@@ -140,6 +171,45 @@ veil shot page.png
|
|
|
140
171
|
|
|
141
172
|
---
|
|
142
173
|
|
|
174
|
+
## Remote MCP Server for Claude
|
|
175
|
+
|
|
176
|
+
veil now includes a real Streamable HTTP MCP server built on the official MCP SDK.
|
|
177
|
+
|
|
178
|
+
Local HTTP example:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
veil serve --host 127.0.0.1 --port 3456
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Direct HTTPS example:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
veil serve \
|
|
188
|
+
--host 0.0.0.0 \
|
|
189
|
+
--port 3443 \
|
|
190
|
+
--allowed-hosts your-domain.example \
|
|
191
|
+
--https-cert /etc/ssl/your-domain/fullchain.pem \
|
|
192
|
+
--https-key /etc/ssl/your-domain/privkey.pem
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Important deployment notes:
|
|
196
|
+
|
|
197
|
+
- Claude needs a reachable `https://` endpoint with a valid certificate. A localhost server or self-signed certificate is useful for testing, but not for production Claude integrations.
|
|
198
|
+
- The MCP endpoint is `POST /mcp`.
|
|
199
|
+
- Health checks are available at `GET /healthz`.
|
|
200
|
+
- When binding to `0.0.0.0` or another non-localhost interface, set `--allowed-hosts` to the real public hostname to keep host-header validation in place.
|
|
201
|
+
- You can also terminate TLS in a reverse proxy or tunnel and forward plain HTTP to `veil serve`.
|
|
202
|
+
- Sessions are still created separately with `veil login <platform>`. The MCP server reuses those saved sessions when tools specify a `platform`.
|
|
203
|
+
|
|
204
|
+
Smoke-test the built server locally:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm run build
|
|
208
|
+
npm run test:mcp
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
143
213
|
## For AI Agents (OpenClaw / MCP)
|
|
144
214
|
|
|
145
215
|
veil ships with a `SKILL.md` that teaches OpenClaw exactly how to use it — all commands, all platform selectors, and complete task sequences.
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import { Browser, BrowserContext, Page } from 'playwright';
|
|
2
|
-
export
|
|
2
|
+
export interface BrowserLaunchOptions {
|
|
3
3
|
headed?: boolean;
|
|
4
4
|
platform?: string;
|
|
5
|
-
|
|
5
|
+
browser?: string;
|
|
6
|
+
browserPath?: string;
|
|
7
|
+
cdpUrl?: string;
|
|
8
|
+
userDataDir?: string;
|
|
9
|
+
timeoutMs?: number | string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Ensure a browser session is available, creating or attaching as needed.
|
|
13
|
+
*/
|
|
14
|
+
export declare function ensureBrowser(opts?: BrowserLaunchOptions): Promise<{
|
|
6
15
|
browser: Browser;
|
|
7
16
|
context: BrowserContext;
|
|
8
17
|
page: Page;
|
|
9
18
|
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Return the current live page if the browser is still open.
|
|
21
|
+
*/
|
|
10
22
|
export declare function getPage(): Promise<Page | null>;
|
|
11
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Persist the current storage state when requested and close veil-owned resources.
|
|
25
|
+
*/
|
|
26
|
+
export declare function closeBrowser(platform?: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Sleep for a short human-like randomized delay.
|
|
29
|
+
*/
|
|
12
30
|
export declare function humanDelay(min?: number, max?: number): Promise<void>;
|
package/dist/browser.js
CHANGED
|
@@ -1,40 +1,159 @@
|
|
|
1
|
+
import { constants as fsConstants, promises as fs } from 'fs';
|
|
1
2
|
import { chromium } from 'playwright';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { execSync } from 'child_process';
|
|
6
|
-
import { loadSession } from './session.js';
|
|
7
|
-
const VEIL_DIR = join(homedir(), '.veil');
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { loadSession, saveSession } from './session.js';
|
|
8
5
|
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
7
|
+
const KNOWN_BROWSERS = {
|
|
8
|
+
playwright: {},
|
|
9
|
+
chrome: {
|
|
10
|
+
executablePaths: [
|
|
11
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
12
|
+
'/usr/bin/google-chrome',
|
|
13
|
+
'/usr/bin/chromium',
|
|
14
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
15
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
16
|
+
],
|
|
17
|
+
channel: 'chrome',
|
|
18
|
+
},
|
|
19
|
+
dia: {
|
|
20
|
+
executablePaths: [
|
|
21
|
+
'/Applications/Dia.app/Contents/MacOS/Dia',
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
27
25
|
let _browser = null;
|
|
28
26
|
let _context = null;
|
|
29
27
|
let _page = null;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
let _closeTarget = 'browser';
|
|
29
|
+
let _exitHandlerRegistered = false;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a CLI option or environment variable override.
|
|
32
|
+
*/
|
|
33
|
+
function resolveOption(value, envKey) {
|
|
34
|
+
if (value !== undefined)
|
|
35
|
+
return String(value);
|
|
36
|
+
return process.env[envKey];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Normalize a path-like option to an absolute path.
|
|
40
|
+
*/
|
|
41
|
+
function normalizePath(value) {
|
|
42
|
+
if (!value)
|
|
43
|
+
return undefined;
|
|
44
|
+
return resolve(value);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validate and normalize the requested browser launch mode.
|
|
48
|
+
*/
|
|
49
|
+
async function resolveBrowserOptions(opts) {
|
|
50
|
+
const browserName = (resolveOption(opts.browser, 'VEIL_BROWSER') ?? 'playwright').toLowerCase();
|
|
51
|
+
const browserPath = normalizePath(resolveOption(opts.browserPath, 'VEIL_BROWSER_PATH'));
|
|
52
|
+
const cdpUrl = resolveOption(opts.cdpUrl, 'VEIL_CDP_URL');
|
|
53
|
+
const userDataDir = normalizePath(resolveOption(opts.userDataDir, 'VEIL_USER_DATA_DIR'));
|
|
54
|
+
const timeoutMs = parseTimeout(resolveOption(opts.timeoutMs, 'VEIL_BROWSER_TIMEOUT_MS'));
|
|
55
|
+
if (!isKnownBrowser(browserName) && !browserPath) {
|
|
56
|
+
throw new Error(`Unsupported browser "${browserName}". Use one of: ${Object.keys(KNOWN_BROWSERS).join(', ')} or pass --browser-path.`);
|
|
57
|
+
}
|
|
58
|
+
if (cdpUrl && userDataDir) {
|
|
59
|
+
throw new Error('Use either --cdp-url or --user-data-dir, not both.');
|
|
60
|
+
}
|
|
61
|
+
if (cdpUrl)
|
|
62
|
+
validateCdpUrl(cdpUrl);
|
|
63
|
+
if (browserPath)
|
|
64
|
+
await ensureExecutable(browserPath);
|
|
65
|
+
if (userDataDir)
|
|
66
|
+
await ensureDirectory(userDataDir);
|
|
67
|
+
return {
|
|
68
|
+
browser: (isKnownBrowser(browserName) ? browserName : 'playwright'),
|
|
69
|
+
browserPath,
|
|
70
|
+
cdpUrl,
|
|
71
|
+
userDataDir,
|
|
72
|
+
headed: !!opts.headed,
|
|
73
|
+
platform: opts.platform,
|
|
74
|
+
timeoutMs,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Check whether the requested browser name is one of veil's built-in targets.
|
|
79
|
+
*/
|
|
80
|
+
function isKnownBrowser(value) {
|
|
81
|
+
return value === 'playwright' || value === 'chrome' || value === 'dia';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parse a timeout input into a positive millisecond value.
|
|
85
|
+
*/
|
|
86
|
+
function parseTimeout(value) {
|
|
87
|
+
if (!value)
|
|
88
|
+
return DEFAULT_TIMEOUT_MS;
|
|
89
|
+
const parsed = Number.parseInt(value, 10);
|
|
90
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
91
|
+
throw new Error(`Invalid timeout "${value}". Expected a positive integer in milliseconds.`);
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate that the requested CDP endpoint is well-formed.
|
|
97
|
+
*/
|
|
98
|
+
function validateCdpUrl(url) {
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = new URL(url);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
throw new Error(`Invalid CDP URL "${url}". Expected http(s)://host:port or ws(s)://...`);
|
|
105
|
+
}
|
|
106
|
+
const protocols = new Set(['http:', 'https:', 'ws:', 'wss:']);
|
|
107
|
+
if (!protocols.has(parsed.protocol)) {
|
|
108
|
+
throw new Error(`Unsupported CDP protocol "${parsed.protocol}". Use http, https, ws, or wss.`);
|
|
33
109
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Verify that the requested browser executable exists and is runnable.
|
|
113
|
+
*/
|
|
114
|
+
async function ensureExecutable(pathValue) {
|
|
115
|
+
try {
|
|
116
|
+
await fs.access(pathValue, fsConstants.X_OK);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new Error(`Browser executable is not accessible: ${pathValue}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Verify that the requested browser profile directory exists and is writable.
|
|
124
|
+
*/
|
|
125
|
+
async function ensureDirectory(pathValue) {
|
|
126
|
+
await fs.mkdir(pathValue, { recursive: true });
|
|
127
|
+
try {
|
|
128
|
+
await fs.access(pathValue, fsConstants.R_OK | fsConstants.W_OK);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw new Error(`Browser profile directory is not readable and writable: ${pathValue}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Locate a known browser executable on the current machine.
|
|
136
|
+
*/
|
|
137
|
+
async function resolveExecutablePath(browser) {
|
|
138
|
+
const candidates = KNOWN_BROWSERS[browser].executablePaths ?? [];
|
|
139
|
+
for (const candidate of candidates) {
|
|
140
|
+
try {
|
|
141
|
+
await fs.access(candidate, fsConstants.X_OK);
|
|
142
|
+
return candidate;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Try the next configured candidate.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Build Playwright launch options for an ephemeral browser instance.
|
|
152
|
+
*/
|
|
153
|
+
async function buildLaunchOptions(config) {
|
|
154
|
+
const options = {
|
|
155
|
+
headless: !config.headed,
|
|
156
|
+
timeout: config.timeoutMs,
|
|
38
157
|
args: [
|
|
39
158
|
'--disable-blink-features=AutomationControlled',
|
|
40
159
|
'--no-sandbox',
|
|
@@ -45,57 +164,198 @@ export async function ensureBrowser(opts = {}) {
|
|
|
45
164
|
'--no-first-run',
|
|
46
165
|
'--no-default-browser-check',
|
|
47
166
|
'--disable-default-apps',
|
|
48
|
-
'--disable-extensions',
|
|
49
167
|
],
|
|
50
|
-
}
|
|
51
|
-
|
|
168
|
+
};
|
|
169
|
+
if (config.browserPath) {
|
|
170
|
+
options.executablePath = config.browserPath;
|
|
171
|
+
return options;
|
|
172
|
+
}
|
|
173
|
+
const executablePath = await resolveExecutablePath(config.browser);
|
|
174
|
+
if (executablePath) {
|
|
175
|
+
options.executablePath = executablePath;
|
|
176
|
+
return options;
|
|
177
|
+
}
|
|
178
|
+
const channel = KNOWN_BROWSERS[config.browser].channel;
|
|
179
|
+
if (channel) {
|
|
180
|
+
options.channel = channel;
|
|
181
|
+
return options;
|
|
182
|
+
}
|
|
183
|
+
if (config.browser !== 'playwright') {
|
|
184
|
+
throw new Error(`Could not locate a ${config.browser} executable. Install it or pass --browser-path.`);
|
|
185
|
+
}
|
|
186
|
+
return options;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Build new-context options, restoring saved storage state when available.
|
|
190
|
+
*/
|
|
191
|
+
function buildContextOptions(storageState) {
|
|
192
|
+
const options = {
|
|
52
193
|
viewport: { width: 1280, height: 800 },
|
|
53
194
|
userAgent: UA,
|
|
54
195
|
locale: 'en-US',
|
|
55
196
|
timezoneId: 'America/New_York',
|
|
56
197
|
extraHTTPHeaders: { 'Accept-Language': 'en-US,en;q=0.9' },
|
|
57
198
|
ignoreHTTPSErrors: true,
|
|
58
|
-
}
|
|
199
|
+
};
|
|
200
|
+
if (storageState) {
|
|
201
|
+
options.storageState = storageState;
|
|
202
|
+
}
|
|
203
|
+
return options;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Apply veil's anti-detection browser shims to a browser context.
|
|
207
|
+
*/
|
|
208
|
+
async function applyStealthContext(context) {
|
|
59
209
|
await context.addInitScript(() => {
|
|
60
|
-
// Spoof all automation detection vectors
|
|
61
210
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
62
211
|
Object.defineProperty(navigator, 'chromeapp', { get: () => undefined });
|
|
63
212
|
window.chrome = { runtime: {} };
|
|
64
213
|
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
|
|
65
214
|
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
query: () => Promise.resolve({ state: Notification.permission })
|
|
215
|
+
const permissionShim = {
|
|
216
|
+
query: (_permissionDesc) => Promise.resolve({ state: Notification.permission }),
|
|
69
217
|
};
|
|
70
|
-
|
|
218
|
+
Object.defineProperty(window.navigator, 'permissions', {
|
|
219
|
+
configurable: true,
|
|
220
|
+
value: permissionShim,
|
|
221
|
+
});
|
|
71
222
|
const originalToString = Function.prototype.toString;
|
|
72
|
-
Function.prototype.toString = function () {
|
|
73
|
-
if (this ===
|
|
223
|
+
Function.prototype.toString = function toString() {
|
|
224
|
+
if (this === permissionShim.query) {
|
|
74
225
|
return 'function query() { [native code] }';
|
|
75
226
|
}
|
|
76
227
|
return originalToString.call(this);
|
|
77
228
|
};
|
|
78
229
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Open a fresh browser instance for one-shot veil operations.
|
|
233
|
+
*/
|
|
234
|
+
async function launchEphemeralBrowser(config, storageState) {
|
|
235
|
+
const browser = await chromium.launch(await buildLaunchOptions(config));
|
|
236
|
+
const context = await browser.newContext(buildContextOptions(storageState));
|
|
237
|
+
await applyStealthContext(context);
|
|
82
238
|
const page = await context.newPage();
|
|
83
239
|
_browser = browser;
|
|
84
240
|
_context = context;
|
|
85
241
|
_page = page;
|
|
86
|
-
|
|
87
|
-
|
|
242
|
+
_closeTarget = 'browser';
|
|
243
|
+
return { browser, context, page };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Open a persistent browser profile backed by a user-data directory.
|
|
247
|
+
*/
|
|
248
|
+
async function launchPersistentBrowser(config) {
|
|
249
|
+
if (!config.userDataDir) {
|
|
250
|
+
throw new Error('Persistent browser launch requires a user data directory.');
|
|
251
|
+
}
|
|
252
|
+
const persistentOptions = {
|
|
253
|
+
...(await buildLaunchOptions(config)),
|
|
254
|
+
...buildContextOptions(null),
|
|
255
|
+
};
|
|
256
|
+
const context = await chromium.launchPersistentContext(config.userDataDir, persistentOptions);
|
|
257
|
+
await applyStealthContext(context);
|
|
258
|
+
const browser = context.browser();
|
|
259
|
+
if (!browser) {
|
|
260
|
+
await context.close();
|
|
261
|
+
throw new Error('Persistent browser launch succeeded but no browser handle was returned.');
|
|
262
|
+
}
|
|
263
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
264
|
+
_browser = browser;
|
|
265
|
+
_context = context;
|
|
266
|
+
_page = page;
|
|
267
|
+
_closeTarget = 'context';
|
|
268
|
+
return { browser, context, page };
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Attach to a manually launched Chromium browser via CDP.
|
|
272
|
+
*/
|
|
273
|
+
async function connectToExistingBrowser(config) {
|
|
274
|
+
if (!config.cdpUrl) {
|
|
275
|
+
throw new Error('CDP browser attach requires a CDP URL.');
|
|
276
|
+
}
|
|
277
|
+
const browser = await chromium.connectOverCDP(config.cdpUrl, { timeout: config.timeoutMs });
|
|
278
|
+
const context = browser.contexts()[0];
|
|
279
|
+
if (!context) {
|
|
280
|
+
await browser.close().catch(() => { });
|
|
281
|
+
throw new Error(`No browser context is available at ${config.cdpUrl}. Open a normal tab in the target browser and try again.`);
|
|
282
|
+
}
|
|
283
|
+
await applyStealthContext(context);
|
|
284
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
285
|
+
_browser = browser;
|
|
286
|
+
_context = context;
|
|
287
|
+
_page = page;
|
|
288
|
+
_closeTarget = 'none';
|
|
88
289
|
return { browser, context, page };
|
|
89
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Register a single process-exit hook that respects veil's close policy.
|
|
293
|
+
*/
|
|
294
|
+
function registerExitHandler() {
|
|
295
|
+
if (_exitHandlerRegistered)
|
|
296
|
+
return;
|
|
297
|
+
_exitHandlerRegistered = true;
|
|
298
|
+
process.once('exit', () => {
|
|
299
|
+
void closeBrowser();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Ensure a browser session is available, creating or attaching as needed.
|
|
304
|
+
*/
|
|
305
|
+
export async function ensureBrowser(opts = {}) {
|
|
306
|
+
if (_browser?.isConnected() && _context && _page && !_page.isClosed()) {
|
|
307
|
+
return { browser: _browser, context: _context, page: _page };
|
|
308
|
+
}
|
|
309
|
+
const config = await resolveBrowserOptions(opts);
|
|
310
|
+
const storageState = config.platform ? await loadSession(config.platform).catch(() => null) : null;
|
|
311
|
+
registerExitHandler();
|
|
312
|
+
if (config.cdpUrl) {
|
|
313
|
+
return connectToExistingBrowser(config);
|
|
314
|
+
}
|
|
315
|
+
if (config.userDataDir) {
|
|
316
|
+
return launchPersistentBrowser(config);
|
|
317
|
+
}
|
|
318
|
+
return launchEphemeralBrowser(config, storageState);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Return the current live page if the browser is still open.
|
|
322
|
+
*/
|
|
90
323
|
export async function getPage() {
|
|
91
|
-
return
|
|
324
|
+
return _page && !_page.isClosed() ? _page : null;
|
|
92
325
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Persist the current storage state when requested and close veil-owned resources.
|
|
328
|
+
*/
|
|
329
|
+
export async function closeBrowser(platform) {
|
|
330
|
+
if (_context && platform) {
|
|
331
|
+
try {
|
|
332
|
+
await saveSession(platform, await _context.storageState());
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// Do not let session save errors block cleanup.
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
if (_closeTarget === 'context' && _context) {
|
|
340
|
+
await _context.close();
|
|
341
|
+
}
|
|
342
|
+
else if (_closeTarget === 'browser' && _browser) {
|
|
343
|
+
await _browser.close();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// Ignore cleanup failures; the process may already be tearing down.
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
_browser = null;
|
|
351
|
+
_context = null;
|
|
352
|
+
_page = null;
|
|
353
|
+
_closeTarget = 'browser';
|
|
354
|
+
}
|
|
98
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Sleep for a short human-like randomized delay.
|
|
358
|
+
*/
|
|
99
359
|
export function humanDelay(min = 400, max = 900) {
|
|
100
|
-
return new Promise(
|
|
360
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, Math.floor(Math.random() * (max - min) + min)));
|
|
101
361
|
}
|