@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/README.md +1 -1
- package/dist/browse.cjs +872 -861
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
package/dist/browse.cjs
CHANGED
|
@@ -62,318 +62,631 @@ 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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
284
|
-
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) {
|
|
285
309
|
return {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
365
|
+
keyCache = /* @__PURE__ */ new Map();
|
|
366
|
+
CHROMIUM_EPOCH_OFFSET = 11644473600000000n;
|
|
312
367
|
}
|
|
313
368
|
});
|
|
314
369
|
|
|
315
|
-
// src/
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
344
|
-
|
|
345
|
-
"
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
"
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
|
415
|
+
return null;
|
|
373
416
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
727
|
+
var fs5, path5, providers, ProviderVault;
|
|
415
728
|
var init_cloud_providers = __esm({
|
|
416
729
|
"src/cloud-providers.ts"() {
|
|
417
730
|
"use strict";
|
|
418
|
-
|
|
419
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
481
|
-
|
|
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 =
|
|
485
|
-
if (!
|
|
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(
|
|
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 (!
|
|
495
|
-
return
|
|
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
|
-
|
|
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 =
|
|
504
|
-
if (!
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
595
|
-
const hasClaude =
|
|
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 =
|
|
602
|
-
|
|
603
|
-
const skillSourceDir =
|
|
604
|
-
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)) {
|
|
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 =
|
|
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
|
-
|
|
616
|
-
console.log(`Skill installed: ${
|
|
617
|
-
}
|
|
618
|
-
const refsSourceDir =
|
|
619
|
-
if (
|
|
620
|
-
const refsDestDir =
|
|
621
|
-
|
|
622
|
-
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"));
|
|
623
936
|
for (const file of refFiles) {
|
|
624
|
-
|
|
625
|
-
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))}`);
|
|
626
939
|
}
|
|
627
940
|
}
|
|
628
|
-
const settingsPath =
|
|
941
|
+
const settingsPath = path6.join(dir, ".claude", "settings.json");
|
|
629
942
|
let settings = {};
|
|
630
|
-
if (
|
|
943
|
+
if (fs6.existsSync(settingsPath)) {
|
|
631
944
|
try {
|
|
632
|
-
settings = JSON.parse(
|
|
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
|
-
|
|
961
|
+
fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
649
962
|
if (added > 0) {
|
|
650
|
-
console.log(`Permissions: ${added} rules added to ${
|
|
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
|
|
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
|
-
|
|
661
|
-
|
|
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
|
|
1136
|
+
return path7.join(localDir, "profiles", sanitized);
|
|
824
1137
|
}
|
|
825
1138
|
function listProfiles(localDir) {
|
|
826
|
-
const profilesDir =
|
|
827
|
-
if (!
|
|
828
|
-
return
|
|
829
|
-
const dir =
|
|
830
|
-
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);
|
|
831
1144
|
let totalSize = 0;
|
|
832
1145
|
try {
|
|
833
|
-
const files =
|
|
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 +=
|
|
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 (!
|
|
855
|
-
|
|
1167
|
+
if (!fs7.existsSync(dir)) throw new Error(`Profile "${name}" not found`);
|
|
1168
|
+
fs7.rmSync(dir, { recursive: true, force: true });
|
|
856
1169
|
}
|
|
857
|
-
var
|
|
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
|
-
|
|
862
|
-
|
|
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 (!
|
|
2233
|
-
const code =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
2711
|
-
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"));
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4104
|
-
|
|
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 =
|
|
4111
|
-
if (!
|
|
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 =
|
|
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
|
|
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 =
|
|
4179
|
-
if (
|
|
4491
|
+
const statesDir = path8.join(localDir, "states");
|
|
4492
|
+
if (fs10.existsSync(statesDir)) {
|
|
4180
4493
|
try {
|
|
4181
|
-
const entries =
|
|
4494
|
+
const entries = fs10.readdirSync(statesDir);
|
|
4182
4495
|
for (const entry of entries) {
|
|
4183
4496
|
if (!entry.endsWith(".json")) continue;
|
|
4184
|
-
const filePath =
|
|
4497
|
+
const filePath = path8.join(statesDir, entry);
|
|
4185
4498
|
try {
|
|
4186
|
-
const stat =
|
|
4499
|
+
const stat = fs10.statSync(filePath);
|
|
4187
4500
|
if (!stat.isFile()) continue;
|
|
4188
4501
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
4189
|
-
|
|
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 =
|
|
4199
|
-
if (
|
|
4511
|
+
const sessionsDir = path8.join(localDir, "sessions");
|
|
4512
|
+
if (fs10.existsSync(sessionsDir)) {
|
|
4200
4513
|
try {
|
|
4201
|
-
const sessionDirs =
|
|
4514
|
+
const sessionDirs = fs10.readdirSync(sessionsDir);
|
|
4202
4515
|
for (const dir of sessionDirs) {
|
|
4203
|
-
const dirPath =
|
|
4516
|
+
const dirPath = path8.join(sessionsDir, dir);
|
|
4204
4517
|
try {
|
|
4205
|
-
const dirStat =
|
|
4518
|
+
const dirStat = fs10.statSync(dirPath);
|
|
4206
4519
|
if (!dirStat.isDirectory()) continue;
|
|
4207
4520
|
} catch (_) {
|
|
4208
4521
|
continue;
|
|
4209
4522
|
}
|
|
4210
|
-
const statePath =
|
|
4523
|
+
const statePath = path8.join(dirPath, STATE_FILENAME);
|
|
4211
4524
|
try {
|
|
4212
|
-
const stat =
|
|
4525
|
+
const stat = fs10.statSync(statePath);
|
|
4213
4526
|
if (now - stat.mtimeMs > maxAgeMs) {
|
|
4214
|
-
|
|
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
|
|
4538
|
+
var fs10, path8, STATE_FILENAME;
|
|
4226
4539
|
var init_session_persist = __esm({
|
|
4227
4540
|
"src/session-persist.ts"() {
|
|
4228
4541
|
"use strict";
|
|
4229
|
-
|
|
4230
|
-
|
|
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 =
|
|
4245
|
-
if (
|
|
4246
|
-
const parent =
|
|
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
|
|
4565
|
+
var fs11, path9, PolicyChecker;
|
|
4253
4566
|
var init_policy = __esm({
|
|
4254
4567
|
"src/policy.ts"() {
|
|
4255
4568
|
"use strict";
|
|
4256
|
-
|
|
4257
|
-
|
|
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 =
|
|
4589
|
+
const stat = fs11.statSync(this.filePath);
|
|
4277
4590
|
if (stat.mtimeMs === this.lastMtime) return;
|
|
4278
4591
|
this.lastMtime = stat.mtimeMs;
|
|
4279
|
-
const raw =
|
|
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
|
|
4864
|
-
|
|
4865
|
-
const
|
|
4866
|
-
|
|
4867
|
-
const
|
|
4868
|
-
|
|
4869
|
-
|
|
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
|
|
4872
|
-
const
|
|
4873
|
-
const
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
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
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
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
|
|
4912
|
-
var
|
|
4913
|
-
"src/
|
|
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
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
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 || "";
|