@ulpi/browse 1.3.1 → 1.3.2

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/browse.cjs CHANGED
@@ -62,318 +62,631 @@ var init_constants = __esm({
62
62
  }
63
63
  });
64
64
 
65
- // src/chrome-discover.ts
66
- var chrome_discover_exports = {};
67
- __export(chrome_discover_exports, {
68
- discoverChrome: () => discoverChrome,
69
- fetchWsUrl: () => fetchWsUrl,
70
- findChromeExecutable: () => findChromeExecutable,
71
- getChromeUserDataDir: () => getChromeUserDataDir,
72
- isChromeRunning: () => isChromeRunning,
73
- launchChrome: () => launchChrome
65
+ // src/cookie-import.ts
66
+ var cookie_import_exports = {};
67
+ __export(cookie_import_exports, {
68
+ CookieImportError: () => CookieImportError,
69
+ findInstalledBrowsers: () => findInstalledBrowsers,
70
+ importCookies: () => importCookies,
71
+ listDomains: () => listDomains
74
72
  });
75
- async function fetchWsUrl(port) {
73
+ function findInstalledBrowsers() {
74
+ const appSupport = path2.join(os.homedir(), "Library", "Application Support");
75
+ return BROWSER_REGISTRY.filter((b) => {
76
+ const dbPath = path2.join(appSupport, b.dataDir, "Default", "Cookies");
77
+ try {
78
+ return fs2.existsSync(dbPath);
79
+ } catch {
80
+ return false;
81
+ }
82
+ });
83
+ }
84
+ function listDomains(browserName, profile = "Default") {
85
+ const browser2 = resolveBrowser(browserName);
86
+ const dbPath = getCookieDbPath(browser2, profile);
87
+ const db = openDb(dbPath, browser2.name);
76
88
  try {
77
- const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
78
- signal: AbortSignal.timeout(2e3)
79
- });
80
- if (!res.ok) return null;
81
- const data = await res.json();
82
- return data.webSocketDebuggerUrl ?? null;
83
- } catch {
84
- return null;
89
+ const now = chromiumNow();
90
+ const rows = db.prepare(
91
+ `SELECT host_key AS domain, COUNT(*) AS count
92
+ FROM cookies
93
+ WHERE has_expires = 0 OR expires_utc > ?
94
+ GROUP BY host_key
95
+ ORDER BY count DESC`
96
+ ).all(now);
97
+ return { domains: rows, browser: browser2.name };
98
+ } finally {
99
+ db.close();
85
100
  }
86
101
  }
87
- function readDevToolsPort(filePath) {
102
+ async function importCookies(browserName, domains, profile = "Default") {
103
+ if (domains.length === 0) return { cookies: [], count: 0, failed: 0, domainCounts: {} };
104
+ const browser2 = resolveBrowser(browserName);
105
+ const derivedKey = await getDerivedKey(browser2);
106
+ const dbPath = getCookieDbPath(browser2, profile);
107
+ const db = openDb(dbPath, browser2.name);
88
108
  try {
89
- const content = fs2.readFileSync(filePath, "utf-8");
90
- const port = parseInt(content.split("\n")[0], 10);
91
- return Number.isFinite(port) && port > 0 ? port : null;
92
- } catch {
93
- return null;
109
+ const now = chromiumNow();
110
+ const placeholders = domains.map(() => "?").join(",");
111
+ const rows = db.prepare(
112
+ `SELECT host_key, name, value, encrypted_value, path, expires_utc,
113
+ is_secure, is_httponly, has_expires, samesite
114
+ FROM cookies
115
+ WHERE host_key IN (${placeholders})
116
+ AND (has_expires = 0 OR expires_utc > ?)
117
+ ORDER BY host_key, name`
118
+ ).all(...domains, now);
119
+ const cookies = [];
120
+ let failed = 0;
121
+ const domainCounts = {};
122
+ for (const row of rows) {
123
+ try {
124
+ const value = decryptCookieValue(row, derivedKey);
125
+ const cookie = toPlaywrightCookie(row, value);
126
+ cookies.push(cookie);
127
+ domainCounts[row.host_key] = (domainCounts[row.host_key] || 0) + 1;
128
+ } catch {
129
+ failed++;
130
+ }
131
+ }
132
+ return { cookies, count: cookies.length, failed, domainCounts };
133
+ } finally {
134
+ db.close();
94
135
  }
95
136
  }
96
- async function discoverChrome() {
97
- const home = os.homedir();
98
- for (const profile of PROFILE_PATHS) {
99
- const filePath = process.platform === "darwin" ? path2.join(home, "Library", "Application Support", profile, "DevToolsActivePort") : path2.join(home, ".config", profile.toLowerCase().replace(/ /g, "-"), "DevToolsActivePort");
100
- const port = readDevToolsPort(filePath);
101
- if (port) {
102
- const wsUrl = await fetchWsUrl(port);
103
- if (wsUrl) return wsUrl;
104
- }
137
+ function resolveBrowser(nameOrAlias) {
138
+ const needle = nameOrAlias.toLowerCase().trim();
139
+ const found = BROWSER_REGISTRY.find(
140
+ (b) => b.aliases.includes(needle) || b.name.toLowerCase() === needle
141
+ );
142
+ if (!found) {
143
+ const supported = BROWSER_REGISTRY.flatMap((b) => b.aliases).join(", ");
144
+ throw new CookieImportError(
145
+ `Unknown browser '${nameOrAlias}'. Supported: ${supported}`,
146
+ "unknown_browser"
147
+ );
105
148
  }
106
- for (const port of PROBE_PORTS) {
107
- const wsUrl = await fetchWsUrl(port);
108
- if (wsUrl) return wsUrl;
149
+ return found;
150
+ }
151
+ function validateProfile(profile) {
152
+ if (/[/\\]|\.\./.test(profile) || /[\x00-\x1f]/.test(profile)) {
153
+ throw new CookieImportError(
154
+ `Invalid profile name: '${profile}'`,
155
+ "bad_request"
156
+ );
109
157
  }
110
- return null;
111
158
  }
112
- function findChromeExecutable() {
113
- const envPath = process.env.BROWSE_CHROME_PATH;
114
- if (envPath) {
115
- if (fs2.existsSync(envPath)) return envPath;
116
- return null;
159
+ function getCookieDbPath(browser2, profile) {
160
+ validateProfile(profile);
161
+ const appSupport = path2.join(os.homedir(), "Library", "Application Support");
162
+ const dbPath = path2.join(appSupport, browser2.dataDir, profile, "Cookies");
163
+ if (!fs2.existsSync(dbPath)) {
164
+ throw new CookieImportError(
165
+ `${browser2.name} is not installed (no cookie database at ${dbPath})`,
166
+ "not_installed"
167
+ );
117
168
  }
118
- if (process.platform === "darwin") {
119
- const candidates = [
120
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
121
- "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
122
- ];
123
- for (const c of candidates) {
124
- if (fs2.existsSync(c)) return c;
169
+ return dbPath;
170
+ }
171
+ function openDb(dbPath, browserName) {
172
+ try {
173
+ return new import_better_sqlite3.default(dbPath, { readonly: true });
174
+ } catch (err) {
175
+ if (err.message?.includes("SQLITE_BUSY") || err.message?.includes("database is locked")) {
176
+ return openDbFromCopy(dbPath, browserName);
125
177
  }
126
- } else if (process.platform === "linux") {
127
- const names = ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"];
128
- for (const name of names) {
129
- try {
130
- const result = (0, import_child_process.execSync)(`which ${name}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
131
- if (result) return result;
132
- } catch {
133
- }
178
+ if (err.message?.includes("SQLITE_CORRUPT") || err.message?.includes("malformed")) {
179
+ throw new CookieImportError(
180
+ "Cookie database is corrupt",
181
+ "db_corrupt"
182
+ );
134
183
  }
184
+ throw err;
135
185
  }
136
- return null;
137
186
  }
138
- function isChromeRunning() {
139
- if (process.platform !== "darwin" && process.platform !== "linux") {
140
- return { running: false, hasDebugPort: false };
141
- }
187
+ function openDbFromCopy(dbPath, browserName) {
188
+ const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}-${crypto.randomUUID()}.db`;
142
189
  try {
143
- const pgrepPattern = process.platform === "darwin" ? "Google Chrome" : "google-chrome|chromium";
144
- const pids = (0, import_child_process.execSync)(`pgrep -f "${pgrepPattern}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n").filter(Boolean);
145
- if (pids.length === 0) return { running: false, hasDebugPort: false };
146
- for (const pid of pids) {
190
+ fs2.copyFileSync(dbPath, tmpPath);
191
+ const walPath = dbPath + "-wal";
192
+ const shmPath = dbPath + "-shm";
193
+ if (fs2.existsSync(walPath)) fs2.copyFileSync(walPath, tmpPath + "-wal");
194
+ if (fs2.existsSync(shmPath)) fs2.copyFileSync(shmPath, tmpPath + "-shm");
195
+ const db = new import_better_sqlite3.default(tmpPath, { readonly: true });
196
+ const origClose = db.close.bind(db);
197
+ db.close = (() => {
198
+ origClose();
147
199
  try {
148
- const cmdline = (0, import_child_process.execSync)(`ps -p ${pid} -o command=`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
149
- const match = cmdline.match(/--remote-debugging-port=(\d+)/);
150
- if (match) {
151
- return { running: true, hasDebugPort: true, debugPort: parseInt(match[1], 10) };
152
- }
200
+ fs2.unlinkSync(tmpPath);
153
201
  } catch {
154
202
  }
155
- }
156
- return { running: true, hasDebugPort: false };
157
- } catch {
158
- return { running: false, hasDebugPort: false };
159
- }
160
- }
161
- function getChromeUserDataDir() {
162
- const home = os.homedir();
163
- if (process.platform === "darwin") {
164
- const dir = path2.join(home, "Library", "Application Support", "Google", "Chrome");
165
- if (fs2.existsSync(dir)) return dir;
166
- } else if (process.platform === "linux") {
167
- const dir = path2.join(home, ".config", "google-chrome");
168
- if (fs2.existsSync(dir)) return dir;
169
- }
170
- return null;
171
- }
172
- function isPortFree(port) {
173
- return new Promise((resolve4) => {
174
- const srv = net.createServer();
175
- srv.once("error", () => resolve4(false));
176
- srv.once("listening", () => {
177
- srv.close(() => resolve4(true));
178
- });
179
- srv.listen(port, "127.0.0.1");
180
- });
181
- }
182
- async function pickDebugPort() {
183
- if (await isPortFree(DEFAULTS.CHROME_DEBUG_PORT)) return DEFAULTS.CHROME_DEBUG_PORT;
184
- for (let port = DEFAULTS.PORT_RANGE_START; port <= DEFAULTS.PORT_RANGE_END; port++) {
185
- if (await isPortFree(port)) return port;
186
- }
187
- throw new Error("No available port for Chrome debug port");
188
- }
189
- function cleanStaleLock(dataDir) {
190
- const lockFiles = ["SingletonLock", "lockfile"];
191
- for (const name of lockFiles) {
192
- const lockPath = path2.join(dataDir, name);
193
- try {
194
- const target = fs2.readlinkSync(lockPath);
195
- const pidMatch = target.match(/-(\d+)$/);
196
- if (pidMatch) {
197
- const pid = parseInt(pidMatch[1], 10);
198
- try {
199
- process.kill(pid, 0);
200
- } catch {
201
- fs2.unlinkSync(lockPath);
202
- }
203
+ try {
204
+ fs2.unlinkSync(tmpPath + "-wal");
205
+ } catch {
203
206
  }
204
- } catch {
205
207
  try {
206
- fs2.unlinkSync(lockPath);
208
+ fs2.unlinkSync(tmpPath + "-shm");
207
209
  } catch {
208
210
  }
211
+ });
212
+ return db;
213
+ } catch {
214
+ try {
215
+ fs2.unlinkSync(tmpPath);
216
+ } catch {
209
217
  }
210
- }
211
- }
212
- async function launchChrome() {
213
- const chromePath = findChromeExecutable();
214
- if (!chromePath) {
215
- throw new Error(
216
- "Chrome not found. Install Google Chrome or set BROWSE_CHROME_PATH environment variable."
218
+ throw new CookieImportError(
219
+ `Cookie database is locked (${browserName} may be running). Try closing ${browserName} first.`,
220
+ "db_locked",
221
+ "retry"
217
222
  );
218
223
  }
219
- const status = isChromeRunning();
220
- if (status.running && status.hasDebugPort && status.debugPort) {
221
- const wsUrl2 = await fetchWsUrl(status.debugPort);
222
- if (wsUrl2) {
223
- const pw2 = await import("playwright");
224
- const browser3 = await pw2.chromium.connectOverCDP(wsUrl2);
225
- return { browser: browser3, child: null, close: async () => {
226
- } };
227
- }
228
- }
229
- const port = await pickDebugPort();
230
- const localDir = process.env.BROWSE_LOCAL_DIR || ".browse";
231
- const dataDir = path2.join(localDir, "chrome-data");
232
- const realDataDir = getChromeUserDataDir();
233
- if (realDataDir && !fs2.existsSync(path2.join(dataDir, "Default", "Preferences"))) {
234
- console.log("[browse] Copying Chrome profile (first-time setup)...");
235
- fs2.mkdirSync(dataDir, { recursive: true });
236
- (0, import_child_process.execSync)(`cp -a "${realDataDir}/Default" "${dataDir}/Default"`, { stdio: "pipe", timeout: 3e4 });
237
- for (const f of ["Local State", "First Run"]) {
238
- const src = path2.join(realDataDir, f);
239
- if (fs2.existsSync(src)) fs2.copyFileSync(src, path2.join(dataDir, f));
240
- }
241
- console.log("[browse] Profile copied.");
242
- } else {
243
- fs2.mkdirSync(dataDir, { recursive: true });
244
- }
245
- cleanStaleLock(dataDir);
246
- const chromeArgs = [
247
- `--remote-debugging-port=${port}`,
248
- "--no-first-run",
249
- "--no-default-browser-check",
250
- `--user-data-dir=${dataDir}`
251
- ];
252
- const child = (0, import_child_process.spawn)(chromePath, chromeArgs, {
253
- stdio: ["ignore", "pipe", "pipe"],
254
- detached: false
255
- });
256
- let stderrData = "";
257
- if (child.stderr) {
258
- child.stderr.setEncoding("utf8");
259
- child.stderr.on("data", (chunk) => {
260
- stderrData += chunk;
261
- });
262
- }
263
- const deadline = Date.now() + DEFAULTS.CHROME_CDP_TIMEOUT_MS;
264
- let wsUrl;
265
- while (Date.now() < deadline) {
266
- if (child.exitCode !== null) {
267
- throw new Error(
268
- `Chrome exited before CDP became ready (exit code: ${child.exitCode})` + (stderrData ? `
269
- stderr: ${stderrData.slice(0, 2e3)}` : "")
224
+ }
225
+ async function getDerivedKey(browser2) {
226
+ const cached = keyCache.get(browser2.keychainService);
227
+ if (cached) return cached;
228
+ const password = await getKeychainPassword(browser2.keychainService);
229
+ const derived = crypto.pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
230
+ keyCache.set(browser2.keychainService, derived);
231
+ return derived;
232
+ }
233
+ async function getKeychainPassword(service) {
234
+ const proc = (0, import_child_process.spawn)("security", ["find-generic-password", "-s", service, "-w"], {
235
+ stdio: ["ignore", "pipe", "pipe"]
236
+ });
237
+ let stdout = "";
238
+ let stderr = "";
239
+ proc.stdout.setEncoding("utf8");
240
+ proc.stderr.setEncoding("utf8");
241
+ proc.stdout.on("data", (chunk) => {
242
+ stdout += chunk;
243
+ });
244
+ proc.stderr.on("data", (chunk) => {
245
+ stderr += chunk;
246
+ });
247
+ const exitPromise = new Promise(
248
+ (resolve4) => proc.on("close", (code) => resolve4(code))
249
+ );
250
+ const timeout = new Promise(
251
+ (_, reject) => setTimeout(() => {
252
+ proc.kill();
253
+ reject(new CookieImportError(
254
+ `macOS is waiting for Keychain permission. Look for a dialog asking to allow access to "${service}".`,
255
+ "keychain_timeout",
256
+ "retry"
257
+ ));
258
+ }, 1e4)
259
+ );
260
+ try {
261
+ const exitCode = await Promise.race([exitPromise, timeout]);
262
+ if (exitCode !== 0) {
263
+ const errText = stderr.trim().toLowerCase();
264
+ if (errText.includes("user canceled") || errText.includes("denied") || errText.includes("interaction not allowed")) {
265
+ throw new CookieImportError(
266
+ `Keychain access denied. Click Allow in the macOS dialog for "${service}".`,
267
+ "keychain_denied",
268
+ "retry"
269
+ );
270
+ }
271
+ if (errText.includes("could not be found") || errText.includes("not found")) {
272
+ throw new CookieImportError(
273
+ `No Keychain entry for "${service}".`,
274
+ "keychain_not_found"
275
+ );
276
+ }
277
+ throw new CookieImportError(
278
+ `Could not read Keychain: ${stderr.trim()}`,
279
+ "keychain_error",
280
+ "retry"
270
281
  );
271
282
  }
272
- const url = await fetchWsUrl(port);
273
- if (url) {
274
- wsUrl = url;
275
- break;
276
- }
277
- await new Promise((r) => setTimeout(r, 200));
283
+ return stdout.trim();
284
+ } catch (err) {
285
+ if (err instanceof CookieImportError) throw err;
286
+ throw new CookieImportError(
287
+ `Could not read Keychain: ${err.message}`,
288
+ "keychain_error",
289
+ "retry"
290
+ );
278
291
  }
279
- if (!wsUrl) {
280
- child.kill();
281
- throw new Error(`Chrome failed to start on port ${port} within ${DEFAULTS.CHROME_CDP_TIMEOUT_MS / 1e3} seconds`);
292
+ }
293
+ function decryptCookieValue(row, key) {
294
+ if (row.value && row.value.length > 0) return row.value;
295
+ const ev = Buffer.from(row.encrypted_value);
296
+ if (ev.length === 0) return "";
297
+ const prefix = ev.slice(0, 3).toString("utf-8");
298
+ if (prefix !== "v10") {
299
+ throw new Error(`Unknown encryption prefix: ${prefix}`);
282
300
  }
283
- const pw = await import("playwright");
284
- const browser2 = await pw.chromium.connectOverCDP(wsUrl);
301
+ const ciphertext = ev.slice(3);
302
+ const iv = Buffer.alloc(16, 32);
303
+ const decipher = crypto.createDecipheriv("aes-128-cbc", key, iv);
304
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
305
+ if (plaintext.length <= 32) return "";
306
+ return plaintext.slice(32).toString("utf-8");
307
+ }
308
+ function toPlaywrightCookie(row, value) {
285
309
  return {
286
- browser: browser2,
287
- child,
288
- close: async () => {
289
- await browser2.close().catch(() => {
290
- });
291
- child.kill("SIGTERM");
292
- }
310
+ name: row.name,
311
+ value,
312
+ domain: row.host_key,
313
+ path: row.path || "/",
314
+ expires: chromiumEpochToUnix(row.expires_utc, row.has_expires),
315
+ secure: row.is_secure === 1,
316
+ httpOnly: row.is_httponly === 1,
317
+ sameSite: mapSameSite(row.samesite)
293
318
  };
294
319
  }
295
- var os, fs2, path2, net, import_child_process, PROFILE_PATHS, PROBE_PORTS;
296
- var init_chrome_discover = __esm({
297
- "src/chrome-discover.ts"() {
320
+ function chromiumNow() {
321
+ return BigInt(Date.now()) * 1000n + CHROMIUM_EPOCH_OFFSET;
322
+ }
323
+ function chromiumEpochToUnix(epoch, hasExpires) {
324
+ if (hasExpires === 0 || epoch === 0 || epoch === 0n) return -1;
325
+ const epochBig = BigInt(epoch);
326
+ const unixMicro = epochBig - CHROMIUM_EPOCH_OFFSET;
327
+ return Number(unixMicro / 1000000n);
328
+ }
329
+ function mapSameSite(value) {
330
+ switch (value) {
331
+ case 0:
332
+ return "None";
333
+ case 1:
334
+ return "Lax";
335
+ case 2:
336
+ return "Strict";
337
+ default:
338
+ return "Lax";
339
+ }
340
+ }
341
+ var import_better_sqlite3, import_child_process, crypto, fs2, path2, os, CookieImportError, BROWSER_REGISTRY, keyCache, CHROMIUM_EPOCH_OFFSET;
342
+ var init_cookie_import = __esm({
343
+ "src/cookie-import.ts"() {
298
344
  "use strict";
299
- os = __toESM(require("os"), 1);
345
+ import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
346
+ import_child_process = require("child_process");
347
+ crypto = __toESM(require("crypto"), 1);
300
348
  fs2 = __toESM(require("fs"), 1);
301
349
  path2 = __toESM(require("path"), 1);
302
- net = __toESM(require("net"), 1);
303
- import_child_process = require("child_process");
304
- init_constants();
305
- PROFILE_PATHS = [
306
- "Google/Chrome",
307
- "Arc/User Data",
308
- "BraveSoftware/Brave-Browser",
309
- "Microsoft Edge"
350
+ os = __toESM(require("os"), 1);
351
+ CookieImportError = class extends Error {
352
+ constructor(message, code, action) {
353
+ super(message);
354
+ this.code = code;
355
+ this.action = action;
356
+ this.name = "CookieImportError";
357
+ }
358
+ };
359
+ BROWSER_REGISTRY = [
360
+ { name: "Chrome", dataDir: "Google/Chrome/", keychainService: "Chrome Safe Storage", aliases: ["chrome", "google-chrome"] },
361
+ { name: "Arc", dataDir: "Arc/User Data/", keychainService: "Arc Safe Storage", aliases: ["arc"] },
362
+ { name: "Brave", dataDir: "BraveSoftware/Brave-Browser/", keychainService: "Brave Safe Storage", aliases: ["brave"] },
363
+ { name: "Edge", dataDir: "Microsoft Edge/", keychainService: "Microsoft Edge Safe Storage", aliases: ["edge"] }
310
364
  ];
311
- PROBE_PORTS = [9222, 9229];
365
+ keyCache = /* @__PURE__ */ new Map();
366
+ CHROMIUM_EPOCH_OFFSET = 11644473600000000n;
312
367
  }
313
368
  });
314
369
 
315
- // src/encryption.ts
316
- function resolveEncryptionKey(localDir) {
317
- const envKey = process.env.BROWSE_ENCRYPTION_KEY;
318
- if (envKey) {
319
- if (envKey.length !== 64) {
320
- throw new Error("BROWSE_ENCRYPTION_KEY must be 64 hex characters (32 bytes)");
321
- }
322
- return Buffer.from(envKey, "hex");
323
- }
324
- const keyPath = path3.join(localDir, ".encryption-key");
325
- if (fs3.existsSync(keyPath)) {
326
- const hex = fs3.readFileSync(keyPath, "utf-8").trim();
327
- return Buffer.from(hex, "hex");
370
+ // src/chrome-discover.ts
371
+ var chrome_discover_exports = {};
372
+ __export(chrome_discover_exports, {
373
+ discoverChrome: () => discoverChrome,
374
+ fetchWsUrl: () => fetchWsUrl,
375
+ findChromeExecutable: () => findChromeExecutable,
376
+ getChromeUserDataDir: () => getChromeUserDataDir,
377
+ isChromeRunning: () => isChromeRunning,
378
+ launchChrome: () => launchChrome
379
+ });
380
+ async function fetchWsUrl(port) {
381
+ try {
382
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
383
+ signal: AbortSignal.timeout(2e3)
384
+ });
385
+ if (!res.ok) return null;
386
+ const data = await res.json();
387
+ return data.webSocketDebuggerUrl ?? null;
388
+ } catch {
389
+ return null;
328
390
  }
329
- const key = crypto.randomBytes(32);
330
- fs3.writeFileSync(keyPath, key.toString("hex") + "\n", { mode: 384 });
331
- return key;
332
- }
333
- function encrypt(plaintext, key) {
334
- const iv = crypto.randomBytes(12);
335
- const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
336
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
337
- return {
338
- ciphertext: encrypted.toString("base64"),
339
- iv: iv.toString("base64"),
340
- authTag: cipher.getAuthTag().toString("base64")
341
- };
342
391
  }
343
- function decrypt(ciphertext, iv, authTag, key) {
344
- const decipher = crypto.createDecipheriv(
345
- "aes-256-gcm",
346
- key,
347
- Buffer.from(iv, "base64")
348
- );
349
- decipher.setAuthTag(Buffer.from(authTag, "base64"));
350
- const decrypted = Buffer.concat([
351
- decipher.update(Buffer.from(ciphertext, "base64")),
352
- decipher.final()
353
- ]);
354
- return decrypted.toString("utf-8");
392
+ function readDevToolsPort(filePath) {
393
+ try {
394
+ const content = fs3.readFileSync(filePath, "utf-8");
395
+ const port = parseInt(content.split("\n")[0], 10);
396
+ return Number.isFinite(port) && port > 0 ? port : null;
397
+ } catch {
398
+ return null;
399
+ }
355
400
  }
356
- var crypto, fs3, path3;
357
- var init_encryption = __esm({
358
- "src/encryption.ts"() {
359
- "use strict";
360
- crypto = __toESM(require("crypto"), 1);
361
- fs3 = __toESM(require("fs"), 1);
362
- path3 = __toESM(require("path"), 1);
401
+ async function discoverChrome() {
402
+ const home = os2.homedir();
403
+ for (const profile of PROFILE_PATHS) {
404
+ const filePath = process.platform === "darwin" ? path3.join(home, "Library", "Application Support", profile, "DevToolsActivePort") : path3.join(home, ".config", profile.toLowerCase().replace(/ /g, "-"), "DevToolsActivePort");
405
+ const port = readDevToolsPort(filePath);
406
+ if (port) {
407
+ const wsUrl = await fetchWsUrl(port);
408
+ if (wsUrl) return wsUrl;
409
+ }
363
410
  }
364
- });
365
-
366
- // src/sanitize.ts
367
- function sanitizeName(name) {
368
- const sanitized = name.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
369
- if (!sanitized || /^[._]+$/.test(sanitized)) {
370
- throw new Error(`Invalid name: "${name}"`);
411
+ for (const port of PROBE_PORTS) {
412
+ const wsUrl = await fetchWsUrl(port);
413
+ if (wsUrl) return wsUrl;
371
414
  }
372
- return sanitized;
415
+ return null;
373
416
  }
374
- var init_sanitize = __esm({
375
- "src/sanitize.ts"() {
376
- "use strict";
417
+ function findChromeExecutable() {
418
+ const envPath = process.env.BROWSE_CHROME_PATH;
419
+ if (envPath) {
420
+ if (fs3.existsSync(envPath)) return envPath;
421
+ return null;
422
+ }
423
+ if (process.platform === "darwin") {
424
+ const candidates = [
425
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
426
+ "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
427
+ ];
428
+ for (const c of candidates) {
429
+ if (fs3.existsSync(c)) return c;
430
+ }
431
+ } else if (process.platform === "linux") {
432
+ const names = ["google-chrome", "google-chrome-stable", "chromium-browser", "chromium"];
433
+ for (const name of names) {
434
+ try {
435
+ const result = (0, import_child_process2.execSync)(`which ${name}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
436
+ if (result) return result;
437
+ } catch {
438
+ }
439
+ }
440
+ }
441
+ return null;
442
+ }
443
+ function isChromeRunning() {
444
+ if (process.platform !== "darwin" && process.platform !== "linux") {
445
+ return { running: false, hasDebugPort: false };
446
+ }
447
+ try {
448
+ const pgrepPattern = process.platform === "darwin" ? "Google Chrome" : "google-chrome|chromium";
449
+ const pids = (0, import_child_process2.execSync)(`pgrep -f "${pgrepPattern}"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n").filter(Boolean);
450
+ if (pids.length === 0) return { running: false, hasDebugPort: false };
451
+ for (const pid of pids) {
452
+ try {
453
+ const cmdline = (0, import_child_process2.execSync)(`ps -p ${pid} -o command=`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
454
+ const match = cmdline.match(/--remote-debugging-port=(\d+)/);
455
+ if (match) {
456
+ return { running: true, hasDebugPort: true, debugPort: parseInt(match[1], 10) };
457
+ }
458
+ } catch {
459
+ }
460
+ }
461
+ return { running: true, hasDebugPort: false };
462
+ } catch {
463
+ return { running: false, hasDebugPort: false };
464
+ }
465
+ }
466
+ function getChromeUserDataDir() {
467
+ const home = os2.homedir();
468
+ if (process.platform === "darwin") {
469
+ const dir = path3.join(home, "Library", "Application Support", "Google", "Chrome");
470
+ if (fs3.existsSync(dir)) return dir;
471
+ } else if (process.platform === "linux") {
472
+ const dir = path3.join(home, ".config", "google-chrome");
473
+ if (fs3.existsSync(dir)) return dir;
474
+ }
475
+ return null;
476
+ }
477
+ function isPortFree(port) {
478
+ return new Promise((resolve4) => {
479
+ const srv = net.createServer();
480
+ srv.once("error", () => resolve4(false));
481
+ srv.once("listening", () => {
482
+ srv.close(() => resolve4(true));
483
+ });
484
+ srv.listen(port, "127.0.0.1");
485
+ });
486
+ }
487
+ async function pickDebugPort() {
488
+ if (await isPortFree(DEFAULTS.CHROME_DEBUG_PORT)) return DEFAULTS.CHROME_DEBUG_PORT;
489
+ for (let port = DEFAULTS.PORT_RANGE_START; port <= DEFAULTS.PORT_RANGE_END; port++) {
490
+ if (await isPortFree(port)) return port;
491
+ }
492
+ throw new Error("No available port for Chrome debug port");
493
+ }
494
+ function cleanStaleLock(dataDir) {
495
+ const lockFiles = ["SingletonLock", "lockfile"];
496
+ for (const name of lockFiles) {
497
+ const lockPath = path3.join(dataDir, name);
498
+ try {
499
+ const target = fs3.readlinkSync(lockPath);
500
+ const pidMatch = target.match(/-(\d+)$/);
501
+ if (pidMatch) {
502
+ const pid = parseInt(pidMatch[1], 10);
503
+ try {
504
+ process.kill(pid, 0);
505
+ } catch {
506
+ fs3.unlinkSync(lockPath);
507
+ }
508
+ }
509
+ } catch {
510
+ try {
511
+ fs3.unlinkSync(lockPath);
512
+ } catch {
513
+ }
514
+ }
515
+ }
516
+ }
517
+ async function launchChrome() {
518
+ const chromePath = findChromeExecutable();
519
+ if (!chromePath) {
520
+ throw new Error(
521
+ "Chrome not found. Install Google Chrome or set BROWSE_CHROME_PATH environment variable."
522
+ );
523
+ }
524
+ const status = isChromeRunning();
525
+ if (status.running && status.hasDebugPort && status.debugPort) {
526
+ const wsUrl2 = await fetchWsUrl(status.debugPort);
527
+ if (wsUrl2) {
528
+ const pw2 = await import("playwright");
529
+ const browser3 = await pw2.chromium.connectOverCDP(wsUrl2);
530
+ return { browser: browser3, child: null, close: async () => {
531
+ } };
532
+ }
533
+ }
534
+ const port = await pickDebugPort();
535
+ const localDir = process.env.BROWSE_LOCAL_DIR || ".browse";
536
+ const dataDir = path3.join(localDir, "chrome-data");
537
+ const realDataDir = getChromeUserDataDir();
538
+ fs3.mkdirSync(dataDir, { recursive: true });
539
+ cleanStaleLock(dataDir);
540
+ const chromeArgs = [
541
+ `--remote-debugging-port=${port}`,
542
+ "--no-first-run",
543
+ "--no-default-browser-check",
544
+ `--user-data-dir=${dataDir}`
545
+ ];
546
+ const child = (0, import_child_process2.spawn)(chromePath, chromeArgs, {
547
+ stdio: ["ignore", "pipe", "pipe"],
548
+ detached: false
549
+ });
550
+ let stderrData = "";
551
+ if (child.stderr) {
552
+ child.stderr.setEncoding("utf8");
553
+ child.stderr.on("data", (chunk) => {
554
+ stderrData += chunk;
555
+ });
556
+ }
557
+ const deadline = Date.now() + DEFAULTS.CHROME_CDP_TIMEOUT_MS;
558
+ let wsUrl;
559
+ while (Date.now() < deadline) {
560
+ if (child.exitCode !== null) {
561
+ throw new Error(
562
+ `Chrome exited before CDP became ready (exit code: ${child.exitCode})` + (stderrData ? `
563
+ stderr: ${stderrData.slice(0, 2e3)}` : "")
564
+ );
565
+ }
566
+ const url = await fetchWsUrl(port);
567
+ if (url) {
568
+ wsUrl = url;
569
+ break;
570
+ }
571
+ await new Promise((r) => setTimeout(r, 200));
572
+ }
573
+ if (!wsUrl) {
574
+ child.kill();
575
+ throw new Error(`Chrome failed to start on port ${port} within ${DEFAULTS.CHROME_CDP_TIMEOUT_MS / 1e3} seconds`);
576
+ }
577
+ const pw = await import("playwright");
578
+ const browser2 = await pw.chromium.connectOverCDP(wsUrl);
579
+ if (realDataDir) {
580
+ try {
581
+ const { importCookies: importCookies2, listDomains: listDomains2 } = await Promise.resolve().then(() => (init_cookie_import(), cookie_import_exports));
582
+ const { domains } = listDomains2("chrome");
583
+ if (domains.length > 0) {
584
+ const allDomains = domains.map((d) => d.domain);
585
+ const result = await importCookies2("chrome", allDomains);
586
+ if (result.cookies.length > 0) {
587
+ const context = browser2.contexts()[0];
588
+ if (context) {
589
+ await context.addCookies(result.cookies);
590
+ console.log(`[browse] Imported ${result.cookies.length} cookies from Chrome.`);
591
+ }
592
+ }
593
+ }
594
+ } catch (err) {
595
+ console.log(`[browse] Cookie import skipped: ${err.message}`);
596
+ }
597
+ }
598
+ return {
599
+ browser: browser2,
600
+ child,
601
+ close: async () => {
602
+ await browser2.close().catch(() => {
603
+ });
604
+ child.kill("SIGTERM");
605
+ }
606
+ };
607
+ }
608
+ var os2, fs3, path3, net, import_child_process2, PROFILE_PATHS, PROBE_PORTS;
609
+ var init_chrome_discover = __esm({
610
+ "src/chrome-discover.ts"() {
611
+ "use strict";
612
+ os2 = __toESM(require("os"), 1);
613
+ fs3 = __toESM(require("fs"), 1);
614
+ path3 = __toESM(require("path"), 1);
615
+ net = __toESM(require("net"), 1);
616
+ import_child_process2 = require("child_process");
617
+ init_constants();
618
+ PROFILE_PATHS = [
619
+ "Google/Chrome",
620
+ "Arc/User Data",
621
+ "BraveSoftware/Brave-Browser",
622
+ "Microsoft Edge"
623
+ ];
624
+ PROBE_PORTS = [9222, 9229];
625
+ }
626
+ });
627
+
628
+ // src/encryption.ts
629
+ function resolveEncryptionKey(localDir) {
630
+ const envKey = process.env.BROWSE_ENCRYPTION_KEY;
631
+ if (envKey) {
632
+ if (envKey.length !== 64) {
633
+ throw new Error("BROWSE_ENCRYPTION_KEY must be 64 hex characters (32 bytes)");
634
+ }
635
+ return Buffer.from(envKey, "hex");
636
+ }
637
+ const keyPath = path4.join(localDir, ".encryption-key");
638
+ if (fs4.existsSync(keyPath)) {
639
+ const hex = fs4.readFileSync(keyPath, "utf-8").trim();
640
+ return Buffer.from(hex, "hex");
641
+ }
642
+ const key = crypto2.randomBytes(32);
643
+ fs4.writeFileSync(keyPath, key.toString("hex") + "\n", { mode: 384 });
644
+ return key;
645
+ }
646
+ function encrypt(plaintext, key) {
647
+ const iv = crypto2.randomBytes(12);
648
+ const cipher = crypto2.createCipheriv("aes-256-gcm", key, iv);
649
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
650
+ return {
651
+ ciphertext: encrypted.toString("base64"),
652
+ iv: iv.toString("base64"),
653
+ authTag: cipher.getAuthTag().toString("base64")
654
+ };
655
+ }
656
+ function decrypt(ciphertext, iv, authTag, key) {
657
+ const decipher = crypto2.createDecipheriv(
658
+ "aes-256-gcm",
659
+ key,
660
+ Buffer.from(iv, "base64")
661
+ );
662
+ decipher.setAuthTag(Buffer.from(authTag, "base64"));
663
+ const decrypted = Buffer.concat([
664
+ decipher.update(Buffer.from(ciphertext, "base64")),
665
+ decipher.final()
666
+ ]);
667
+ return decrypted.toString("utf-8");
668
+ }
669
+ var crypto2, fs4, path4;
670
+ var init_encryption = __esm({
671
+ "src/encryption.ts"() {
672
+ "use strict";
673
+ crypto2 = __toESM(require("crypto"), 1);
674
+ fs4 = __toESM(require("fs"), 1);
675
+ path4 = __toESM(require("path"), 1);
676
+ }
677
+ });
678
+
679
+ // src/sanitize.ts
680
+ function sanitizeName(name) {
681
+ const sanitized = name.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
682
+ if (!sanitized || /^[._]+$/.test(sanitized)) {
683
+ throw new Error(`Invalid name: "${name}"`);
684
+ }
685
+ return sanitized;
686
+ }
687
+ var init_sanitize = __esm({
688
+ "src/sanitize.ts"() {
689
+ "use strict";
377
690
  }
378
691
  });
379
692
 
@@ -411,12 +724,12 @@ async function cleanupProvider(providerName, apiKey, sessionId) {
411
724
  await provider.cleanup(apiKey, sessionId);
412
725
  }
413
726
  }
414
- var fs4, path4, providers, ProviderVault;
727
+ var fs5, path5, providers, ProviderVault;
415
728
  var init_cloud_providers = __esm({
416
729
  "src/cloud-providers.ts"() {
417
730
  "use strict";
418
- fs4 = __toESM(require("fs"), 1);
419
- path4 = __toESM(require("path"), 1);
731
+ fs5 = __toESM(require("fs"), 1);
732
+ path5 = __toESM(require("path"), 1);
420
733
  init_encryption();
421
734
  init_sanitize();
422
735
  providers = {
@@ -462,12 +775,12 @@ var init_cloud_providers = __esm({
462
775
  dir;
463
776
  encryptionKey;
464
777
  constructor(localDir) {
465
- this.dir = path4.join(localDir, "providers");
778
+ this.dir = path5.join(localDir, "providers");
466
779
  this.encryptionKey = resolveEncryptionKey(localDir);
467
780
  }
468
781
  save(name, apiKey) {
469
782
  getProvider(name);
470
- fs4.mkdirSync(this.dir, { recursive: true });
783
+ fs5.mkdirSync(this.dir, { recursive: true });
471
784
  const { ciphertext, iv, authTag } = encrypt(apiKey, this.encryptionKey);
472
785
  const stored = {
473
786
  name,
@@ -477,34 +790,34 @@ var init_cloud_providers = __esm({
477
790
  data: ciphertext,
478
791
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
479
792
  };
480
- const filePath = path4.join(this.dir, `${sanitizeName(name)}.json`);
481
- fs4.writeFileSync(filePath, JSON.stringify(stored, null, 2), { mode: 384 });
793
+ const filePath = path5.join(this.dir, `${sanitizeName(name)}.json`);
794
+ fs5.writeFileSync(filePath, JSON.stringify(stored, null, 2), { mode: 384 });
482
795
  }
483
796
  load(name) {
484
- const filePath = path4.join(this.dir, `${sanitizeName(name)}.json`);
485
- if (!fs4.existsSync(filePath)) {
797
+ const filePath = path5.join(this.dir, `${sanitizeName(name)}.json`);
798
+ if (!fs5.existsSync(filePath)) {
486
799
  throw new Error(
487
800
  `No API key saved for "${name}". Run: browse provider save ${name} <api-key>`
488
801
  );
489
802
  }
490
- const stored = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
803
+ const stored = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
491
804
  return decrypt(stored.data, stored.iv, stored.authTag, this.encryptionKey);
492
805
  }
493
806
  list() {
494
- if (!fs4.existsSync(this.dir)) return [];
495
- return fs4.readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
807
+ if (!fs5.existsSync(this.dir)) return [];
808
+ return fs5.readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
496
809
  const stored = JSON.parse(
497
- fs4.readFileSync(path4.join(this.dir, f), "utf-8")
810
+ fs5.readFileSync(path5.join(this.dir, f), "utf-8")
498
811
  );
499
812
  return { name: stored.name, createdAt: stored.createdAt };
500
813
  });
501
814
  }
502
815
  delete(name) {
503
- const filePath = path4.join(this.dir, `${sanitizeName(name)}.json`);
504
- if (!fs4.existsSync(filePath)) {
816
+ const filePath = path5.join(this.dir, `${sanitizeName(name)}.json`);
817
+ if (!fs5.existsSync(filePath)) {
505
818
  throw new Error(`No saved key for "${name}"`);
506
819
  }
507
- fs4.unlinkSync(filePath);
820
+ fs5.unlinkSync(filePath);
508
821
  }
509
822
  };
510
823
  }
@@ -515,7 +828,7 @@ var require_package = __commonJS({
515
828
  "package.json"(exports2, module2) {
516
829
  module2.exports = {
517
830
  name: "@ulpi/browse",
518
- version: "1.3.0",
831
+ version: "1.3.2",
519
832
  repository: {
520
833
  type: "git",
521
834
  url: "https://github.com/ulpi-io/browse"
@@ -591,45 +904,45 @@ __export(install_skill_exports, {
591
904
  });
592
905
  function installSkill(targetDir) {
593
906
  const dir = targetDir || process.cwd();
594
- const hasGit = fs5.existsSync(path5.join(dir, ".git"));
595
- const hasClaude = fs5.existsSync(path5.join(dir, ".claude"));
907
+ const hasGit = fs6.existsSync(path6.join(dir, ".git"));
908
+ const hasClaude = fs6.existsSync(path6.join(dir, ".claude"));
596
909
  if (!hasGit && !hasClaude) {
597
910
  console.error(`Not a project root: ${dir}`);
598
911
  console.error("Run from a directory with .git or .claude, or pass the path as an argument.");
599
912
  process.exit(1);
600
913
  }
601
- const skillDir = path5.join(dir, ".claude", "skills", "browse");
602
- fs5.mkdirSync(skillDir, { recursive: true });
603
- const skillSourceDir = path5.resolve(path5.dirname((0, import_url.fileURLToPath)(__import_meta_url)), "..", "skill");
604
- if (!fs5.existsSync(skillSourceDir)) {
914
+ const skillDir = path6.join(dir, ".claude", "skills", "browse");
915
+ fs6.mkdirSync(skillDir, { recursive: true });
916
+ const skillSourceDir = path6.resolve(path6.dirname((0, import_url.fileURLToPath)(__import_meta_url)), "..", "skill");
917
+ if (!fs6.existsSync(skillSourceDir)) {
605
918
  console.error(`Skill directory not found at ${skillSourceDir}`);
606
919
  console.error("Is @ulpi/browse installed correctly?");
607
920
  process.exit(1);
608
921
  }
609
- const mdFiles = fs5.readdirSync(skillSourceDir).filter((f) => f.endsWith(".md"));
922
+ const mdFiles = fs6.readdirSync(skillSourceDir).filter((f) => f.endsWith(".md"));
610
923
  if (mdFiles.length === 0) {
611
924
  console.error(`No .md files found in ${skillSourceDir}`);
612
925
  process.exit(1);
613
926
  }
614
927
  for (const file of mdFiles) {
615
- fs5.copyFileSync(path5.join(skillSourceDir, file), path5.join(skillDir, file));
616
- console.log(`Skill installed: ${path5.relative(dir, path5.join(skillDir, file))}`);
617
- }
618
- const refsSourceDir = path5.join(skillSourceDir, "references");
619
- if (fs5.existsSync(refsSourceDir)) {
620
- const refsDestDir = path5.join(skillDir, "references");
621
- fs5.mkdirSync(refsDestDir, { recursive: true });
622
- const refFiles = fs5.readdirSync(refsSourceDir).filter((f) => f.endsWith(".md"));
928
+ fs6.copyFileSync(path6.join(skillSourceDir, file), path6.join(skillDir, file));
929
+ console.log(`Skill installed: ${path6.relative(dir, path6.join(skillDir, file))}`);
930
+ }
931
+ const refsSourceDir = path6.join(skillSourceDir, "references");
932
+ if (fs6.existsSync(refsSourceDir)) {
933
+ const refsDestDir = path6.join(skillDir, "references");
934
+ fs6.mkdirSync(refsDestDir, { recursive: true });
935
+ const refFiles = fs6.readdirSync(refsSourceDir).filter((f) => f.endsWith(".md"));
623
936
  for (const file of refFiles) {
624
- fs5.copyFileSync(path5.join(refsSourceDir, file), path5.join(refsDestDir, file));
625
- console.log(`Skill installed: ${path5.relative(dir, path5.join(refsDestDir, file))}`);
937
+ fs6.copyFileSync(path6.join(refsSourceDir, file), path6.join(refsDestDir, file));
938
+ console.log(`Skill installed: ${path6.relative(dir, path6.join(refsDestDir, file))}`);
626
939
  }
627
940
  }
628
- const settingsPath = path5.join(dir, ".claude", "settings.json");
941
+ const settingsPath = path6.join(dir, ".claude", "settings.json");
629
942
  let settings = {};
630
- if (fs5.existsSync(settingsPath)) {
943
+ if (fs6.existsSync(settingsPath)) {
631
944
  try {
632
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
945
+ settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
633
946
  } catch {
634
947
  console.error(`Warning: could not parse ${settingsPath}, creating fresh`);
635
948
  settings = {};
@@ -645,20 +958,20 @@ function installSkill(targetDir) {
645
958
  added++;
646
959
  }
647
960
  }
648
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
961
+ fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
649
962
  if (added > 0) {
650
- console.log(`Permissions: ${added} rules added to ${path5.relative(dir, settingsPath)}`);
963
+ console.log(`Permissions: ${added} rules added to ${path6.relative(dir, settingsPath)}`);
651
964
  } else {
652
965
  console.log(`Permissions: already configured`);
653
966
  }
654
967
  console.log("\nDone. Claude Code will now use browse for web tasks automatically.");
655
968
  }
656
- var fs5, path5, import_url, PERMISSIONS;
969
+ var fs6, path6, import_url, PERMISSIONS;
657
970
  var init_install_skill = __esm({
658
971
  "src/install-skill.ts"() {
659
972
  "use strict";
660
- fs5 = __toESM(require("fs"), 1);
661
- path5 = __toESM(require("path"), 1);
973
+ fs6 = __toESM(require("fs"), 1);
974
+ path6 = __toESM(require("path"), 1);
662
975
  import_url = require("url");
663
976
  PERMISSIONS = [
664
977
  "Bash(browse:*)",
@@ -820,21 +1133,21 @@ function listDevices() {
820
1133
  function getProfileDir(localDir, name) {
821
1134
  const sanitized = sanitizeName(name);
822
1135
  if (!sanitized) throw new Error("Invalid profile name");
823
- return path6.join(localDir, "profiles", sanitized);
1136
+ return path7.join(localDir, "profiles", sanitized);
824
1137
  }
825
1138
  function listProfiles(localDir) {
826
- const profilesDir = path6.join(localDir, "profiles");
827
- if (!fs6.existsSync(profilesDir)) return [];
828
- return fs6.readdirSync(profilesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
829
- const dir = path6.join(profilesDir, d.name);
830
- const stat = fs6.statSync(dir);
1139
+ const profilesDir = path7.join(localDir, "profiles");
1140
+ if (!fs7.existsSync(profilesDir)) return [];
1141
+ return fs7.readdirSync(profilesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => {
1142
+ const dir = path7.join(profilesDir, d.name);
1143
+ const stat = fs7.statSync(dir);
831
1144
  let totalSize = 0;
832
1145
  try {
833
- const files = fs6.readdirSync(dir, { recursive: true, withFileTypes: true });
1146
+ const files = fs7.readdirSync(dir, { recursive: true, withFileTypes: true });
834
1147
  for (const f of files) {
835
1148
  if (f.isFile()) {
836
1149
  try {
837
- totalSize += fs6.statSync(path6.join(f.parentPath || f.path || dir, f.name)).size;
1150
+ totalSize += fs7.statSync(path7.join(f.parentPath || f.path || dir, f.name)).size;
838
1151
  } catch {
839
1152
  }
840
1153
  }
@@ -851,15 +1164,15 @@ function listProfiles(localDir) {
851
1164
  }
852
1165
  function deleteProfile(localDir, name) {
853
1166
  const dir = getProfileDir(localDir, name);
854
- if (!fs6.existsSync(dir)) throw new Error(`Profile "${name}" not found`);
855
- fs6.rmSync(dir, { recursive: true, force: true });
1167
+ if (!fs7.existsSync(dir)) throw new Error(`Profile "${name}" not found`);
1168
+ fs7.rmSync(dir, { recursive: true, force: true });
856
1169
  }
857
- var path6, fs6, import_playwright, DEVICE_ALIASES, CUSTOM_DEVICES, BrowserManager;
1170
+ var path7, fs7, import_playwright, DEVICE_ALIASES, CUSTOM_DEVICES, BrowserManager;
858
1171
  var init_browser_manager = __esm({
859
1172
  "src/browser-manager.ts"() {
860
1173
  "use strict";
861
- path6 = __toESM(require("path"), 1);
862
- fs6 = __toESM(require("fs"), 1);
1174
+ path7 = __toESM(require("path"), 1);
1175
+ fs7 = __toESM(require("fs"), 1);
863
1176
  import_playwright = require("playwright");
864
1177
  init_buffers();
865
1178
  init_sanitize();
@@ -2229,8 +2542,8 @@ async function handleReadCommand(command, args, bm, buffers) {
2229
2542
  case "eval": {
2230
2543
  const filePath = args[0];
2231
2544
  if (!filePath) throw new Error("Usage: browse eval <js-file>");
2232
- if (!fs7.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
2233
- const code = fs7.readFileSync(filePath, "utf-8");
2545
+ if (!fs8.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
2546
+ const code = fs8.readFileSync(filePath, "utf-8");
2234
2547
  const result = await evalCtx.evaluate(code);
2235
2548
  return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result ?? "");
2236
2549
  }
@@ -2468,13 +2781,13 @@ async function handleReadCommand(command, args, bm, buffers) {
2468
2781
  throw new Error(`Unknown read command: ${command}`);
2469
2782
  }
2470
2783
  }
2471
- var fs7;
2784
+ var fs8;
2472
2785
  var init_read = __esm({
2473
2786
  "src/commands/read.ts"() {
2474
2787
  "use strict";
2475
2788
  init_browser_manager();
2476
2789
  init_constants();
2477
- fs7 = __toESM(require("fs"), 1);
2790
+ fs8 = __toESM(require("fs"), 1);
2478
2791
  }
2479
2792
  });
2480
2793
 
@@ -2701,14 +3014,14 @@ async function handleWriteCommand(command, args, bm, domainFilter) {
2701
3014
  const file = args[1];
2702
3015
  if (!file) throw new Error("Usage: browse cookie export <file>");
2703
3016
  const cookies = await page.context().cookies();
2704
- fs8.writeFileSync(file, JSON.stringify(cookies, null, 2));
3017
+ fs9.writeFileSync(file, JSON.stringify(cookies, null, 2));
2705
3018
  return `Exported ${cookies.length} cookie(s) to ${file}`;
2706
3019
  }
2707
3020
  if (cookieStr === "import") {
2708
3021
  const file = args[1];
2709
3022
  if (!file) throw new Error("Usage: browse cookie import <file>");
2710
- if (!fs8.existsSync(file)) throw new Error(`File not found: ${file}`);
2711
- const cookies = JSON.parse(fs8.readFileSync(file, "utf-8"));
3023
+ if (!fs9.existsSync(file)) throw new Error(`File not found: ${file}`);
3024
+ const cookies = JSON.parse(fs9.readFileSync(file, "utf-8"));
2712
3025
  if (!Array.isArray(cookies)) throw new Error("Cookie file must contain a JSON array of cookie objects");
2713
3026
  await page.context().addCookies(cookies);
2714
3027
  return `Imported ${cookies.length} cookie(s) from ${file}`;
@@ -2775,7 +3088,7 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
2775
3088
  const [selector, ...filePaths] = args;
2776
3089
  if (!selector || filePaths.length === 0) throw new Error("Usage: browse upload <selector> <file1> [file2] ...");
2777
3090
  for (const fp of filePaths) {
2778
- if (!fs8.existsSync(fp)) throw new Error(`File not found: ${fp}`);
3091
+ if (!fs9.existsSync(fp)) throw new Error(`File not found: ${fp}`);
2779
3092
  }
2780
3093
  const resolved = bm.resolveRef(selector);
2781
3094
  if ("locator" in resolved) {
@@ -3111,13 +3424,13 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
3111
3424
  throw new Error(`Unknown write command: ${command}`);
3112
3425
  }
3113
3426
  }
3114
- var fs8;
3427
+ var fs9;
3115
3428
  var init_write = __esm({
3116
3429
  "src/commands/write.ts"() {
3117
3430
  "use strict";
3118
3431
  init_browser_manager();
3119
3432
  init_constants();
3120
- fs8 = __toESM(require("fs"), 1);
3433
+ fs9 = __toESM(require("fs"), 1);
3121
3434
  }
3122
3435
  });
3123
3436
 
@@ -3921,7 +4234,7 @@ var init_lib = __esm({
3921
4234
  tokenize: function tokenize(value) {
3922
4235
  return Array.from(value);
3923
4236
  },
3924
- join: function join7(chars) {
4237
+ join: function join8(chars) {
3925
4238
  return chars.join("");
3926
4239
  },
3927
4240
  postProcess: function postProcess(changeObjects) {
@@ -4100,20 +4413,20 @@ async function saveSessionState(sessionDir, context, encryptionKey) {
4100
4413
  } else {
4101
4414
  content = json;
4102
4415
  }
4103
- fs9.mkdirSync(sessionDir, { recursive: true });
4104
- fs9.writeFileSync(path7.join(sessionDir, STATE_FILENAME), content, { mode: 384 });
4416
+ fs10.mkdirSync(sessionDir, { recursive: true });
4417
+ fs10.writeFileSync(path8.join(sessionDir, STATE_FILENAME), content, { mode: 384 });
4105
4418
  } catch (err) {
4106
4419
  console.log(`[session-persist] Warning: failed to save state: ${err.message}`);
4107
4420
  }
4108
4421
  }
4109
4422
  async function loadSessionState(sessionDir, context, encryptionKey) {
4110
- const statePath = path7.join(sessionDir, STATE_FILENAME);
4111
- if (!fs9.existsSync(statePath)) {
4423
+ const statePath = path8.join(sessionDir, STATE_FILENAME);
4424
+ if (!fs10.existsSync(statePath)) {
4112
4425
  return false;
4113
4426
  }
4114
4427
  let stateData;
4115
4428
  try {
4116
- const raw = fs9.readFileSync(statePath, "utf-8");
4429
+ const raw = fs10.readFileSync(statePath, "utf-8");
4117
4430
  const parsed = JSON.parse(raw);
4118
4431
  if (parsed.encrypted) {
4119
4432
  if (!encryptionKey) {
@@ -4169,24 +4482,24 @@ async function loadSessionState(sessionDir, context, encryptionKey) {
4169
4482
  }
4170
4483
  }
4171
4484
  function hasPersistedState(sessionDir) {
4172
- return fs9.existsSync(path7.join(sessionDir, STATE_FILENAME));
4485
+ return fs10.existsSync(path8.join(sessionDir, STATE_FILENAME));
4173
4486
  }
4174
4487
  function cleanOldStates(localDir, maxAgeDays) {
4175
4488
  const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
4176
4489
  const now = Date.now();
4177
4490
  let deleted = 0;
4178
- const statesDir = path7.join(localDir, "states");
4179
- if (fs9.existsSync(statesDir)) {
4491
+ const statesDir = path8.join(localDir, "states");
4492
+ if (fs10.existsSync(statesDir)) {
4180
4493
  try {
4181
- const entries = fs9.readdirSync(statesDir);
4494
+ const entries = fs10.readdirSync(statesDir);
4182
4495
  for (const entry of entries) {
4183
4496
  if (!entry.endsWith(".json")) continue;
4184
- const filePath = path7.join(statesDir, entry);
4497
+ const filePath = path8.join(statesDir, entry);
4185
4498
  try {
4186
- const stat = fs9.statSync(filePath);
4499
+ const stat = fs10.statSync(filePath);
4187
4500
  if (!stat.isFile()) continue;
4188
4501
  if (now - stat.mtimeMs > maxAgeMs) {
4189
- fs9.unlinkSync(filePath);
4502
+ fs10.unlinkSync(filePath);
4190
4503
  deleted++;
4191
4504
  }
4192
4505
  } catch (_) {
@@ -4195,23 +4508,23 @@ function cleanOldStates(localDir, maxAgeDays) {
4195
4508
  } catch (_) {
4196
4509
  }
4197
4510
  }
4198
- const sessionsDir = path7.join(localDir, "sessions");
4199
- if (fs9.existsSync(sessionsDir)) {
4511
+ const sessionsDir = path8.join(localDir, "sessions");
4512
+ if (fs10.existsSync(sessionsDir)) {
4200
4513
  try {
4201
- const sessionDirs = fs9.readdirSync(sessionsDir);
4514
+ const sessionDirs = fs10.readdirSync(sessionsDir);
4202
4515
  for (const dir of sessionDirs) {
4203
- const dirPath = path7.join(sessionsDir, dir);
4516
+ const dirPath = path8.join(sessionsDir, dir);
4204
4517
  try {
4205
- const dirStat = fs9.statSync(dirPath);
4518
+ const dirStat = fs10.statSync(dirPath);
4206
4519
  if (!dirStat.isDirectory()) continue;
4207
4520
  } catch (_) {
4208
4521
  continue;
4209
4522
  }
4210
- const statePath = path7.join(dirPath, STATE_FILENAME);
4523
+ const statePath = path8.join(dirPath, STATE_FILENAME);
4211
4524
  try {
4212
- const stat = fs9.statSync(statePath);
4525
+ const stat = fs10.statSync(statePath);
4213
4526
  if (now - stat.mtimeMs > maxAgeMs) {
4214
- fs9.unlinkSync(statePath);
4527
+ fs10.unlinkSync(statePath);
4215
4528
  deleted++;
4216
4529
  }
4217
4530
  } catch (_) {
@@ -4222,13 +4535,13 @@ function cleanOldStates(localDir, maxAgeDays) {
4222
4535
  }
4223
4536
  return { deleted };
4224
4537
  }
4225
- var fs9, path7, STATE_FILENAME;
4538
+ var fs10, path8, STATE_FILENAME;
4226
4539
  var init_session_persist = __esm({
4227
4540
  "src/session-persist.ts"() {
4228
4541
  "use strict";
4229
- fs9 = __toESM(require("fs"), 1);
4230
- path7 = __toESM(require("path"), 1);
4231
- init_encryption();
4542
+ fs10 = __toESM(require("fs"), 1);
4543
+ path8 = __toESM(require("path"), 1);
4544
+ init_encryption();
4232
4545
  STATE_FILENAME = "state.json";
4233
4546
  }
4234
4547
  });
@@ -4241,20 +4554,20 @@ __export(policy_exports, {
4241
4554
  function findFileUpward(filename) {
4242
4555
  let dir = process.cwd();
4243
4556
  for (let i = 0; i < 20; i++) {
4244
- const candidate = path8.join(dir, filename);
4245
- if (fs10.existsSync(candidate)) return candidate;
4246
- const parent = path8.dirname(dir);
4557
+ const candidate = path9.join(dir, filename);
4558
+ if (fs11.existsSync(candidate)) return candidate;
4559
+ const parent = path9.dirname(dir);
4247
4560
  if (parent === dir) break;
4248
4561
  dir = parent;
4249
4562
  }
4250
4563
  return null;
4251
4564
  }
4252
- var fs10, path8, PolicyChecker;
4565
+ var fs11, path9, PolicyChecker;
4253
4566
  var init_policy = __esm({
4254
4567
  "src/policy.ts"() {
4255
4568
  "use strict";
4256
- fs10 = __toESM(require("fs"), 1);
4257
- path8 = __toESM(require("path"), 1);
4569
+ fs11 = __toESM(require("fs"), 1);
4570
+ path9 = __toESM(require("path"), 1);
4258
4571
  PolicyChecker = class {
4259
4572
  filePath = null;
4260
4573
  lastMtime = 0;
@@ -4273,10 +4586,10 @@ var init_policy = __esm({
4273
4586
  reload() {
4274
4587
  if (!this.filePath) return;
4275
4588
  try {
4276
- const stat = fs10.statSync(this.filePath);
4589
+ const stat = fs11.statSync(this.filePath);
4277
4590
  if (stat.mtimeMs === this.lastMtime) return;
4278
4591
  this.lastMtime = stat.mtimeMs;
4279
- const raw = fs10.readFileSync(this.filePath, "utf-8");
4592
+ const raw = fs11.readFileSync(this.filePath, "utf-8");
4280
4593
  this.policy = JSON.parse(raw);
4281
4594
  } catch {
4282
4595
  }
@@ -4424,516 +4737,211 @@ function generateDiffImage(base, curr, colorThreshold) {
4424
4737
  diffData[di + 1] = 0;
4425
4738
  diffData[di + 2] = 0;
4426
4739
  diffData[di + 3] = 255;
4427
- continue;
4428
- }
4429
- const bi = (y * base.width + x) * 4;
4430
- const ci = (y * curr.width + x) * 4;
4431
- const dr = base.data[bi] - curr.data[ci];
4432
- const dg = base.data[bi + 1] - curr.data[ci + 1];
4433
- const db = base.data[bi + 2] - curr.data[ci + 2];
4434
- const distSq = dr * dr + dg * dg + db * db;
4435
- const isDiff = colorThreshold === 0 ? distSq > 0 : distSq > colorThreshSq;
4436
- if (isDiff) {
4437
- diffData[di] = 255;
4438
- diffData[di + 1] = curr.data[ci + 1] / 3 | 0;
4439
- diffData[di + 2] = curr.data[ci + 2] / 3 | 0;
4440
- diffData[di + 3] = 255;
4441
- } else {
4442
- diffData[di] = curr.data[ci] / 3 | 0;
4443
- diffData[di + 1] = curr.data[ci + 1] / 3 | 0;
4444
- diffData[di + 2] = curr.data[ci + 2] / 3 | 0;
4445
- diffData[di + 3] = 128;
4446
- }
4447
- }
4448
- }
4449
- return encodePNG({ width: w, height: h, data: diffData });
4450
- }
4451
- function compareScreenshots(baselineBuf, currentBuf, thresholdPct = 0.1, colorThreshold = 30) {
4452
- const base = decodePNG(baselineBuf);
4453
- const curr = decodePNG(currentBuf);
4454
- const w = Math.max(base.width, curr.width);
4455
- const h = Math.max(base.height, curr.height);
4456
- const totalPixels = w * h;
4457
- let diffPixels = 0;
4458
- const colorThreshSq = colorThreshold * colorThreshold * 3;
4459
- for (let y = 0; y < h; y++) {
4460
- for (let x = 0; x < w; x++) {
4461
- const inBase = x < base.width && y < base.height;
4462
- const inCurr = x < curr.width && y < curr.height;
4463
- if (!inBase || !inCurr) {
4464
- diffPixels++;
4465
- continue;
4466
- }
4467
- const bi = (y * base.width + x) * 4;
4468
- const ci = (y * curr.width + x) * 4;
4469
- const dr = base.data[bi] - curr.data[ci];
4470
- const dg = base.data[bi + 1] - curr.data[ci + 1];
4471
- const db = base.data[bi + 2] - curr.data[ci + 2];
4472
- const distSq = dr * dr + dg * dg + db * db;
4473
- if (colorThreshold === 0 ? distSq > 0 : distSq > colorThreshSq) diffPixels++;
4474
- }
4475
- }
4476
- const mismatchPct = totalPixels > 0 ? diffPixels / totalPixels * 100 : 0;
4477
- const passed = mismatchPct <= thresholdPct;
4478
- const result = { totalPixels, diffPixels, mismatchPct, passed };
4479
- if (!passed) {
4480
- result.diffImage = generateDiffImage(base, curr, colorThreshold);
4481
- }
4482
- return result;
4483
- }
4484
- var zlib, PNG_MAGIC;
4485
- var init_png_compare = __esm({
4486
- "src/png-compare.ts"() {
4487
- "use strict";
4488
- zlib = __toESM(require("zlib"), 1);
4489
- PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
4490
- }
4491
- });
4492
-
4493
- // src/auth-vault.ts
4494
- var auth_vault_exports = {};
4495
- __export(auth_vault_exports, {
4496
- AuthVault: () => AuthVault
4497
- });
4498
- async function autoDetectSelector(page, field) {
4499
- if (field === "username") {
4500
- const candidates2 = [
4501
- 'input[type="email"]',
4502
- 'input[name="email"]',
4503
- 'input[name="username"]',
4504
- 'input[name="user"]',
4505
- 'input[name="login"]',
4506
- 'input[autocomplete="username"]',
4507
- 'input[autocomplete="email"]',
4508
- 'input[type="text"]:first-of-type'
4509
- ];
4510
- for (const sel of candidates2) {
4511
- const count = await page.locator(sel).count();
4512
- if (count > 0) return sel;
4513
- }
4514
- throw new Error("Could not auto-detect username field. Save with explicit selectors: browse auth save <name> <url> <user> <pass> --user-sel <sel> --pass-sel <sel> --submit-sel <sel>");
4515
- }
4516
- if (field === "password") {
4517
- const candidates2 = [
4518
- 'input[type="password"]',
4519
- 'input[name="password"]',
4520
- 'input[name="pass"]',
4521
- 'input[autocomplete="current-password"]'
4522
- ];
4523
- for (const sel of candidates2) {
4524
- const count = await page.locator(sel).count();
4525
- if (count > 0) return sel;
4526
- }
4527
- throw new Error("Could not auto-detect password field.");
4528
- }
4529
- const candidates = [
4530
- 'button[type="submit"]',
4531
- 'input[type="submit"]',
4532
- "form button",
4533
- 'button:has-text("Log in")',
4534
- 'button:has-text("Sign in")',
4535
- 'button:has-text("Login")',
4536
- 'button:has-text("Submit")'
4537
- ];
4538
- for (const sel of candidates) {
4539
- const count = await page.locator(sel).count();
4540
- if (count > 0) return sel;
4541
- }
4542
- throw new Error("Could not auto-detect submit button.");
4543
- }
4544
- var fs11, path9, AuthVault;
4545
- var init_auth_vault = __esm({
4546
- "src/auth-vault.ts"() {
4547
- "use strict";
4548
- fs11 = __toESM(require("fs"), 1);
4549
- path9 = __toESM(require("path"), 1);
4550
- init_constants();
4551
- init_encryption();
4552
- init_sanitize();
4553
- AuthVault = class {
4554
- authDir;
4555
- encryptionKey;
4556
- constructor(localDir) {
4557
- this.authDir = path9.join(localDir, "auth");
4558
- this.encryptionKey = resolveEncryptionKey(localDir);
4559
- }
4560
- save(name, url, username, password, selectors) {
4561
- fs11.mkdirSync(this.authDir, { recursive: true });
4562
- const { ciphertext, iv, authTag } = encrypt(password, this.encryptionKey);
4563
- const now = (/* @__PURE__ */ new Date()).toISOString();
4564
- const credential = {
4565
- name,
4566
- url,
4567
- username,
4568
- encrypted: true,
4569
- iv,
4570
- authTag,
4571
- data: ciphertext,
4572
- usernameSelector: selectors?.username,
4573
- passwordSelector: selectors?.password,
4574
- submitSelector: selectors?.submit,
4575
- createdAt: now,
4576
- updatedAt: now
4577
- };
4578
- const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
4579
- fs11.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 384 });
4580
- }
4581
- load(name) {
4582
- const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
4583
- if (!fs11.existsSync(filePath)) {
4584
- throw new Error(`Credential "${name}" not found. Run "browse auth list" to see saved credentials.`);
4585
- }
4586
- return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
4587
- }
4588
- async login(name, bm) {
4589
- const cred = this.load(name);
4590
- const password = decrypt(cred.data, cred.iv, cred.authTag, this.encryptionKey);
4591
- const page = bm.getPage();
4592
- await page.goto(cred.url, {
4593
- waitUntil: "domcontentloaded",
4594
- timeout: DEFAULTS.COMMAND_TIMEOUT_MS
4595
- });
4596
- const userSel = cred.usernameSelector || await autoDetectSelector(page, "username");
4597
- const passSel = cred.passwordSelector || await autoDetectSelector(page, "password");
4598
- const submitSel = cred.submitSelector || await autoDetectSelector(page, "submit");
4599
- await page.fill(userSel, cred.username, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4600
- await page.fill(passSel, password, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4601
- await page.click(submitSel, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4602
- await page.waitForLoadState("domcontentloaded").catch(() => {
4603
- });
4604
- return `Logged in as ${cred.username} at ${page.url()}`;
4605
- }
4606
- list() {
4607
- if (!fs11.existsSync(this.authDir)) return [];
4608
- const files = fs11.readdirSync(this.authDir).filter((f) => f.endsWith(".json"));
4609
- return files.map((f) => {
4610
- try {
4611
- const data = JSON.parse(fs11.readFileSync(path9.join(this.authDir, f), "utf-8"));
4612
- return {
4613
- name: data.name,
4614
- url: data.url,
4615
- username: data.username,
4616
- hasPassword: true,
4617
- createdAt: data.createdAt
4618
- };
4619
- } catch {
4620
- return null;
4621
- }
4622
- }).filter(Boolean);
4623
- }
4624
- delete(name) {
4625
- const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
4626
- if (!fs11.existsSync(filePath)) {
4627
- throw new Error(`Credential "${name}" not found.`);
4628
- }
4629
- fs11.unlinkSync(filePath);
4630
- }
4631
- };
4632
- }
4633
- });
4634
-
4635
- // src/cookie-import.ts
4636
- var cookie_import_exports = {};
4637
- __export(cookie_import_exports, {
4638
- CookieImportError: () => CookieImportError,
4639
- findInstalledBrowsers: () => findInstalledBrowsers,
4640
- importCookies: () => importCookies,
4641
- listDomains: () => listDomains
4642
- });
4643
- function findInstalledBrowsers() {
4644
- const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
4645
- return BROWSER_REGISTRY.filter((b) => {
4646
- const dbPath = path10.join(appSupport, b.dataDir, "Default", "Cookies");
4647
- try {
4648
- return fs12.existsSync(dbPath);
4649
- } catch {
4650
- return false;
4651
- }
4652
- });
4653
- }
4654
- function listDomains(browserName, profile = "Default") {
4655
- const browser2 = resolveBrowser(browserName);
4656
- const dbPath = getCookieDbPath(browser2, profile);
4657
- const db = openDb(dbPath, browser2.name);
4658
- try {
4659
- const now = chromiumNow();
4660
- const rows = db.prepare(
4661
- `SELECT host_key AS domain, COUNT(*) AS count
4662
- FROM cookies
4663
- WHERE has_expires = 0 OR expires_utc > ?
4664
- GROUP BY host_key
4665
- ORDER BY count DESC`
4666
- ).all(now);
4667
- return { domains: rows, browser: browser2.name };
4668
- } finally {
4669
- db.close();
4670
- }
4671
- }
4672
- async function importCookies(browserName, domains, profile = "Default") {
4673
- if (domains.length === 0) return { cookies: [], count: 0, failed: 0, domainCounts: {} };
4674
- const browser2 = resolveBrowser(browserName);
4675
- const derivedKey = await getDerivedKey(browser2);
4676
- const dbPath = getCookieDbPath(browser2, profile);
4677
- const db = openDb(dbPath, browser2.name);
4678
- try {
4679
- const now = chromiumNow();
4680
- const placeholders = domains.map(() => "?").join(",");
4681
- const rows = db.prepare(
4682
- `SELECT host_key, name, value, encrypted_value, path, expires_utc,
4683
- is_secure, is_httponly, has_expires, samesite
4684
- FROM cookies
4685
- WHERE host_key IN (${placeholders})
4686
- AND (has_expires = 0 OR expires_utc > ?)
4687
- ORDER BY host_key, name`
4688
- ).all(...domains, now);
4689
- const cookies = [];
4690
- let failed = 0;
4691
- const domainCounts = {};
4692
- for (const row of rows) {
4693
- try {
4694
- const value = decryptCookieValue(row, derivedKey);
4695
- const cookie = toPlaywrightCookie(row, value);
4696
- cookies.push(cookie);
4697
- domainCounts[row.host_key] = (domainCounts[row.host_key] || 0) + 1;
4698
- } catch {
4699
- failed++;
4700
- }
4701
- }
4702
- return { cookies, count: cookies.length, failed, domainCounts };
4703
- } finally {
4704
- db.close();
4705
- }
4706
- }
4707
- function resolveBrowser(nameOrAlias) {
4708
- const needle = nameOrAlias.toLowerCase().trim();
4709
- const found = BROWSER_REGISTRY.find(
4710
- (b) => b.aliases.includes(needle) || b.name.toLowerCase() === needle
4711
- );
4712
- if (!found) {
4713
- const supported = BROWSER_REGISTRY.flatMap((b) => b.aliases).join(", ");
4714
- throw new CookieImportError(
4715
- `Unknown browser '${nameOrAlias}'. Supported: ${supported}`,
4716
- "unknown_browser"
4717
- );
4718
- }
4719
- return found;
4720
- }
4721
- function validateProfile(profile) {
4722
- if (/[/\\]|\.\./.test(profile) || /[\x00-\x1f]/.test(profile)) {
4723
- throw new CookieImportError(
4724
- `Invalid profile name: '${profile}'`,
4725
- "bad_request"
4726
- );
4727
- }
4728
- }
4729
- function getCookieDbPath(browser2, profile) {
4730
- validateProfile(profile);
4731
- const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
4732
- const dbPath = path10.join(appSupport, browser2.dataDir, profile, "Cookies");
4733
- if (!fs12.existsSync(dbPath)) {
4734
- throw new CookieImportError(
4735
- `${browser2.name} is not installed (no cookie database at ${dbPath})`,
4736
- "not_installed"
4737
- );
4738
- }
4739
- return dbPath;
4740
- }
4741
- function openDb(dbPath, browserName) {
4742
- try {
4743
- return new import_better_sqlite3.default(dbPath, { readonly: true });
4744
- } catch (err) {
4745
- if (err.message?.includes("SQLITE_BUSY") || err.message?.includes("database is locked")) {
4746
- return openDbFromCopy(dbPath, browserName);
4747
- }
4748
- if (err.message?.includes("SQLITE_CORRUPT") || err.message?.includes("malformed")) {
4749
- throw new CookieImportError(
4750
- "Cookie database is corrupt",
4751
- "db_corrupt"
4752
- );
4753
- }
4754
- throw err;
4755
- }
4756
- }
4757
- function openDbFromCopy(dbPath, browserName) {
4758
- const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}-${crypto2.randomUUID()}.db`;
4759
- try {
4760
- fs12.copyFileSync(dbPath, tmpPath);
4761
- const walPath = dbPath + "-wal";
4762
- const shmPath = dbPath + "-shm";
4763
- if (fs12.existsSync(walPath)) fs12.copyFileSync(walPath, tmpPath + "-wal");
4764
- if (fs12.existsSync(shmPath)) fs12.copyFileSync(shmPath, tmpPath + "-shm");
4765
- const db = new import_better_sqlite3.default(tmpPath, { readonly: true });
4766
- const origClose = db.close.bind(db);
4767
- db.close = (() => {
4768
- origClose();
4769
- try {
4770
- fs12.unlinkSync(tmpPath);
4771
- } catch {
4772
- }
4773
- try {
4774
- fs12.unlinkSync(tmpPath + "-wal");
4775
- } catch {
4776
- }
4777
- try {
4778
- fs12.unlinkSync(tmpPath + "-shm");
4779
- } catch {
4780
- }
4781
- });
4782
- return db;
4783
- } catch {
4784
- try {
4785
- fs12.unlinkSync(tmpPath);
4786
- } catch {
4787
- }
4788
- throw new CookieImportError(
4789
- `Cookie database is locked (${browserName} may be running). Try closing ${browserName} first.`,
4790
- "db_locked",
4791
- "retry"
4792
- );
4793
- }
4794
- }
4795
- async function getDerivedKey(browser2) {
4796
- const cached = keyCache.get(browser2.keychainService);
4797
- if (cached) return cached;
4798
- const password = await getKeychainPassword(browser2.keychainService);
4799
- const derived = crypto2.pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
4800
- keyCache.set(browser2.keychainService, derived);
4801
- return derived;
4802
- }
4803
- async function getKeychainPassword(service) {
4804
- const proc = (0, import_child_process2.spawn)("security", ["find-generic-password", "-s", service, "-w"], {
4805
- stdio: ["ignore", "pipe", "pipe"]
4806
- });
4807
- let stdout = "";
4808
- let stderr = "";
4809
- proc.stdout.setEncoding("utf8");
4810
- proc.stderr.setEncoding("utf8");
4811
- proc.stdout.on("data", (chunk) => {
4812
- stdout += chunk;
4813
- });
4814
- proc.stderr.on("data", (chunk) => {
4815
- stderr += chunk;
4816
- });
4817
- const exitPromise = new Promise(
4818
- (resolve4) => proc.on("close", (code) => resolve4(code))
4819
- );
4820
- const timeout = new Promise(
4821
- (_, reject) => setTimeout(() => {
4822
- proc.kill();
4823
- reject(new CookieImportError(
4824
- `macOS is waiting for Keychain permission. Look for a dialog asking to allow access to "${service}".`,
4825
- "keychain_timeout",
4826
- "retry"
4827
- ));
4828
- }, 1e4)
4829
- );
4830
- try {
4831
- const exitCode = await Promise.race([exitPromise, timeout]);
4832
- if (exitCode !== 0) {
4833
- const errText = stderr.trim().toLowerCase();
4834
- if (errText.includes("user canceled") || errText.includes("denied") || errText.includes("interaction not allowed")) {
4835
- throw new CookieImportError(
4836
- `Keychain access denied. Click Allow in the macOS dialog for "${service}".`,
4837
- "keychain_denied",
4838
- "retry"
4839
- );
4840
- }
4841
- if (errText.includes("could not be found") || errText.includes("not found")) {
4842
- throw new CookieImportError(
4843
- `No Keychain entry for "${service}".`,
4844
- "keychain_not_found"
4845
- );
4740
+ continue;
4741
+ }
4742
+ const bi = (y * base.width + x) * 4;
4743
+ const ci = (y * curr.width + x) * 4;
4744
+ const dr = base.data[bi] - curr.data[ci];
4745
+ const dg = base.data[bi + 1] - curr.data[ci + 1];
4746
+ const db = base.data[bi + 2] - curr.data[ci + 2];
4747
+ const distSq = dr * dr + dg * dg + db * db;
4748
+ const isDiff = colorThreshold === 0 ? distSq > 0 : distSq > colorThreshSq;
4749
+ if (isDiff) {
4750
+ diffData[di] = 255;
4751
+ diffData[di + 1] = curr.data[ci + 1] / 3 | 0;
4752
+ diffData[di + 2] = curr.data[ci + 2] / 3 | 0;
4753
+ diffData[di + 3] = 255;
4754
+ } else {
4755
+ diffData[di] = curr.data[ci] / 3 | 0;
4756
+ diffData[di + 1] = curr.data[ci + 1] / 3 | 0;
4757
+ diffData[di + 2] = curr.data[ci + 2] / 3 | 0;
4758
+ diffData[di + 3] = 128;
4846
4759
  }
4847
- throw new CookieImportError(
4848
- `Could not read Keychain: ${stderr.trim()}`,
4849
- "keychain_error",
4850
- "retry"
4851
- );
4852
4760
  }
4853
- return stdout.trim();
4854
- } catch (err) {
4855
- if (err instanceof CookieImportError) throw err;
4856
- throw new CookieImportError(
4857
- `Could not read Keychain: ${err.message}`,
4858
- "keychain_error",
4859
- "retry"
4860
- );
4861
4761
  }
4762
+ return encodePNG({ width: w, height: h, data: diffData });
4862
4763
  }
4863
- function decryptCookieValue(row, key) {
4864
- if (row.value && row.value.length > 0) return row.value;
4865
- const ev = Buffer.from(row.encrypted_value);
4866
- if (ev.length === 0) return "";
4867
- const prefix = ev.slice(0, 3).toString("utf-8");
4868
- if (prefix !== "v10") {
4869
- throw new Error(`Unknown encryption prefix: ${prefix}`);
4764
+ function compareScreenshots(baselineBuf, currentBuf, thresholdPct = 0.1, colorThreshold = 30) {
4765
+ const base = decodePNG(baselineBuf);
4766
+ const curr = decodePNG(currentBuf);
4767
+ const w = Math.max(base.width, curr.width);
4768
+ const h = Math.max(base.height, curr.height);
4769
+ const totalPixels = w * h;
4770
+ let diffPixels = 0;
4771
+ const colorThreshSq = colorThreshold * colorThreshold * 3;
4772
+ for (let y = 0; y < h; y++) {
4773
+ for (let x = 0; x < w; x++) {
4774
+ const inBase = x < base.width && y < base.height;
4775
+ const inCurr = x < curr.width && y < curr.height;
4776
+ if (!inBase || !inCurr) {
4777
+ diffPixels++;
4778
+ continue;
4779
+ }
4780
+ const bi = (y * base.width + x) * 4;
4781
+ const ci = (y * curr.width + x) * 4;
4782
+ const dr = base.data[bi] - curr.data[ci];
4783
+ const dg = base.data[bi + 1] - curr.data[ci + 1];
4784
+ const db = base.data[bi + 2] - curr.data[ci + 2];
4785
+ const distSq = dr * dr + dg * dg + db * db;
4786
+ if (colorThreshold === 0 ? distSq > 0 : distSq > colorThreshSq) diffPixels++;
4787
+ }
4870
4788
  }
4871
- const ciphertext = ev.slice(3);
4872
- const iv = Buffer.alloc(16, 32);
4873
- const decipher = crypto2.createDecipheriv("aes-128-cbc", key, iv);
4874
- const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
4875
- if (plaintext.length <= 32) return "";
4876
- return plaintext.slice(32).toString("utf-8");
4877
- }
4878
- function toPlaywrightCookie(row, value) {
4879
- return {
4880
- name: row.name,
4881
- value,
4882
- domain: row.host_key,
4883
- path: row.path || "/",
4884
- expires: chromiumEpochToUnix(row.expires_utc, row.has_expires),
4885
- secure: row.is_secure === 1,
4886
- httpOnly: row.is_httponly === 1,
4887
- sameSite: mapSameSite(row.samesite)
4888
- };
4889
- }
4890
- function chromiumNow() {
4891
- return BigInt(Date.now()) * 1000n + CHROMIUM_EPOCH_OFFSET;
4892
- }
4893
- function chromiumEpochToUnix(epoch, hasExpires) {
4894
- if (hasExpires === 0 || epoch === 0 || epoch === 0n) return -1;
4895
- const epochBig = BigInt(epoch);
4896
- const unixMicro = epochBig - CHROMIUM_EPOCH_OFFSET;
4897
- return Number(unixMicro / 1000000n);
4789
+ const mismatchPct = totalPixels > 0 ? diffPixels / totalPixels * 100 : 0;
4790
+ const passed = mismatchPct <= thresholdPct;
4791
+ const result = { totalPixels, diffPixels, mismatchPct, passed };
4792
+ if (!passed) {
4793
+ result.diffImage = generateDiffImage(base, curr, colorThreshold);
4794
+ }
4795
+ return result;
4898
4796
  }
4899
- function mapSameSite(value) {
4900
- switch (value) {
4901
- case 0:
4902
- return "None";
4903
- case 1:
4904
- return "Lax";
4905
- case 2:
4906
- return "Strict";
4907
- default:
4908
- return "Lax";
4797
+ var zlib, PNG_MAGIC;
4798
+ var init_png_compare = __esm({
4799
+ "src/png-compare.ts"() {
4800
+ "use strict";
4801
+ zlib = __toESM(require("zlib"), 1);
4802
+ PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
4803
+ }
4804
+ });
4805
+
4806
+ // src/auth-vault.ts
4807
+ var auth_vault_exports = {};
4808
+ __export(auth_vault_exports, {
4809
+ AuthVault: () => AuthVault
4810
+ });
4811
+ async function autoDetectSelector(page, field) {
4812
+ if (field === "username") {
4813
+ const candidates2 = [
4814
+ 'input[type="email"]',
4815
+ 'input[name="email"]',
4816
+ 'input[name="username"]',
4817
+ 'input[name="user"]',
4818
+ 'input[name="login"]',
4819
+ 'input[autocomplete="username"]',
4820
+ 'input[autocomplete="email"]',
4821
+ 'input[type="text"]:first-of-type'
4822
+ ];
4823
+ for (const sel of candidates2) {
4824
+ const count = await page.locator(sel).count();
4825
+ if (count > 0) return sel;
4826
+ }
4827
+ throw new Error("Could not auto-detect username field. Save with explicit selectors: browse auth save <name> <url> <user> <pass> --user-sel <sel> --pass-sel <sel> --submit-sel <sel>");
4828
+ }
4829
+ if (field === "password") {
4830
+ const candidates2 = [
4831
+ 'input[type="password"]',
4832
+ 'input[name="password"]',
4833
+ 'input[name="pass"]',
4834
+ 'input[autocomplete="current-password"]'
4835
+ ];
4836
+ for (const sel of candidates2) {
4837
+ const count = await page.locator(sel).count();
4838
+ if (count > 0) return sel;
4839
+ }
4840
+ throw new Error("Could not auto-detect password field.");
4841
+ }
4842
+ const candidates = [
4843
+ 'button[type="submit"]',
4844
+ 'input[type="submit"]',
4845
+ "form button",
4846
+ 'button:has-text("Log in")',
4847
+ 'button:has-text("Sign in")',
4848
+ 'button:has-text("Login")',
4849
+ 'button:has-text("Submit")'
4850
+ ];
4851
+ for (const sel of candidates) {
4852
+ const count = await page.locator(sel).count();
4853
+ if (count > 0) return sel;
4909
4854
  }
4855
+ throw new Error("Could not auto-detect submit button.");
4910
4856
  }
4911
- var import_better_sqlite3, import_child_process2, crypto2, fs12, path10, os2, CookieImportError, BROWSER_REGISTRY, keyCache, CHROMIUM_EPOCH_OFFSET;
4912
- var init_cookie_import = __esm({
4913
- "src/cookie-import.ts"() {
4857
+ var fs12, path10, AuthVault;
4858
+ var init_auth_vault = __esm({
4859
+ "src/auth-vault.ts"() {
4914
4860
  "use strict";
4915
- import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
4916
- import_child_process2 = require("child_process");
4917
- crypto2 = __toESM(require("crypto"), 1);
4918
4861
  fs12 = __toESM(require("fs"), 1);
4919
4862
  path10 = __toESM(require("path"), 1);
4920
- os2 = __toESM(require("os"), 1);
4921
- CookieImportError = class extends Error {
4922
- constructor(message, code, action) {
4923
- super(message);
4924
- this.code = code;
4925
- this.action = action;
4926
- this.name = "CookieImportError";
4863
+ init_constants();
4864
+ init_encryption();
4865
+ init_sanitize();
4866
+ AuthVault = class {
4867
+ authDir;
4868
+ encryptionKey;
4869
+ constructor(localDir) {
4870
+ this.authDir = path10.join(localDir, "auth");
4871
+ this.encryptionKey = resolveEncryptionKey(localDir);
4872
+ }
4873
+ save(name, url, username, password, selectors) {
4874
+ fs12.mkdirSync(this.authDir, { recursive: true });
4875
+ const { ciphertext, iv, authTag } = encrypt(password, this.encryptionKey);
4876
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4877
+ const credential = {
4878
+ name,
4879
+ url,
4880
+ username,
4881
+ encrypted: true,
4882
+ iv,
4883
+ authTag,
4884
+ data: ciphertext,
4885
+ usernameSelector: selectors?.username,
4886
+ passwordSelector: selectors?.password,
4887
+ submitSelector: selectors?.submit,
4888
+ createdAt: now,
4889
+ updatedAt: now
4890
+ };
4891
+ const filePath = path10.join(this.authDir, `${sanitizeName(name)}.json`);
4892
+ fs12.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 384 });
4893
+ }
4894
+ load(name) {
4895
+ const filePath = path10.join(this.authDir, `${sanitizeName(name)}.json`);
4896
+ if (!fs12.existsSync(filePath)) {
4897
+ throw new Error(`Credential "${name}" not found. Run "browse auth list" to see saved credentials.`);
4898
+ }
4899
+ return JSON.parse(fs12.readFileSync(filePath, "utf-8"));
4900
+ }
4901
+ async login(name, bm) {
4902
+ const cred = this.load(name);
4903
+ const password = decrypt(cred.data, cred.iv, cred.authTag, this.encryptionKey);
4904
+ const page = bm.getPage();
4905
+ await page.goto(cred.url, {
4906
+ waitUntil: "domcontentloaded",
4907
+ timeout: DEFAULTS.COMMAND_TIMEOUT_MS
4908
+ });
4909
+ const userSel = cred.usernameSelector || await autoDetectSelector(page, "username");
4910
+ const passSel = cred.passwordSelector || await autoDetectSelector(page, "password");
4911
+ const submitSel = cred.submitSelector || await autoDetectSelector(page, "submit");
4912
+ await page.fill(userSel, cred.username, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4913
+ await page.fill(passSel, password, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4914
+ await page.click(submitSel, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
4915
+ await page.waitForLoadState("domcontentloaded").catch(() => {
4916
+ });
4917
+ return `Logged in as ${cred.username} at ${page.url()}`;
4918
+ }
4919
+ list() {
4920
+ if (!fs12.existsSync(this.authDir)) return [];
4921
+ const files = fs12.readdirSync(this.authDir).filter((f) => f.endsWith(".json"));
4922
+ return files.map((f) => {
4923
+ try {
4924
+ const data = JSON.parse(fs12.readFileSync(path10.join(this.authDir, f), "utf-8"));
4925
+ return {
4926
+ name: data.name,
4927
+ url: data.url,
4928
+ username: data.username,
4929
+ hasPassword: true,
4930
+ createdAt: data.createdAt
4931
+ };
4932
+ } catch {
4933
+ return null;
4934
+ }
4935
+ }).filter(Boolean);
4936
+ }
4937
+ delete(name) {
4938
+ const filePath = path10.join(this.authDir, `${sanitizeName(name)}.json`);
4939
+ if (!fs12.existsSync(filePath)) {
4940
+ throw new Error(`Credential "${name}" not found.`);
4941
+ }
4942
+ fs12.unlinkSync(filePath);
4927
4943
  }
4928
4944
  };
4929
- BROWSER_REGISTRY = [
4930
- { name: "Chrome", dataDir: "Google/Chrome/", keychainService: "Chrome Safe Storage", aliases: ["chrome", "google-chrome"] },
4931
- { name: "Arc", dataDir: "Arc/User Data/", keychainService: "Arc Safe Storage", aliases: ["arc"] },
4932
- { name: "Brave", dataDir: "BraveSoftware/Brave-Browser/", keychainService: "Brave Safe Storage", aliases: ["brave"] },
4933
- { name: "Edge", dataDir: "Microsoft Edge/", keychainService: "Microsoft Edge Safe Storage", aliases: ["edge"] }
4934
- ];
4935
- keyCache = /* @__PURE__ */ new Map();
4936
- CHROMIUM_EPOCH_OFFSET = 11644473600000000n;
4937
4945
  }
4938
4946
  });
4939
4947
 
@@ -9904,6 +9912,9 @@ async function main() {
9904
9912
  runtimeName = "chrome";
9905
9913
  headed = true;
9906
9914
  }
9915
+ if (runtimeName === "chrome") {
9916
+ headed = true;
9917
+ }
9907
9918
  cliFlags.json = jsonMode;
9908
9919
  cliFlags.contentBoundaries = contentBoundaries;
9909
9920
  cliFlags.allowedDomains = allowedDomains || "";