@ulpi/browse 1.3.0 → 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/README.md +1 -1
- package/dist/browse.cjs +906 -864
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
- package/skill/references/permissions.md +3 -1
package/dist/browse.cjs
CHANGED
|
@@ -62,310 +62,634 @@ var init_constants = __esm({
|
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
// src/
|
|
66
|
-
var
|
|
67
|
-
__export(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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(
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
stderr
|
|
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"
|
|
259
281
|
);
|
|
260
282
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
+
);
|
|
267
291
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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}`);
|
|
271
300
|
}
|
|
272
|
-
const
|
|
273
|
-
const
|
|
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) {
|
|
274
309
|
return {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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)
|
|
282
318
|
};
|
|
283
319
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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"() {
|
|
287
344
|
"use strict";
|
|
288
|
-
|
|
345
|
+
import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
346
|
+
import_child_process = require("child_process");
|
|
347
|
+
crypto = __toESM(require("crypto"), 1);
|
|
289
348
|
fs2 = __toESM(require("fs"), 1);
|
|
290
349
|
path2 = __toESM(require("path"), 1);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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"] }
|
|
299
364
|
];
|
|
300
|
-
|
|
365
|
+
keyCache = /* @__PURE__ */ new Map();
|
|
366
|
+
CHROMIUM_EPOCH_OFFSET = 11644473600000000n;
|
|
301
367
|
}
|
|
302
368
|
});
|
|
303
369
|
|
|
304
|
-
// src/
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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;
|
|
317
390
|
}
|
|
318
|
-
const key = crypto.randomBytes(32);
|
|
319
|
-
fs3.writeFileSync(keyPath, key.toString("hex") + "\n", { mode: 384 });
|
|
320
|
-
return key;
|
|
321
|
-
}
|
|
322
|
-
function encrypt(plaintext, key) {
|
|
323
|
-
const iv = crypto.randomBytes(12);
|
|
324
|
-
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
325
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
326
|
-
return {
|
|
327
|
-
ciphertext: encrypted.toString("base64"),
|
|
328
|
-
iv: iv.toString("base64"),
|
|
329
|
-
authTag: cipher.getAuthTag().toString("base64")
|
|
330
|
-
};
|
|
331
391
|
}
|
|
332
|
-
function
|
|
333
|
-
|
|
334
|
-
"
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
decipher.update(Buffer.from(ciphertext, "base64")),
|
|
341
|
-
decipher.final()
|
|
342
|
-
]);
|
|
343
|
-
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
|
+
}
|
|
344
400
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
"
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
}
|
|
352
410
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
function sanitizeName(name) {
|
|
357
|
-
const sanitized = name.replace(/[\/\\]/g, "_").replace(/\.\./g, "_");
|
|
358
|
-
if (!sanitized || /^[._]+$/.test(sanitized)) {
|
|
359
|
-
throw new Error(`Invalid name: "${name}"`);
|
|
411
|
+
for (const port of PROBE_PORTS) {
|
|
412
|
+
const wsUrl = await fetchWsUrl(port);
|
|
413
|
+
if (wsUrl) return wsUrl;
|
|
360
414
|
}
|
|
361
|
-
return
|
|
415
|
+
return null;
|
|
362
416
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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";
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
369
693
|
// src/cloud-providers.ts
|
|
370
694
|
var cloud_providers_exports = {};
|
|
371
695
|
__export(cloud_providers_exports, {
|
|
@@ -400,12 +724,12 @@ async function cleanupProvider(providerName, apiKey, sessionId) {
|
|
|
400
724
|
await provider.cleanup(apiKey, sessionId);
|
|
401
725
|
}
|
|
402
726
|
}
|
|
403
|
-
var
|
|
727
|
+
var fs5, path5, providers, ProviderVault;
|
|
404
728
|
var init_cloud_providers = __esm({
|
|
405
729
|
"src/cloud-providers.ts"() {
|
|
406
730
|
"use strict";
|
|
407
|
-
|
|
408
|
-
|
|
731
|
+
fs5 = __toESM(require("fs"), 1);
|
|
732
|
+
path5 = __toESM(require("path"), 1);
|
|
409
733
|
init_encryption();
|
|
410
734
|
init_sanitize();
|
|
411
735
|
providers = {
|
|
@@ -451,12 +775,12 @@ var init_cloud_providers = __esm({
|
|
|
451
775
|
dir;
|
|
452
776
|
encryptionKey;
|
|
453
777
|
constructor(localDir) {
|
|
454
|
-
this.dir =
|
|
778
|
+
this.dir = path5.join(localDir, "providers");
|
|
455
779
|
this.encryptionKey = resolveEncryptionKey(localDir);
|
|
456
780
|
}
|
|
457
781
|
save(name, apiKey) {
|
|
458
782
|
getProvider(name);
|
|
459
|
-
|
|
783
|
+
fs5.mkdirSync(this.dir, { recursive: true });
|
|
460
784
|
const { ciphertext, iv, authTag } = encrypt(apiKey, this.encryptionKey);
|
|
461
785
|
const stored = {
|
|
462
786
|
name,
|
|
@@ -466,34 +790,34 @@ var init_cloud_providers = __esm({
|
|
|
466
790
|
data: ciphertext,
|
|
467
791
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
468
792
|
};
|
|
469
|
-
const filePath =
|
|
470
|
-
|
|
793
|
+
const filePath = path5.join(this.dir, `${sanitizeName(name)}.json`);
|
|
794
|
+
fs5.writeFileSync(filePath, JSON.stringify(stored, null, 2), { mode: 384 });
|
|
471
795
|
}
|
|
472
796
|
load(name) {
|
|
473
|
-
const filePath =
|
|
474
|
-
if (!
|
|
797
|
+
const filePath = path5.join(this.dir, `${sanitizeName(name)}.json`);
|
|
798
|
+
if (!fs5.existsSync(filePath)) {
|
|
475
799
|
throw new Error(
|
|
476
800
|
`No API key saved for "${name}". Run: browse provider save ${name} <api-key>`
|
|
477
801
|
);
|
|
478
802
|
}
|
|
479
|
-
const stored = JSON.parse(
|
|
803
|
+
const stored = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
|
|
480
804
|
return decrypt(stored.data, stored.iv, stored.authTag, this.encryptionKey);
|
|
481
805
|
}
|
|
482
806
|
list() {
|
|
483
|
-
if (!
|
|
484
|
-
return
|
|
807
|
+
if (!fs5.existsSync(this.dir)) return [];
|
|
808
|
+
return fs5.readdirSync(this.dir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
485
809
|
const stored = JSON.parse(
|
|
486
|
-
|
|
810
|
+
fs5.readFileSync(path5.join(this.dir, f), "utf-8")
|
|
487
811
|
);
|
|
488
812
|
return { name: stored.name, createdAt: stored.createdAt };
|
|
489
813
|
});
|
|
490
814
|
}
|
|
491
815
|
delete(name) {
|
|
492
|
-
const filePath =
|
|
493
|
-
if (!
|
|
816
|
+
const filePath = path5.join(this.dir, `${sanitizeName(name)}.json`);
|
|
817
|
+
if (!fs5.existsSync(filePath)) {
|
|
494
818
|
throw new Error(`No saved key for "${name}"`);
|
|
495
819
|
}
|
|
496
|
-
|
|
820
|
+
fs5.unlinkSync(filePath);
|
|
497
821
|
}
|
|
498
822
|
};
|
|
499
823
|
}
|
|
@@ -504,7 +828,7 @@ var require_package = __commonJS({
|
|
|
504
828
|
"package.json"(exports2, module2) {
|
|
505
829
|
module2.exports = {
|
|
506
830
|
name: "@ulpi/browse",
|
|
507
|
-
version: "1.3.
|
|
831
|
+
version: "1.3.2",
|
|
508
832
|
repository: {
|
|
509
833
|
type: "git",
|
|
510
834
|
url: "https://github.com/ulpi-io/browse"
|
|
@@ -580,45 +904,45 @@ __export(install_skill_exports, {
|
|
|
580
904
|
});
|
|
581
905
|
function installSkill(targetDir) {
|
|
582
906
|
const dir = targetDir || process.cwd();
|
|
583
|
-
const hasGit =
|
|
584
|
-
const hasClaude =
|
|
907
|
+
const hasGit = fs6.existsSync(path6.join(dir, ".git"));
|
|
908
|
+
const hasClaude = fs6.existsSync(path6.join(dir, ".claude"));
|
|
585
909
|
if (!hasGit && !hasClaude) {
|
|
586
910
|
console.error(`Not a project root: ${dir}`);
|
|
587
911
|
console.error("Run from a directory with .git or .claude, or pass the path as an argument.");
|
|
588
912
|
process.exit(1);
|
|
589
913
|
}
|
|
590
|
-
const skillDir =
|
|
591
|
-
|
|
592
|
-
const skillSourceDir =
|
|
593
|
-
if (!
|
|
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)) {
|
|
594
918
|
console.error(`Skill directory not found at ${skillSourceDir}`);
|
|
595
919
|
console.error("Is @ulpi/browse installed correctly?");
|
|
596
920
|
process.exit(1);
|
|
597
921
|
}
|
|
598
|
-
const mdFiles =
|
|
922
|
+
const mdFiles = fs6.readdirSync(skillSourceDir).filter((f) => f.endsWith(".md"));
|
|
599
923
|
if (mdFiles.length === 0) {
|
|
600
924
|
console.error(`No .md files found in ${skillSourceDir}`);
|
|
601
925
|
process.exit(1);
|
|
602
926
|
}
|
|
603
927
|
for (const file of mdFiles) {
|
|
604
|
-
|
|
605
|
-
console.log(`Skill installed: ${
|
|
606
|
-
}
|
|
607
|
-
const refsSourceDir =
|
|
608
|
-
if (
|
|
609
|
-
const refsDestDir =
|
|
610
|
-
|
|
611
|
-
const refFiles =
|
|
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"));
|
|
612
936
|
for (const file of refFiles) {
|
|
613
|
-
|
|
614
|
-
console.log(`Skill installed: ${
|
|
937
|
+
fs6.copyFileSync(path6.join(refsSourceDir, file), path6.join(refsDestDir, file));
|
|
938
|
+
console.log(`Skill installed: ${path6.relative(dir, path6.join(refsDestDir, file))}`);
|
|
615
939
|
}
|
|
616
940
|
}
|
|
617
|
-
const settingsPath =
|
|
941
|
+
const settingsPath = path6.join(dir, ".claude", "settings.json");
|
|
618
942
|
let settings = {};
|
|
619
|
-
if (
|
|
943
|
+
if (fs6.existsSync(settingsPath)) {
|
|
620
944
|
try {
|
|
621
|
-
settings = JSON.parse(
|
|
945
|
+
settings = JSON.parse(fs6.readFileSync(settingsPath, "utf-8"));
|
|
622
946
|
} catch {
|
|
623
947
|
console.error(`Warning: could not parse ${settingsPath}, creating fresh`);
|
|
624
948
|
settings = {};
|
|
@@ -634,20 +958,20 @@ function installSkill(targetDir) {
|
|
|
634
958
|
added++;
|
|
635
959
|
}
|
|
636
960
|
}
|
|
637
|
-
|
|
961
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
638
962
|
if (added > 0) {
|
|
639
|
-
console.log(`Permissions: ${added} rules added to ${
|
|
963
|
+
console.log(`Permissions: ${added} rules added to ${path6.relative(dir, settingsPath)}`);
|
|
640
964
|
} else {
|
|
641
965
|
console.log(`Permissions: already configured`);
|
|
642
966
|
}
|
|
643
967
|
console.log("\nDone. Claude Code will now use browse for web tasks automatically.");
|
|
644
968
|
}
|
|
645
|
-
var
|
|
969
|
+
var fs6, path6, import_url, PERMISSIONS;
|
|
646
970
|
var init_install_skill = __esm({
|
|
647
971
|
"src/install-skill.ts"() {
|
|
648
972
|
"use strict";
|
|
649
|
-
|
|
650
|
-
|
|
973
|
+
fs6 = __toESM(require("fs"), 1);
|
|
974
|
+
path6 = __toESM(require("path"), 1);
|
|
651
975
|
import_url = require("url");
|
|
652
976
|
PERMISSIONS = [
|
|
653
977
|
"Bash(browse:*)",
|
|
@@ -809,21 +1133,21 @@ function listDevices() {
|
|
|
809
1133
|
function getProfileDir(localDir, name) {
|
|
810
1134
|
const sanitized = sanitizeName(name);
|
|
811
1135
|
if (!sanitized) throw new Error("Invalid profile name");
|
|
812
|
-
return
|
|
1136
|
+
return path7.join(localDir, "profiles", sanitized);
|
|
813
1137
|
}
|
|
814
1138
|
function listProfiles(localDir) {
|
|
815
|
-
const profilesDir =
|
|
816
|
-
if (!
|
|
817
|
-
return
|
|
818
|
-
const dir =
|
|
819
|
-
const stat =
|
|
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);
|
|
820
1144
|
let totalSize = 0;
|
|
821
1145
|
try {
|
|
822
|
-
const files =
|
|
1146
|
+
const files = fs7.readdirSync(dir, { recursive: true, withFileTypes: true });
|
|
823
1147
|
for (const f of files) {
|
|
824
1148
|
if (f.isFile()) {
|
|
825
1149
|
try {
|
|
826
|
-
totalSize +=
|
|
1150
|
+
totalSize += fs7.statSync(path7.join(f.parentPath || f.path || dir, f.name)).size;
|
|
827
1151
|
} catch {
|
|
828
1152
|
}
|
|
829
1153
|
}
|
|
@@ -840,15 +1164,15 @@ function listProfiles(localDir) {
|
|
|
840
1164
|
}
|
|
841
1165
|
function deleteProfile(localDir, name) {
|
|
842
1166
|
const dir = getProfileDir(localDir, name);
|
|
843
|
-
if (!
|
|
844
|
-
|
|
1167
|
+
if (!fs7.existsSync(dir)) throw new Error(`Profile "${name}" not found`);
|
|
1168
|
+
fs7.rmSync(dir, { recursive: true, force: true });
|
|
845
1169
|
}
|
|
846
|
-
var
|
|
1170
|
+
var path7, fs7, import_playwright, DEVICE_ALIASES, CUSTOM_DEVICES, BrowserManager;
|
|
847
1171
|
var init_browser_manager = __esm({
|
|
848
1172
|
"src/browser-manager.ts"() {
|
|
849
1173
|
"use strict";
|
|
850
|
-
|
|
851
|
-
|
|
1174
|
+
path7 = __toESM(require("path"), 1);
|
|
1175
|
+
fs7 = __toESM(require("fs"), 1);
|
|
852
1176
|
import_playwright = require("playwright");
|
|
853
1177
|
init_buffers();
|
|
854
1178
|
init_sanitize();
|
|
@@ -1001,17 +1325,34 @@ var init_browser_manager = __esm({
|
|
|
1001
1325
|
* Creates a new BrowserContext on the shared browser.
|
|
1002
1326
|
* This instance does NOT own the browser — close() only closes the context.
|
|
1003
1327
|
*/
|
|
1004
|
-
async launchWithBrowser(browser2) {
|
|
1328
|
+
async launchWithBrowser(browser2, reuseContext = false) {
|
|
1005
1329
|
this.browser = browser2;
|
|
1006
1330
|
this.ownsBrowser = false;
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1331
|
+
if (reuseContext) {
|
|
1332
|
+
const contexts = browser2.contexts();
|
|
1333
|
+
this.context = contexts[0] || await browser2.newContext({
|
|
1334
|
+
viewport: { width: 1920, height: 1080 }
|
|
1335
|
+
});
|
|
1336
|
+
const pages = this.context.pages();
|
|
1337
|
+
if (pages.length > 0) {
|
|
1338
|
+
for (const page of pages) {
|
|
1339
|
+
const tabId = this.nextTabId++;
|
|
1340
|
+
this.wirePageEvents(page);
|
|
1341
|
+
this.pages.set(tabId, page);
|
|
1342
|
+
this.activeTabId = tabId;
|
|
1343
|
+
}
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
} else {
|
|
1347
|
+
this.context = await browser2.newContext({
|
|
1348
|
+
viewport: { width: 1920, height: 1080 },
|
|
1349
|
+
...this.customUserAgent ? { userAgent: this.customUserAgent } : {}
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
await this.newTab();
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Launch with a persistent browser profile directory.
|
|
1015
1356
|
* Data (cookies, localStorage, cache) persists across restarts.
|
|
1016
1357
|
* The context IS the browser — closing it closes everything.
|
|
1017
1358
|
*/
|
|
@@ -2201,8 +2542,8 @@ async function handleReadCommand(command, args, bm, buffers) {
|
|
|
2201
2542
|
case "eval": {
|
|
2202
2543
|
const filePath = args[0];
|
|
2203
2544
|
if (!filePath) throw new Error("Usage: browse eval <js-file>");
|
|
2204
|
-
if (!
|
|
2205
|
-
const code =
|
|
2545
|
+
if (!fs8.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
2546
|
+
const code = fs8.readFileSync(filePath, "utf-8");
|
|
2206
2547
|
const result = await evalCtx.evaluate(code);
|
|
2207
2548
|
return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result ?? "");
|
|
2208
2549
|
}
|
|
@@ -2440,13 +2781,13 @@ async function handleReadCommand(command, args, bm, buffers) {
|
|
|
2440
2781
|
throw new Error(`Unknown read command: ${command}`);
|
|
2441
2782
|
}
|
|
2442
2783
|
}
|
|
2443
|
-
var
|
|
2784
|
+
var fs8;
|
|
2444
2785
|
var init_read = __esm({
|
|
2445
2786
|
"src/commands/read.ts"() {
|
|
2446
2787
|
"use strict";
|
|
2447
2788
|
init_browser_manager();
|
|
2448
2789
|
init_constants();
|
|
2449
|
-
|
|
2790
|
+
fs8 = __toESM(require("fs"), 1);
|
|
2450
2791
|
}
|
|
2451
2792
|
});
|
|
2452
2793
|
|
|
@@ -2673,14 +3014,14 @@ async function handleWriteCommand(command, args, bm, domainFilter) {
|
|
|
2673
3014
|
const file = args[1];
|
|
2674
3015
|
if (!file) throw new Error("Usage: browse cookie export <file>");
|
|
2675
3016
|
const cookies = await page.context().cookies();
|
|
2676
|
-
|
|
3017
|
+
fs9.writeFileSync(file, JSON.stringify(cookies, null, 2));
|
|
2677
3018
|
return `Exported ${cookies.length} cookie(s) to ${file}`;
|
|
2678
3019
|
}
|
|
2679
3020
|
if (cookieStr === "import") {
|
|
2680
3021
|
const file = args[1];
|
|
2681
3022
|
if (!file) throw new Error("Usage: browse cookie import <file>");
|
|
2682
|
-
if (!
|
|
2683
|
-
const cookies = JSON.parse(
|
|
3023
|
+
if (!fs9.existsSync(file)) throw new Error(`File not found: ${file}`);
|
|
3024
|
+
const cookies = JSON.parse(fs9.readFileSync(file, "utf-8"));
|
|
2684
3025
|
if (!Array.isArray(cookies)) throw new Error("Cookie file must contain a JSON array of cookie objects");
|
|
2685
3026
|
await page.context().addCookies(cookies);
|
|
2686
3027
|
return `Imported ${cookies.length} cookie(s) from ${file}`;
|
|
@@ -2747,7 +3088,7 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
|
|
|
2747
3088
|
const [selector, ...filePaths] = args;
|
|
2748
3089
|
if (!selector || filePaths.length === 0) throw new Error("Usage: browse upload <selector> <file1> [file2] ...");
|
|
2749
3090
|
for (const fp of filePaths) {
|
|
2750
|
-
if (!
|
|
3091
|
+
if (!fs9.existsSync(fp)) throw new Error(`File not found: ${fp}`);
|
|
2751
3092
|
}
|
|
2752
3093
|
const resolved = bm.resolveRef(selector);
|
|
2753
3094
|
if ("locator" in resolved) {
|
|
@@ -3083,13 +3424,13 @@ Note: Cookies and tab URLs preserved. localStorage/sessionStorage were reset (Pl
|
|
|
3083
3424
|
throw new Error(`Unknown write command: ${command}`);
|
|
3084
3425
|
}
|
|
3085
3426
|
}
|
|
3086
|
-
var
|
|
3427
|
+
var fs9;
|
|
3087
3428
|
var init_write = __esm({
|
|
3088
3429
|
"src/commands/write.ts"() {
|
|
3089
3430
|
"use strict";
|
|
3090
3431
|
init_browser_manager();
|
|
3091
3432
|
init_constants();
|
|
3092
|
-
|
|
3433
|
+
fs9 = __toESM(require("fs"), 1);
|
|
3093
3434
|
}
|
|
3094
3435
|
});
|
|
3095
3436
|
|
|
@@ -3893,7 +4234,7 @@ var init_lib = __esm({
|
|
|
3893
4234
|
tokenize: function tokenize(value) {
|
|
3894
4235
|
return Array.from(value);
|
|
3895
4236
|
},
|
|
3896
|
-
join: function
|
|
4237
|
+
join: function join8(chars) {
|
|
3897
4238
|
return chars.join("");
|
|
3898
4239
|
},
|
|
3899
4240
|
postProcess: function postProcess(changeObjects) {
|
|
@@ -4072,20 +4413,20 @@ async function saveSessionState(sessionDir, context, encryptionKey) {
|
|
|
4072
4413
|
} else {
|
|
4073
4414
|
content = json;
|
|
4074
4415
|
}
|
|
4075
|
-
|
|
4076
|
-
|
|
4416
|
+
fs10.mkdirSync(sessionDir, { recursive: true });
|
|
4417
|
+
fs10.writeFileSync(path8.join(sessionDir, STATE_FILENAME), content, { mode: 384 });
|
|
4077
4418
|
} catch (err) {
|
|
4078
4419
|
console.log(`[session-persist] Warning: failed to save state: ${err.message}`);
|
|
4079
4420
|
}
|
|
4080
4421
|
}
|
|
4081
4422
|
async function loadSessionState(sessionDir, context, encryptionKey) {
|
|
4082
|
-
const statePath =
|
|
4083
|
-
if (!
|
|
4423
|
+
const statePath = path8.join(sessionDir, STATE_FILENAME);
|
|
4424
|
+
if (!fs10.existsSync(statePath)) {
|
|
4084
4425
|
return false;
|
|
4085
4426
|
}
|
|
4086
4427
|
let stateData;
|
|
4087
4428
|
try {
|
|
4088
|
-
const raw =
|
|
4429
|
+
const raw = fs10.readFileSync(statePath, "utf-8");
|
|
4089
4430
|
const parsed = JSON.parse(raw);
|
|
4090
4431
|
if (parsed.encrypted) {
|
|
4091
4432
|
if (!encryptionKey) {
|
|
@@ -4141,24 +4482,24 @@ async function loadSessionState(sessionDir, context, encryptionKey) {
|
|
|
4141
4482
|
}
|
|
4142
4483
|
}
|
|
4143
4484
|
function hasPersistedState(sessionDir) {
|
|
4144
|
-
return
|
|
4485
|
+
return fs10.existsSync(path8.join(sessionDir, STATE_FILENAME));
|
|
4145
4486
|
}
|
|
4146
4487
|
function cleanOldStates(localDir, maxAgeDays) {
|
|
4147
4488
|
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
4148
4489
|
const now = Date.now();
|
|
4149
4490
|
let deleted = 0;
|
|
4150
|
-
const statesDir =
|
|
4151
|
-
if (
|
|
4491
|
+
const statesDir = path8.join(localDir, "states");
|
|
4492
|
+
if (fs10.existsSync(statesDir)) {
|
|
4152
4493
|
try {
|
|
4153
|
-
const entries =
|
|
4494
|
+
const entries = fs10.readdirSync(statesDir);
|
|
4154
4495
|
for (const entry of entries) {
|
|
4155
4496
|
if (!entry.endsWith(".json")) continue;
|
|
4156
|
-
const filePath =
|
|
4497
|
+
const filePath = path8.join(statesDir, entry);
|
|
4157
4498
|
try {
|
|
4158
|
-
const stat =
|
|
4499
|
+
const stat = fs10.statSync(filePath);
|
|
4159
4500
|
if (!stat.isFile()) continue;
|
|
4160
4501
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
4161
|
-
|
|
4502
|
+
fs10.unlinkSync(filePath);
|
|
4162
4503
|
deleted++;
|
|
4163
4504
|
}
|
|
4164
4505
|
} catch (_) {
|
|
@@ -4167,23 +4508,23 @@ function cleanOldStates(localDir, maxAgeDays) {
|
|
|
4167
4508
|
} catch (_) {
|
|
4168
4509
|
}
|
|
4169
4510
|
}
|
|
4170
|
-
const sessionsDir =
|
|
4171
|
-
if (
|
|
4511
|
+
const sessionsDir = path8.join(localDir, "sessions");
|
|
4512
|
+
if (fs10.existsSync(sessionsDir)) {
|
|
4172
4513
|
try {
|
|
4173
|
-
const sessionDirs =
|
|
4514
|
+
const sessionDirs = fs10.readdirSync(sessionsDir);
|
|
4174
4515
|
for (const dir of sessionDirs) {
|
|
4175
|
-
const dirPath =
|
|
4516
|
+
const dirPath = path8.join(sessionsDir, dir);
|
|
4176
4517
|
try {
|
|
4177
|
-
const dirStat =
|
|
4518
|
+
const dirStat = fs10.statSync(dirPath);
|
|
4178
4519
|
if (!dirStat.isDirectory()) continue;
|
|
4179
4520
|
} catch (_) {
|
|
4180
4521
|
continue;
|
|
4181
4522
|
}
|
|
4182
|
-
const statePath =
|
|
4523
|
+
const statePath = path8.join(dirPath, STATE_FILENAME);
|
|
4183
4524
|
try {
|
|
4184
|
-
const stat =
|
|
4525
|
+
const stat = fs10.statSync(statePath);
|
|
4185
4526
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
4186
|
-
|
|
4527
|
+
fs10.unlinkSync(statePath);
|
|
4187
4528
|
deleted++;
|
|
4188
4529
|
}
|
|
4189
4530
|
} catch (_) {
|
|
@@ -4194,12 +4535,12 @@ function cleanOldStates(localDir, maxAgeDays) {
|
|
|
4194
4535
|
}
|
|
4195
4536
|
return { deleted };
|
|
4196
4537
|
}
|
|
4197
|
-
var
|
|
4538
|
+
var fs10, path8, STATE_FILENAME;
|
|
4198
4539
|
var init_session_persist = __esm({
|
|
4199
4540
|
"src/session-persist.ts"() {
|
|
4200
4541
|
"use strict";
|
|
4201
|
-
|
|
4202
|
-
|
|
4542
|
+
fs10 = __toESM(require("fs"), 1);
|
|
4543
|
+
path8 = __toESM(require("path"), 1);
|
|
4203
4544
|
init_encryption();
|
|
4204
4545
|
STATE_FILENAME = "state.json";
|
|
4205
4546
|
}
|
|
@@ -4213,20 +4554,20 @@ __export(policy_exports, {
|
|
|
4213
4554
|
function findFileUpward(filename) {
|
|
4214
4555
|
let dir = process.cwd();
|
|
4215
4556
|
for (let i = 0; i < 20; i++) {
|
|
4216
|
-
const candidate =
|
|
4217
|
-
if (
|
|
4218
|
-
const parent =
|
|
4557
|
+
const candidate = path9.join(dir, filename);
|
|
4558
|
+
if (fs11.existsSync(candidate)) return candidate;
|
|
4559
|
+
const parent = path9.dirname(dir);
|
|
4219
4560
|
if (parent === dir) break;
|
|
4220
4561
|
dir = parent;
|
|
4221
4562
|
}
|
|
4222
4563
|
return null;
|
|
4223
4564
|
}
|
|
4224
|
-
var
|
|
4565
|
+
var fs11, path9, PolicyChecker;
|
|
4225
4566
|
var init_policy = __esm({
|
|
4226
4567
|
"src/policy.ts"() {
|
|
4227
4568
|
"use strict";
|
|
4228
|
-
|
|
4229
|
-
|
|
4569
|
+
fs11 = __toESM(require("fs"), 1);
|
|
4570
|
+
path9 = __toESM(require("path"), 1);
|
|
4230
4571
|
PolicyChecker = class {
|
|
4231
4572
|
filePath = null;
|
|
4232
4573
|
lastMtime = 0;
|
|
@@ -4245,10 +4586,10 @@ var init_policy = __esm({
|
|
|
4245
4586
|
reload() {
|
|
4246
4587
|
if (!this.filePath) return;
|
|
4247
4588
|
try {
|
|
4248
|
-
const stat =
|
|
4589
|
+
const stat = fs11.statSync(this.filePath);
|
|
4249
4590
|
if (stat.mtimeMs === this.lastMtime) return;
|
|
4250
4591
|
this.lastMtime = stat.mtimeMs;
|
|
4251
|
-
const raw =
|
|
4592
|
+
const raw = fs11.readFileSync(this.filePath, "utf-8");
|
|
4252
4593
|
this.policy = JSON.parse(raw);
|
|
4253
4594
|
} catch {
|
|
4254
4595
|
}
|
|
@@ -4396,516 +4737,211 @@ function generateDiffImage(base, curr, colorThreshold) {
|
|
|
4396
4737
|
diffData[di + 1] = 0;
|
|
4397
4738
|
diffData[di + 2] = 0;
|
|
4398
4739
|
diffData[di + 3] = 255;
|
|
4399
|
-
continue;
|
|
4400
|
-
}
|
|
4401
|
-
const bi = (y * base.width + x) * 4;
|
|
4402
|
-
const ci = (y * curr.width + x) * 4;
|
|
4403
|
-
const dr = base.data[bi] - curr.data[ci];
|
|
4404
|
-
const dg = base.data[bi + 1] - curr.data[ci + 1];
|
|
4405
|
-
const db = base.data[bi + 2] - curr.data[ci + 2];
|
|
4406
|
-
const distSq = dr * dr + dg * dg + db * db;
|
|
4407
|
-
const isDiff = colorThreshold === 0 ? distSq > 0 : distSq > colorThreshSq;
|
|
4408
|
-
if (isDiff) {
|
|
4409
|
-
diffData[di] = 255;
|
|
4410
|
-
diffData[di + 1] = curr.data[ci + 1] / 3 | 0;
|
|
4411
|
-
diffData[di + 2] = curr.data[ci + 2] / 3 | 0;
|
|
4412
|
-
diffData[di + 3] = 255;
|
|
4413
|
-
} else {
|
|
4414
|
-
diffData[di] = curr.data[ci] / 3 | 0;
|
|
4415
|
-
diffData[di + 1] = curr.data[ci + 1] / 3 | 0;
|
|
4416
|
-
diffData[di + 2] = curr.data[ci + 2] / 3 | 0;
|
|
4417
|
-
diffData[di + 3] = 128;
|
|
4418
|
-
}
|
|
4419
|
-
}
|
|
4420
|
-
}
|
|
4421
|
-
return encodePNG({ width: w, height: h, data: diffData });
|
|
4422
|
-
}
|
|
4423
|
-
function compareScreenshots(baselineBuf, currentBuf, thresholdPct = 0.1, colorThreshold = 30) {
|
|
4424
|
-
const base = decodePNG(baselineBuf);
|
|
4425
|
-
const curr = decodePNG(currentBuf);
|
|
4426
|
-
const w = Math.max(base.width, curr.width);
|
|
4427
|
-
const h = Math.max(base.height, curr.height);
|
|
4428
|
-
const totalPixels = w * h;
|
|
4429
|
-
let diffPixels = 0;
|
|
4430
|
-
const colorThreshSq = colorThreshold * colorThreshold * 3;
|
|
4431
|
-
for (let y = 0; y < h; y++) {
|
|
4432
|
-
for (let x = 0; x < w; x++) {
|
|
4433
|
-
const inBase = x < base.width && y < base.height;
|
|
4434
|
-
const inCurr = x < curr.width && y < curr.height;
|
|
4435
|
-
if (!inBase || !inCurr) {
|
|
4436
|
-
diffPixels++;
|
|
4437
|
-
continue;
|
|
4438
|
-
}
|
|
4439
|
-
const bi = (y * base.width + x) * 4;
|
|
4440
|
-
const ci = (y * curr.width + x) * 4;
|
|
4441
|
-
const dr = base.data[bi] - curr.data[ci];
|
|
4442
|
-
const dg = base.data[bi + 1] - curr.data[ci + 1];
|
|
4443
|
-
const db = base.data[bi + 2] - curr.data[ci + 2];
|
|
4444
|
-
const distSq = dr * dr + dg * dg + db * db;
|
|
4445
|
-
if (colorThreshold === 0 ? distSq > 0 : distSq > colorThreshSq) diffPixels++;
|
|
4446
|
-
}
|
|
4447
|
-
}
|
|
4448
|
-
const mismatchPct = totalPixels > 0 ? diffPixels / totalPixels * 100 : 0;
|
|
4449
|
-
const passed = mismatchPct <= thresholdPct;
|
|
4450
|
-
const result = { totalPixels, diffPixels, mismatchPct, passed };
|
|
4451
|
-
if (!passed) {
|
|
4452
|
-
result.diffImage = generateDiffImage(base, curr, colorThreshold);
|
|
4453
|
-
}
|
|
4454
|
-
return result;
|
|
4455
|
-
}
|
|
4456
|
-
var zlib, PNG_MAGIC;
|
|
4457
|
-
var init_png_compare = __esm({
|
|
4458
|
-
"src/png-compare.ts"() {
|
|
4459
|
-
"use strict";
|
|
4460
|
-
zlib = __toESM(require("zlib"), 1);
|
|
4461
|
-
PNG_MAGIC = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
4462
|
-
}
|
|
4463
|
-
});
|
|
4464
|
-
|
|
4465
|
-
// src/auth-vault.ts
|
|
4466
|
-
var auth_vault_exports = {};
|
|
4467
|
-
__export(auth_vault_exports, {
|
|
4468
|
-
AuthVault: () => AuthVault
|
|
4469
|
-
});
|
|
4470
|
-
async function autoDetectSelector(page, field) {
|
|
4471
|
-
if (field === "username") {
|
|
4472
|
-
const candidates2 = [
|
|
4473
|
-
'input[type="email"]',
|
|
4474
|
-
'input[name="email"]',
|
|
4475
|
-
'input[name="username"]',
|
|
4476
|
-
'input[name="user"]',
|
|
4477
|
-
'input[name="login"]',
|
|
4478
|
-
'input[autocomplete="username"]',
|
|
4479
|
-
'input[autocomplete="email"]',
|
|
4480
|
-
'input[type="text"]:first-of-type'
|
|
4481
|
-
];
|
|
4482
|
-
for (const sel of candidates2) {
|
|
4483
|
-
const count = await page.locator(sel).count();
|
|
4484
|
-
if (count > 0) return sel;
|
|
4485
|
-
}
|
|
4486
|
-
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>");
|
|
4487
|
-
}
|
|
4488
|
-
if (field === "password") {
|
|
4489
|
-
const candidates2 = [
|
|
4490
|
-
'input[type="password"]',
|
|
4491
|
-
'input[name="password"]',
|
|
4492
|
-
'input[name="pass"]',
|
|
4493
|
-
'input[autocomplete="current-password"]'
|
|
4494
|
-
];
|
|
4495
|
-
for (const sel of candidates2) {
|
|
4496
|
-
const count = await page.locator(sel).count();
|
|
4497
|
-
if (count > 0) return sel;
|
|
4498
|
-
}
|
|
4499
|
-
throw new Error("Could not auto-detect password field.");
|
|
4500
|
-
}
|
|
4501
|
-
const candidates = [
|
|
4502
|
-
'button[type="submit"]',
|
|
4503
|
-
'input[type="submit"]',
|
|
4504
|
-
"form button",
|
|
4505
|
-
'button:has-text("Log in")',
|
|
4506
|
-
'button:has-text("Sign in")',
|
|
4507
|
-
'button:has-text("Login")',
|
|
4508
|
-
'button:has-text("Submit")'
|
|
4509
|
-
];
|
|
4510
|
-
for (const sel of candidates) {
|
|
4511
|
-
const count = await page.locator(sel).count();
|
|
4512
|
-
if (count > 0) return sel;
|
|
4513
|
-
}
|
|
4514
|
-
throw new Error("Could not auto-detect submit button.");
|
|
4515
|
-
}
|
|
4516
|
-
var fs11, path9, AuthVault;
|
|
4517
|
-
var init_auth_vault = __esm({
|
|
4518
|
-
"src/auth-vault.ts"() {
|
|
4519
|
-
"use strict";
|
|
4520
|
-
fs11 = __toESM(require("fs"), 1);
|
|
4521
|
-
path9 = __toESM(require("path"), 1);
|
|
4522
|
-
init_constants();
|
|
4523
|
-
init_encryption();
|
|
4524
|
-
init_sanitize();
|
|
4525
|
-
AuthVault = class {
|
|
4526
|
-
authDir;
|
|
4527
|
-
encryptionKey;
|
|
4528
|
-
constructor(localDir) {
|
|
4529
|
-
this.authDir = path9.join(localDir, "auth");
|
|
4530
|
-
this.encryptionKey = resolveEncryptionKey(localDir);
|
|
4531
|
-
}
|
|
4532
|
-
save(name, url, username, password, selectors) {
|
|
4533
|
-
fs11.mkdirSync(this.authDir, { recursive: true });
|
|
4534
|
-
const { ciphertext, iv, authTag } = encrypt(password, this.encryptionKey);
|
|
4535
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4536
|
-
const credential = {
|
|
4537
|
-
name,
|
|
4538
|
-
url,
|
|
4539
|
-
username,
|
|
4540
|
-
encrypted: true,
|
|
4541
|
-
iv,
|
|
4542
|
-
authTag,
|
|
4543
|
-
data: ciphertext,
|
|
4544
|
-
usernameSelector: selectors?.username,
|
|
4545
|
-
passwordSelector: selectors?.password,
|
|
4546
|
-
submitSelector: selectors?.submit,
|
|
4547
|
-
createdAt: now,
|
|
4548
|
-
updatedAt: now
|
|
4549
|
-
};
|
|
4550
|
-
const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
4551
|
-
fs11.writeFileSync(filePath, JSON.stringify(credential, null, 2), { mode: 384 });
|
|
4552
|
-
}
|
|
4553
|
-
load(name) {
|
|
4554
|
-
const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
4555
|
-
if (!fs11.existsSync(filePath)) {
|
|
4556
|
-
throw new Error(`Credential "${name}" not found. Run "browse auth list" to see saved credentials.`);
|
|
4557
|
-
}
|
|
4558
|
-
return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
|
|
4559
|
-
}
|
|
4560
|
-
async login(name, bm) {
|
|
4561
|
-
const cred = this.load(name);
|
|
4562
|
-
const password = decrypt(cred.data, cred.iv, cred.authTag, this.encryptionKey);
|
|
4563
|
-
const page = bm.getPage();
|
|
4564
|
-
await page.goto(cred.url, {
|
|
4565
|
-
waitUntil: "domcontentloaded",
|
|
4566
|
-
timeout: DEFAULTS.COMMAND_TIMEOUT_MS
|
|
4567
|
-
});
|
|
4568
|
-
const userSel = cred.usernameSelector || await autoDetectSelector(page, "username");
|
|
4569
|
-
const passSel = cred.passwordSelector || await autoDetectSelector(page, "password");
|
|
4570
|
-
const submitSel = cred.submitSelector || await autoDetectSelector(page, "submit");
|
|
4571
|
-
await page.fill(userSel, cred.username, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
|
|
4572
|
-
await page.fill(passSel, password, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
|
|
4573
|
-
await page.click(submitSel, { timeout: DEFAULTS.ACTION_TIMEOUT_MS });
|
|
4574
|
-
await page.waitForLoadState("domcontentloaded").catch(() => {
|
|
4575
|
-
});
|
|
4576
|
-
return `Logged in as ${cred.username} at ${page.url()}`;
|
|
4577
|
-
}
|
|
4578
|
-
list() {
|
|
4579
|
-
if (!fs11.existsSync(this.authDir)) return [];
|
|
4580
|
-
const files = fs11.readdirSync(this.authDir).filter((f) => f.endsWith(".json"));
|
|
4581
|
-
return files.map((f) => {
|
|
4582
|
-
try {
|
|
4583
|
-
const data = JSON.parse(fs11.readFileSync(path9.join(this.authDir, f), "utf-8"));
|
|
4584
|
-
return {
|
|
4585
|
-
name: data.name,
|
|
4586
|
-
url: data.url,
|
|
4587
|
-
username: data.username,
|
|
4588
|
-
hasPassword: true,
|
|
4589
|
-
createdAt: data.createdAt
|
|
4590
|
-
};
|
|
4591
|
-
} catch {
|
|
4592
|
-
return null;
|
|
4593
|
-
}
|
|
4594
|
-
}).filter(Boolean);
|
|
4595
|
-
}
|
|
4596
|
-
delete(name) {
|
|
4597
|
-
const filePath = path9.join(this.authDir, `${sanitizeName(name)}.json`);
|
|
4598
|
-
if (!fs11.existsSync(filePath)) {
|
|
4599
|
-
throw new Error(`Credential "${name}" not found.`);
|
|
4600
|
-
}
|
|
4601
|
-
fs11.unlinkSync(filePath);
|
|
4602
|
-
}
|
|
4603
|
-
};
|
|
4604
|
-
}
|
|
4605
|
-
});
|
|
4606
|
-
|
|
4607
|
-
// src/cookie-import.ts
|
|
4608
|
-
var cookie_import_exports = {};
|
|
4609
|
-
__export(cookie_import_exports, {
|
|
4610
|
-
CookieImportError: () => CookieImportError,
|
|
4611
|
-
findInstalledBrowsers: () => findInstalledBrowsers,
|
|
4612
|
-
importCookies: () => importCookies,
|
|
4613
|
-
listDomains: () => listDomains
|
|
4614
|
-
});
|
|
4615
|
-
function findInstalledBrowsers() {
|
|
4616
|
-
const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
|
|
4617
|
-
return BROWSER_REGISTRY.filter((b) => {
|
|
4618
|
-
const dbPath = path10.join(appSupport, b.dataDir, "Default", "Cookies");
|
|
4619
|
-
try {
|
|
4620
|
-
return fs12.existsSync(dbPath);
|
|
4621
|
-
} catch {
|
|
4622
|
-
return false;
|
|
4623
|
-
}
|
|
4624
|
-
});
|
|
4625
|
-
}
|
|
4626
|
-
function listDomains(browserName, profile = "Default") {
|
|
4627
|
-
const browser2 = resolveBrowser(browserName);
|
|
4628
|
-
const dbPath = getCookieDbPath(browser2, profile);
|
|
4629
|
-
const db = openDb(dbPath, browser2.name);
|
|
4630
|
-
try {
|
|
4631
|
-
const now = chromiumNow();
|
|
4632
|
-
const rows = db.prepare(
|
|
4633
|
-
`SELECT host_key AS domain, COUNT(*) AS count
|
|
4634
|
-
FROM cookies
|
|
4635
|
-
WHERE has_expires = 0 OR expires_utc > ?
|
|
4636
|
-
GROUP BY host_key
|
|
4637
|
-
ORDER BY count DESC`
|
|
4638
|
-
).all(now);
|
|
4639
|
-
return { domains: rows, browser: browser2.name };
|
|
4640
|
-
} finally {
|
|
4641
|
-
db.close();
|
|
4642
|
-
}
|
|
4643
|
-
}
|
|
4644
|
-
async function importCookies(browserName, domains, profile = "Default") {
|
|
4645
|
-
if (domains.length === 0) return { cookies: [], count: 0, failed: 0, domainCounts: {} };
|
|
4646
|
-
const browser2 = resolveBrowser(browserName);
|
|
4647
|
-
const derivedKey = await getDerivedKey(browser2);
|
|
4648
|
-
const dbPath = getCookieDbPath(browser2, profile);
|
|
4649
|
-
const db = openDb(dbPath, browser2.name);
|
|
4650
|
-
try {
|
|
4651
|
-
const now = chromiumNow();
|
|
4652
|
-
const placeholders = domains.map(() => "?").join(",");
|
|
4653
|
-
const rows = db.prepare(
|
|
4654
|
-
`SELECT host_key, name, value, encrypted_value, path, expires_utc,
|
|
4655
|
-
is_secure, is_httponly, has_expires, samesite
|
|
4656
|
-
FROM cookies
|
|
4657
|
-
WHERE host_key IN (${placeholders})
|
|
4658
|
-
AND (has_expires = 0 OR expires_utc > ?)
|
|
4659
|
-
ORDER BY host_key, name`
|
|
4660
|
-
).all(...domains, now);
|
|
4661
|
-
const cookies = [];
|
|
4662
|
-
let failed = 0;
|
|
4663
|
-
const domainCounts = {};
|
|
4664
|
-
for (const row of rows) {
|
|
4665
|
-
try {
|
|
4666
|
-
const value = decryptCookieValue(row, derivedKey);
|
|
4667
|
-
const cookie = toPlaywrightCookie(row, value);
|
|
4668
|
-
cookies.push(cookie);
|
|
4669
|
-
domainCounts[row.host_key] = (domainCounts[row.host_key] || 0) + 1;
|
|
4670
|
-
} catch {
|
|
4671
|
-
failed++;
|
|
4672
|
-
}
|
|
4673
|
-
}
|
|
4674
|
-
return { cookies, count: cookies.length, failed, domainCounts };
|
|
4675
|
-
} finally {
|
|
4676
|
-
db.close();
|
|
4677
|
-
}
|
|
4678
|
-
}
|
|
4679
|
-
function resolveBrowser(nameOrAlias) {
|
|
4680
|
-
const needle = nameOrAlias.toLowerCase().trim();
|
|
4681
|
-
const found = BROWSER_REGISTRY.find(
|
|
4682
|
-
(b) => b.aliases.includes(needle) || b.name.toLowerCase() === needle
|
|
4683
|
-
);
|
|
4684
|
-
if (!found) {
|
|
4685
|
-
const supported = BROWSER_REGISTRY.flatMap((b) => b.aliases).join(", ");
|
|
4686
|
-
throw new CookieImportError(
|
|
4687
|
-
`Unknown browser '${nameOrAlias}'. Supported: ${supported}`,
|
|
4688
|
-
"unknown_browser"
|
|
4689
|
-
);
|
|
4690
|
-
}
|
|
4691
|
-
return found;
|
|
4692
|
-
}
|
|
4693
|
-
function validateProfile(profile) {
|
|
4694
|
-
if (/[/\\]|\.\./.test(profile) || /[\x00-\x1f]/.test(profile)) {
|
|
4695
|
-
throw new CookieImportError(
|
|
4696
|
-
`Invalid profile name: '${profile}'`,
|
|
4697
|
-
"bad_request"
|
|
4698
|
-
);
|
|
4699
|
-
}
|
|
4700
|
-
}
|
|
4701
|
-
function getCookieDbPath(browser2, profile) {
|
|
4702
|
-
validateProfile(profile);
|
|
4703
|
-
const appSupport = path10.join(os2.homedir(), "Library", "Application Support");
|
|
4704
|
-
const dbPath = path10.join(appSupport, browser2.dataDir, profile, "Cookies");
|
|
4705
|
-
if (!fs12.existsSync(dbPath)) {
|
|
4706
|
-
throw new CookieImportError(
|
|
4707
|
-
`${browser2.name} is not installed (no cookie database at ${dbPath})`,
|
|
4708
|
-
"not_installed"
|
|
4709
|
-
);
|
|
4710
|
-
}
|
|
4711
|
-
return dbPath;
|
|
4712
|
-
}
|
|
4713
|
-
function openDb(dbPath, browserName) {
|
|
4714
|
-
try {
|
|
4715
|
-
return new import_better_sqlite3.default(dbPath, { readonly: true });
|
|
4716
|
-
} catch (err) {
|
|
4717
|
-
if (err.message?.includes("SQLITE_BUSY") || err.message?.includes("database is locked")) {
|
|
4718
|
-
return openDbFromCopy(dbPath, browserName);
|
|
4719
|
-
}
|
|
4720
|
-
if (err.message?.includes("SQLITE_CORRUPT") || err.message?.includes("malformed")) {
|
|
4721
|
-
throw new CookieImportError(
|
|
4722
|
-
"Cookie database is corrupt",
|
|
4723
|
-
"db_corrupt"
|
|
4724
|
-
);
|
|
4725
|
-
}
|
|
4726
|
-
throw err;
|
|
4727
|
-
}
|
|
4728
|
-
}
|
|
4729
|
-
function openDbFromCopy(dbPath, browserName) {
|
|
4730
|
-
const tmpPath = `/tmp/browse-cookies-${browserName.toLowerCase()}-${crypto2.randomUUID()}.db`;
|
|
4731
|
-
try {
|
|
4732
|
-
fs12.copyFileSync(dbPath, tmpPath);
|
|
4733
|
-
const walPath = dbPath + "-wal";
|
|
4734
|
-
const shmPath = dbPath + "-shm";
|
|
4735
|
-
if (fs12.existsSync(walPath)) fs12.copyFileSync(walPath, tmpPath + "-wal");
|
|
4736
|
-
if (fs12.existsSync(shmPath)) fs12.copyFileSync(shmPath, tmpPath + "-shm");
|
|
4737
|
-
const db = new import_better_sqlite3.default(tmpPath, { readonly: true });
|
|
4738
|
-
const origClose = db.close.bind(db);
|
|
4739
|
-
db.close = (() => {
|
|
4740
|
-
origClose();
|
|
4741
|
-
try {
|
|
4742
|
-
fs12.unlinkSync(tmpPath);
|
|
4743
|
-
} catch {
|
|
4744
|
-
}
|
|
4745
|
-
try {
|
|
4746
|
-
fs12.unlinkSync(tmpPath + "-wal");
|
|
4747
|
-
} catch {
|
|
4748
|
-
}
|
|
4749
|
-
try {
|
|
4750
|
-
fs12.unlinkSync(tmpPath + "-shm");
|
|
4751
|
-
} catch {
|
|
4752
|
-
}
|
|
4753
|
-
});
|
|
4754
|
-
return db;
|
|
4755
|
-
} catch {
|
|
4756
|
-
try {
|
|
4757
|
-
fs12.unlinkSync(tmpPath);
|
|
4758
|
-
} catch {
|
|
4759
|
-
}
|
|
4760
|
-
throw new CookieImportError(
|
|
4761
|
-
`Cookie database is locked (${browserName} may be running). Try closing ${browserName} first.`,
|
|
4762
|
-
"db_locked",
|
|
4763
|
-
"retry"
|
|
4764
|
-
);
|
|
4765
|
-
}
|
|
4766
|
-
}
|
|
4767
|
-
async function getDerivedKey(browser2) {
|
|
4768
|
-
const cached = keyCache.get(browser2.keychainService);
|
|
4769
|
-
if (cached) return cached;
|
|
4770
|
-
const password = await getKeychainPassword(browser2.keychainService);
|
|
4771
|
-
const derived = crypto2.pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
|
|
4772
|
-
keyCache.set(browser2.keychainService, derived);
|
|
4773
|
-
return derived;
|
|
4774
|
-
}
|
|
4775
|
-
async function getKeychainPassword(service) {
|
|
4776
|
-
const proc = (0, import_child_process2.spawn)("security", ["find-generic-password", "-s", service, "-w"], {
|
|
4777
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
4778
|
-
});
|
|
4779
|
-
let stdout = "";
|
|
4780
|
-
let stderr = "";
|
|
4781
|
-
proc.stdout.setEncoding("utf8");
|
|
4782
|
-
proc.stderr.setEncoding("utf8");
|
|
4783
|
-
proc.stdout.on("data", (chunk) => {
|
|
4784
|
-
stdout += chunk;
|
|
4785
|
-
});
|
|
4786
|
-
proc.stderr.on("data", (chunk) => {
|
|
4787
|
-
stderr += chunk;
|
|
4788
|
-
});
|
|
4789
|
-
const exitPromise = new Promise(
|
|
4790
|
-
(resolve4) => proc.on("close", (code) => resolve4(code))
|
|
4791
|
-
);
|
|
4792
|
-
const timeout = new Promise(
|
|
4793
|
-
(_, reject) => setTimeout(() => {
|
|
4794
|
-
proc.kill();
|
|
4795
|
-
reject(new CookieImportError(
|
|
4796
|
-
`macOS is waiting for Keychain permission. Look for a dialog asking to allow access to "${service}".`,
|
|
4797
|
-
"keychain_timeout",
|
|
4798
|
-
"retry"
|
|
4799
|
-
));
|
|
4800
|
-
}, 1e4)
|
|
4801
|
-
);
|
|
4802
|
-
try {
|
|
4803
|
-
const exitCode = await Promise.race([exitPromise, timeout]);
|
|
4804
|
-
if (exitCode !== 0) {
|
|
4805
|
-
const errText = stderr.trim().toLowerCase();
|
|
4806
|
-
if (errText.includes("user canceled") || errText.includes("denied") || errText.includes("interaction not allowed")) {
|
|
4807
|
-
throw new CookieImportError(
|
|
4808
|
-
`Keychain access denied. Click Allow in the macOS dialog for "${service}".`,
|
|
4809
|
-
"keychain_denied",
|
|
4810
|
-
"retry"
|
|
4811
|
-
);
|
|
4812
|
-
}
|
|
4813
|
-
if (errText.includes("could not be found") || errText.includes("not found")) {
|
|
4814
|
-
throw new CookieImportError(
|
|
4815
|
-
`No Keychain entry for "${service}".`,
|
|
4816
|
-
"keychain_not_found"
|
|
4817
|
-
);
|
|
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;
|
|
4818
4759
|
}
|
|
4819
|
-
throw new CookieImportError(
|
|
4820
|
-
`Could not read Keychain: ${stderr.trim()}`,
|
|
4821
|
-
"keychain_error",
|
|
4822
|
-
"retry"
|
|
4823
|
-
);
|
|
4824
4760
|
}
|
|
4825
|
-
return stdout.trim();
|
|
4826
|
-
} catch (err) {
|
|
4827
|
-
if (err instanceof CookieImportError) throw err;
|
|
4828
|
-
throw new CookieImportError(
|
|
4829
|
-
`Could not read Keychain: ${err.message}`,
|
|
4830
|
-
"keychain_error",
|
|
4831
|
-
"retry"
|
|
4832
|
-
);
|
|
4833
4761
|
}
|
|
4762
|
+
return encodePNG({ width: w, height: h, data: diffData });
|
|
4834
4763
|
}
|
|
4835
|
-
function
|
|
4836
|
-
|
|
4837
|
-
const
|
|
4838
|
-
|
|
4839
|
-
const
|
|
4840
|
-
|
|
4841
|
-
|
|
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
|
+
}
|
|
4842
4788
|
}
|
|
4843
|
-
const
|
|
4844
|
-
const
|
|
4845
|
-
const
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
function toPlaywrightCookie(row, value) {
|
|
4851
|
-
return {
|
|
4852
|
-
name: row.name,
|
|
4853
|
-
value,
|
|
4854
|
-
domain: row.host_key,
|
|
4855
|
-
path: row.path || "/",
|
|
4856
|
-
expires: chromiumEpochToUnix(row.expires_utc, row.has_expires),
|
|
4857
|
-
secure: row.is_secure === 1,
|
|
4858
|
-
httpOnly: row.is_httponly === 1,
|
|
4859
|
-
sameSite: mapSameSite(row.samesite)
|
|
4860
|
-
};
|
|
4861
|
-
}
|
|
4862
|
-
function chromiumNow() {
|
|
4863
|
-
return BigInt(Date.now()) * 1000n + CHROMIUM_EPOCH_OFFSET;
|
|
4864
|
-
}
|
|
4865
|
-
function chromiumEpochToUnix(epoch, hasExpires) {
|
|
4866
|
-
if (hasExpires === 0 || epoch === 0 || epoch === 0n) return -1;
|
|
4867
|
-
const epochBig = BigInt(epoch);
|
|
4868
|
-
const unixMicro = epochBig - CHROMIUM_EPOCH_OFFSET;
|
|
4869
|
-
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;
|
|
4870
4796
|
}
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
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;
|
|
4881
4854
|
}
|
|
4855
|
+
throw new Error("Could not auto-detect submit button.");
|
|
4882
4856
|
}
|
|
4883
|
-
var
|
|
4884
|
-
var
|
|
4885
|
-
"src/
|
|
4857
|
+
var fs12, path10, AuthVault;
|
|
4858
|
+
var init_auth_vault = __esm({
|
|
4859
|
+
"src/auth-vault.ts"() {
|
|
4886
4860
|
"use strict";
|
|
4887
|
-
import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
4888
|
-
import_child_process2 = require("child_process");
|
|
4889
|
-
crypto2 = __toESM(require("crypto"), 1);
|
|
4890
4861
|
fs12 = __toESM(require("fs"), 1);
|
|
4891
4862
|
path10 = __toESM(require("path"), 1);
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
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);
|
|
4899
4943
|
}
|
|
4900
4944
|
};
|
|
4901
|
-
BROWSER_REGISTRY = [
|
|
4902
|
-
{ name: "Chrome", dataDir: "Google/Chrome/", keychainService: "Chrome Safe Storage", aliases: ["chrome", "google-chrome"] },
|
|
4903
|
-
{ name: "Arc", dataDir: "Arc/User Data/", keychainService: "Arc Safe Storage", aliases: ["arc"] },
|
|
4904
|
-
{ name: "Brave", dataDir: "BraveSoftware/Brave-Browser/", keychainService: "Brave Safe Storage", aliases: ["brave"] },
|
|
4905
|
-
{ name: "Edge", dataDir: "Microsoft Edge/", keychainService: "Microsoft Edge Safe Storage", aliases: ["edge"] }
|
|
4906
|
-
];
|
|
4907
|
-
keyCache = /* @__PURE__ */ new Map();
|
|
4908
|
-
CHROMIUM_EPOCH_OFFSET = 11644473600000000n;
|
|
4909
4945
|
}
|
|
4910
4946
|
});
|
|
4911
4947
|
|
|
@@ -8377,9 +8413,11 @@ var init_session_manager = __esm({
|
|
|
8377
8413
|
browser;
|
|
8378
8414
|
localDir;
|
|
8379
8415
|
encryptionKey;
|
|
8380
|
-
|
|
8416
|
+
reuseContext;
|
|
8417
|
+
constructor(browser2, localDir = "/tmp", reuseContext = false) {
|
|
8381
8418
|
this.browser = browser2;
|
|
8382
8419
|
this.localDir = localDir;
|
|
8420
|
+
this.reuseContext = reuseContext;
|
|
8383
8421
|
try {
|
|
8384
8422
|
this.encryptionKey = resolveEncryptionKey(localDir);
|
|
8385
8423
|
} catch {
|
|
@@ -8428,7 +8466,7 @@ var init_session_manager = __esm({
|
|
|
8428
8466
|
fs15.mkdirSync(outputDir, { recursive: true });
|
|
8429
8467
|
const buffers = new SessionBuffers();
|
|
8430
8468
|
const manager = new BrowserManager(buffers);
|
|
8431
|
-
await manager.launchWithBrowser(this.browser);
|
|
8469
|
+
await manager.launchWithBrowser(this.browser, this.reuseContext && this.sessions.size === 0);
|
|
8432
8470
|
let domainFilter = null;
|
|
8433
8471
|
if (allowedDomains) {
|
|
8434
8472
|
const domains = allowedDomains.split(",").map((d) => d.trim()).filter(Boolean);
|
|
@@ -8913,7 +8951,8 @@ async function start() {
|
|
|
8913
8951
|
shutdown();
|
|
8914
8952
|
});
|
|
8915
8953
|
}
|
|
8916
|
-
|
|
8954
|
+
const reuseContext = runtime.name === "chrome";
|
|
8955
|
+
sessionManager = new SessionManager(browser, LOCAL_DIR2, reuseContext);
|
|
8917
8956
|
}
|
|
8918
8957
|
const startTime = Date.now();
|
|
8919
8958
|
const server = nodeServe({
|
|
@@ -9873,6 +9912,9 @@ async function main() {
|
|
|
9873
9912
|
runtimeName = "chrome";
|
|
9874
9913
|
headed = true;
|
|
9875
9914
|
}
|
|
9915
|
+
if (runtimeName === "chrome") {
|
|
9916
|
+
headed = true;
|
|
9917
|
+
}
|
|
9876
9918
|
cliFlags.json = jsonMode;
|
|
9877
9919
|
cliFlags.contentBoundaries = contentBoundaries;
|
|
9878
9920
|
cliFlags.allowedDomains = allowedDomains || "";
|