arn-browser 0.1.5 → 0.1.6
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/package.json
CHANGED
|
@@ -31,14 +31,14 @@ export async function deleteDirectoryWithRetries(targetPath, maxRetries = 5, ret
|
|
|
31
31
|
|
|
32
32
|
// If it's the last attempt, log error
|
|
33
33
|
if (isLastAttempt) {
|
|
34
|
-
console.error(
|
|
34
|
+
console.error(`░░░░░ Failed to delete directory after ${maxRetries} attempts: ${targetPath}`);
|
|
35
35
|
console.error(` Error: ${error.message}`);
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Log warning and wait
|
|
40
40
|
console.warn(
|
|
41
|
-
|
|
41
|
+
`░░░░░ Delete failed (Attempt ${attempt + 1}/${maxRetries}). File might be locked. Retrying in ${retryDelayMs / 1000
|
|
42
42
|
}s...`
|
|
43
43
|
);
|
|
44
44
|
await sleep(retryDelayMs);
|
|
@@ -150,23 +150,22 @@ export interface LaunchOptions {
|
|
|
150
150
|
// ========================================================================
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
|
-
* Path to a
|
|
153
|
+
* Path to a persistent profile.
|
|
154
154
|
* - If absolute path: Uses that path exactly (no prefix added).
|
|
155
|
-
* - If folder name: Creates inside `~/.arn-browser/
|
|
156
|
-
* - If null: Creates a temporary, random profile
|
|
157
|
-
*
|
|
158
|
-
* When combined with `cleanupMinutes`, named profiles can be auto-deleted
|
|
159
|
-
* after a period of inactivity (based on `_profile_meta.json`).
|
|
155
|
+
* - If folder name: Creates inside `~/.arn-browser/persistent/` AND adds a browser prefix (e.g., `brave_myProfile`).
|
|
156
|
+
* - If null: Creates a temporary, random profile in `~/.arn-browser/temp/`.
|
|
160
157
|
*/
|
|
161
158
|
profile_path?: string | null;
|
|
162
159
|
|
|
163
160
|
/**
|
|
164
|
-
* Auto-
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
161
|
+
* Auto-cleanup profiles after X minutes since last launch.
|
|
162
|
+
*
|
|
163
|
+
* **Persistent profiles:** Cleaned after X minutes since last launch. Default: 0 (never clean).
|
|
164
|
+
* **Temp profiles:** Default: 15 minutes if not specified.
|
|
165
|
+
*
|
|
166
|
+
* Each profile stores a `_profile_meta.json` with `created_at`, `last_launched_at`,
|
|
167
|
+
* and `cleanup_minutes`. The profile currently being launched is always skipped during cleanup.
|
|
168
|
+
*
|
|
170
169
|
* Default: 0
|
|
171
170
|
*/
|
|
172
171
|
cleanupMinutes?: number;
|
|
@@ -232,7 +231,7 @@ export interface LaunchOptions {
|
|
|
232
231
|
humanize_options?: HumanizeOptions;
|
|
233
232
|
|
|
234
233
|
// ========================================================================
|
|
235
|
-
//
|
|
234
|
+
// 6. LOGGING
|
|
236
235
|
// ========================================================================
|
|
237
236
|
|
|
238
237
|
/**
|
|
@@ -40,8 +40,16 @@ const detectedOs = osMap[process.platform] || "windows";
|
|
|
40
40
|
const MULTILOGIN_LAUNCHER_URL = "https://launcher.mlx.yt:45001";
|
|
41
41
|
const MULTILOGIN_FOLDER_ID = "bad9e7e1-cfab-4c8d-bd19-91aa82929711";
|
|
42
42
|
|
|
43
|
-
const BASE_PROFILE_DIR = path.join(os.homedir(), ".arn-browser"
|
|
44
|
-
|
|
43
|
+
const BASE_PROFILE_DIR = path.join(os.homedir(), ".arn-browser");
|
|
44
|
+
const PERSISTENT_DIR = path.join(BASE_PROFILE_DIR, "persistent");
|
|
45
|
+
const TEMP_DIR = path.join(BASE_PROFILE_DIR, "temp");
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(PERSISTENT_DIR)) fs.mkdirSync(PERSISTENT_DIR, { recursive: true });
|
|
48
|
+
if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
49
|
+
|
|
50
|
+
// Module-level log flags (set per-launch via launchBrowser options)
|
|
51
|
+
let _launchLogs = false;
|
|
52
|
+
let _cleanupLogs = false;
|
|
45
53
|
|
|
46
54
|
// ==========================================================================
|
|
47
55
|
// 2. HELPER FUNCTIONS
|
|
@@ -62,7 +70,7 @@ function resolveProfilePath(nameOrPath, browserName) {
|
|
|
62
70
|
else if (prefix.includes("camoufox")) prefix = "camoufox";
|
|
63
71
|
|
|
64
72
|
const folderName = `${prefix}_${nameOrPath}`;
|
|
65
|
-
return path.join(
|
|
73
|
+
return path.join(PERSISTENT_DIR, folderName);
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/**
|
|
@@ -96,7 +104,7 @@ function getBinaryPath(browserName) {
|
|
|
96
104
|
|
|
97
105
|
if (!binaryPath || !fs.existsSync(binaryPath)) {
|
|
98
106
|
throw new Error(
|
|
99
|
-
|
|
107
|
+
`░░░░░ [LaunchBrowser] Binary not found for ${browserName} at: ${binaryPath}\n` +
|
|
100
108
|
` Linux checked: ~/.cache/brave/brave AND ~/Downloads/brave/brave`
|
|
101
109
|
);
|
|
102
110
|
}
|
|
@@ -104,61 +112,92 @@ function getBinaryPath(browserName) {
|
|
|
104
112
|
return binaryPath;
|
|
105
113
|
}
|
|
106
114
|
|
|
115
|
+
const PROFILE_META_FILE = "_profile_meta.json";
|
|
116
|
+
|
|
107
117
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
118
|
+
* Writes or updates the profile metadata file in a profile directory.
|
|
119
|
+
* On first creation: sets all fields.
|
|
120
|
+
* On subsequent launches: only updates `last_launched_at`.
|
|
110
121
|
*/
|
|
111
|
-
function writeProfileMeta(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
function writeProfileMeta(dirPath, type, cleanupMinutes) {
|
|
123
|
+
try {
|
|
124
|
+
const metaPath = path.join(dirPath, PROFILE_META_FILE);
|
|
125
|
+
const now = new Date().toISOString();
|
|
126
|
+
|
|
127
|
+
if (fs.existsSync(metaPath)) {
|
|
128
|
+
// Update last_launched_at only
|
|
129
|
+
const existing = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
130
|
+
existing.last_launched_at = now;
|
|
131
|
+
fs.writeFileSync(metaPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
132
|
+
} else {
|
|
133
|
+
// Create new meta
|
|
134
|
+
const meta = {
|
|
135
|
+
type,
|
|
136
|
+
created_at: now,
|
|
137
|
+
last_launched_at: now,
|
|
138
|
+
cleanup_minutes: cleanupMinutes,
|
|
139
|
+
};
|
|
140
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// Non-critical — don't break the launch
|
|
120
144
|
}
|
|
121
|
-
|
|
122
|
-
meta.last_used_at = new Date().toISOString();
|
|
123
|
-
meta.delete_after_minutes = deleteAfterMinutes || null;
|
|
124
|
-
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
148
|
+
* Scans both persistent and temp profile directories.
|
|
149
|
+
* Deletes profiles whose `_profile_meta.json` indicates they are expired.
|
|
150
|
+
* Skips the profile currently being launched (skipPath).
|
|
130
151
|
*/
|
|
131
|
-
function cleanUpProfiles(
|
|
132
|
-
if (!fs.existsSync(BASE_PROFILE_DIR)) return;
|
|
133
|
-
|
|
152
|
+
function cleanUpProfiles(skipPath) {
|
|
134
153
|
const now = Date.now();
|
|
135
|
-
try {
|
|
136
|
-
const entries = fs.readdirSync(BASE_PROFILE_DIR);
|
|
137
|
-
for (const entry of entries) {
|
|
138
|
-
const profileDir = path.join(BASE_PROFILE_DIR, entry);
|
|
139
|
-
try {
|
|
140
|
-
if (!fs.statSync(profileDir).isDirectory()) continue;
|
|
141
|
-
|
|
142
|
-
const metaPath = path.join(profileDir, "_profile_meta.json");
|
|
143
|
-
if (!fs.existsSync(metaPath)) continue; // No metadata = skip
|
|
144
154
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
function scanDir(dir) {
|
|
156
|
+
if (!fs.existsSync(dir)) return;
|
|
157
|
+
try {
|
|
158
|
+
const folders = fs.readdirSync(dir);
|
|
159
|
+
for (const folder of folders) {
|
|
160
|
+
const folderPath = path.join(dir, folder);
|
|
161
|
+
try {
|
|
162
|
+
const stats = fs.statSync(folderPath);
|
|
163
|
+
if (!stats.isDirectory()) continue;
|
|
164
|
+
|
|
165
|
+
// Skip the profile being launched right now
|
|
166
|
+
if (skipPath && path.resolve(folderPath) === path.resolve(skipPath)) continue;
|
|
167
|
+
|
|
168
|
+
const metaPath = path.join(folderPath, PROFILE_META_FILE);
|
|
169
|
+
if (!fs.existsSync(metaPath)) {
|
|
170
|
+
// Legacy temp profile without meta — use mtime, default 15 min
|
|
171
|
+
if (dir === TEMP_DIR && now - stats.mtimeMs > 15 * 60 * 1000) {
|
|
172
|
+
if (_cleanupLogs) console.log(`░░░░░ [Cleanup] Deleting legacy temp profile: ${folder}`);
|
|
173
|
+
deleteDirectoryWithRetries(folderPath);
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
179
|
+
const cleanupMs = meta.cleanup_minutes;
|
|
180
|
+
|
|
181
|
+
// Skip if cleanup is disabled (null or 0)
|
|
182
|
+
if (!cleanupMs || cleanupMs <= 0) continue;
|
|
183
|
+
|
|
184
|
+
const lastLaunch = new Date(meta.last_launched_at).getTime();
|
|
185
|
+
if (now - lastLaunch > cleanupMs * 60 * 1000) {
|
|
186
|
+
if (_cleanupLogs) console.log(`░░░░░ [Cleanup] Deleting expired ${meta.type} profile: ${folder} (last launched ${Math.round((now - lastLaunch) / 60000)} mins ago)`);
|
|
187
|
+
deleteDirectoryWithRetries(folderPath);
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// Ignore per-folder errors (locks, permission, corrupted meta)
|
|
154
191
|
}
|
|
155
|
-
} catch (e) {
|
|
156
|
-
// Ignore errors for individual profiles
|
|
157
192
|
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error("Cleanup scan error:", err);
|
|
158
195
|
}
|
|
159
|
-
} catch (err) {
|
|
160
|
-
console.error("Cleanup Error:", err);
|
|
161
196
|
}
|
|
197
|
+
|
|
198
|
+
if (_cleanupLogs) console.log("░░░░░ [Cleanup] Scanning for expired profiles...");
|
|
199
|
+
scanDir(PERSISTENT_DIR);
|
|
200
|
+
scanDir(TEMP_DIR);
|
|
162
201
|
}
|
|
163
202
|
|
|
164
203
|
/**
|
|
@@ -230,21 +269,23 @@ export async function launchBrowser({
|
|
|
230
269
|
try {
|
|
231
270
|
if (custom_profile_path) throw new Error("Please use profile_path");
|
|
232
271
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const activePath = resolvedPath || path.join(BASE_PROFILE_DIR, crypto.randomUUID());
|
|
272
|
+
// Set module-level log flags
|
|
273
|
+
_launchLogs = launch_logs;
|
|
274
|
+
_cleanupLogs = cleanup_logs;
|
|
237
275
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
// (safety net — if process crashes before closeBrowser, cleanup catches it)
|
|
241
|
-
writeProfileMeta(activePath, isNamed ? cleanupMinutes : (cleanupMinutes || 15));
|
|
276
|
+
// Resolve path using the browser type to ensure prefixes
|
|
277
|
+
const fullPath = resolveProfilePath(profile_path, which_browser);
|
|
242
278
|
|
|
243
|
-
//
|
|
244
|
-
|
|
279
|
+
// Calculate effective cleanup minutes:
|
|
280
|
+
// Persistent: user value if > 0, otherwise null (never clean)
|
|
281
|
+
// Temp: user value if > 0, otherwise 15 min default
|
|
282
|
+
const isPersistent = !!profile_path;
|
|
283
|
+
const effectiveCleanupMinutes = cleanupMinutes > 0
|
|
284
|
+
? cleanupMinutes
|
|
285
|
+
: (isPersistent ? null : 15);
|
|
245
286
|
|
|
246
|
-
//
|
|
247
|
-
|
|
287
|
+
// Run cleanup scan — skips the profile being launched
|
|
288
|
+
cleanUpProfiles(fullPath);
|
|
248
289
|
|
|
249
290
|
let browserInstance;
|
|
250
291
|
|
|
@@ -259,53 +300,45 @@ export async function launchBrowser({
|
|
|
259
300
|
case "chromium":
|
|
260
301
|
case "chrome":
|
|
261
302
|
browserInstance = await chromiumLauncher({
|
|
262
|
-
|
|
263
|
-
isPersistent: isNamed,
|
|
264
|
-
dirToDelete,
|
|
265
|
-
launch_logs,
|
|
303
|
+
profilePath: fullPath,
|
|
266
304
|
proxy,
|
|
267
305
|
timezoneId,
|
|
268
306
|
CapSolver,
|
|
269
307
|
humanize_options: effectiveHumanizeOptions,
|
|
270
308
|
spoof_fingerprint,
|
|
309
|
+
cleanupMinutes: effectiveCleanupMinutes,
|
|
271
310
|
});
|
|
272
311
|
break;
|
|
273
312
|
case "firefox":
|
|
274
313
|
browserInstance = await firefoxLauncher({
|
|
275
|
-
|
|
276
|
-
isPersistent: isNamed,
|
|
277
|
-
dirToDelete,
|
|
278
|
-
launch_logs,
|
|
314
|
+
profilePath: fullPath,
|
|
279
315
|
proxy,
|
|
280
316
|
timezoneId,
|
|
281
317
|
humanize_options: effectiveHumanizeOptions,
|
|
282
318
|
spoof_fingerprint,
|
|
319
|
+
cleanupMinutes: effectiveCleanupMinutes,
|
|
283
320
|
});
|
|
284
321
|
break;
|
|
285
322
|
case "brave":
|
|
286
323
|
case "braveLauncher":
|
|
287
324
|
browserInstance = await braveLauncher({
|
|
288
|
-
|
|
289
|
-
isPersistent: isNamed,
|
|
290
|
-
dirToDelete,
|
|
291
|
-
launch_logs,
|
|
325
|
+
profilePath: fullPath,
|
|
292
326
|
proxy,
|
|
293
327
|
timezoneId,
|
|
294
328
|
CapSolver,
|
|
295
329
|
humanize_options: effectiveHumanizeOptions,
|
|
296
330
|
spoof_fingerprint,
|
|
331
|
+
cleanupMinutes: effectiveCleanupMinutes,
|
|
297
332
|
});
|
|
298
333
|
break;
|
|
299
334
|
case "camoufox":
|
|
300
335
|
// Camoufox already has its own humanize in camoufox_options
|
|
301
336
|
browserInstance = await camoufoxLauncher({
|
|
302
|
-
|
|
303
|
-
isPersistent: isNamed,
|
|
304
|
-
dirToDelete,
|
|
305
|
-
launch_logs,
|
|
337
|
+
profilePath: fullPath,
|
|
306
338
|
proxy,
|
|
307
339
|
timezoneId,
|
|
308
340
|
camoufox_options,
|
|
341
|
+
cleanupMinutes: effectiveCleanupMinutes,
|
|
309
342
|
// Spoof Fingerprint not needed for Camoufox
|
|
310
343
|
});
|
|
311
344
|
break;
|
|
@@ -314,7 +347,6 @@ export async function launchBrowser({
|
|
|
314
347
|
proxy,
|
|
315
348
|
multilogin_options,
|
|
316
349
|
humanize_options: effectiveHumanizeOptions,
|
|
317
|
-
launch_logs,
|
|
318
350
|
// Spoof Fingerprint not needed for Multilogin
|
|
319
351
|
});
|
|
320
352
|
break;
|
|
@@ -324,7 +356,7 @@ export async function launchBrowser({
|
|
|
324
356
|
|
|
325
357
|
return browserInstance;
|
|
326
358
|
} catch (error) {
|
|
327
|
-
console.error("
|
|
359
|
+
console.error("░░░░░ [LaunchBrowser] Critical Error:", error.message || error);
|
|
328
360
|
return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
|
|
329
361
|
}
|
|
330
362
|
}
|
|
@@ -332,9 +364,14 @@ export async function launchBrowser({
|
|
|
332
364
|
// ==========================================================================
|
|
333
365
|
// 4. ENGINE: CHROMIUM
|
|
334
366
|
// ==========================================================================
|
|
335
|
-
async function chromiumLauncher({
|
|
367
|
+
async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, humanize_options, spoof_fingerprint, cleanupMinutes }) {
|
|
368
|
+
const isPersistent = !!profilePath;
|
|
336
369
|
|
|
337
|
-
|
|
370
|
+
// 1. Determine Path (Temp needs it for fingerprint storage, Persistent needs it for data)
|
|
371
|
+
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
372
|
+
if (!fs.existsSync(activePath)) fs.mkdirSync(activePath, { recursive: true });
|
|
373
|
+
writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
|
|
374
|
+
if (_launchLogs) console.log(`░░░░░ Starting Chromium [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
|
|
338
375
|
|
|
339
376
|
// 2. Define Args
|
|
340
377
|
const args = [
|
|
@@ -414,8 +451,8 @@ async function chromiumLauncher({ activePath, isPersistent, dirToDelete, launch_
|
|
|
414
451
|
|
|
415
452
|
const page = context.pages()[0] || (await context.newPage());
|
|
416
453
|
|
|
417
|
-
// Pass
|
|
418
|
-
return createBrowserController(browser, context, page,
|
|
454
|
+
// Pass 'activePath' so the temp folder (containing fingerprint.json) gets deleted on close
|
|
455
|
+
return createBrowserController(browser, context, page, activePath, humanize_options);
|
|
419
456
|
} catch (err) {
|
|
420
457
|
console.error("Chromium Temp Launch Error:", err);
|
|
421
458
|
throw err;
|
|
@@ -444,7 +481,7 @@ async function chromiumLauncher({ activePath, isPersistent, dirToDelete, launch_
|
|
|
444
481
|
|
|
445
482
|
const page = context.pages()[0];
|
|
446
483
|
|
|
447
|
-
return createBrowserController(context, context, page,
|
|
484
|
+
return createBrowserController(context, context, page, null, humanize_options);
|
|
448
485
|
} catch (err) {
|
|
449
486
|
console.error("Chromium Persistent Launch Error:", err);
|
|
450
487
|
throw err;
|
|
@@ -455,9 +492,14 @@ async function chromiumLauncher({ activePath, isPersistent, dirToDelete, launch_
|
|
|
455
492
|
// ==========================================================================
|
|
456
493
|
// 5. ENGINE: FIREFOX
|
|
457
494
|
// ==========================================================================
|
|
458
|
-
async function firefoxLauncher({
|
|
495
|
+
async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_options, spoof_fingerprint, cleanupMinutes }) {
|
|
496
|
+
const isPersistent = !!profilePath;
|
|
459
497
|
|
|
460
|
-
|
|
498
|
+
// 1. Determine Path
|
|
499
|
+
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
500
|
+
if (!fs.existsSync(activePath)) fs.mkdirSync(activePath, { recursive: true });
|
|
501
|
+
writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
|
|
502
|
+
if (_launchLogs) console.log(`░░░░░ Starting Firefox [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
|
|
461
503
|
|
|
462
504
|
const proxyObj = formatProxy(proxy);
|
|
463
505
|
const tz = timezoneId || undefined;
|
|
@@ -506,8 +548,8 @@ async function firefoxLauncher({ activePath, isPersistent, dirToDelete, launch_l
|
|
|
506
548
|
|
|
507
549
|
const page = context.pages()[0] || (await context.newPage());
|
|
508
550
|
|
|
509
|
-
// Pass
|
|
510
|
-
return createBrowserController(browser, context, page,
|
|
551
|
+
// Pass 'activePath' to delete temp folder later
|
|
552
|
+
return createBrowserController(browser, context, page, activePath, humanize_options);
|
|
511
553
|
} catch (err) {
|
|
512
554
|
console.error("Firefox Temp Launch Error:", err);
|
|
513
555
|
throw err;
|
|
@@ -535,7 +577,7 @@ async function firefoxLauncher({ activePath, isPersistent, dirToDelete, launch_l
|
|
|
535
577
|
|
|
536
578
|
const page = context.pages()[0];
|
|
537
579
|
|
|
538
|
-
return createBrowserController(context, context, page,
|
|
580
|
+
return createBrowserController(context, context, page, null, humanize_options);
|
|
539
581
|
} catch (err) {
|
|
540
582
|
console.error("Firefox Persistent Launch Error:", err);
|
|
541
583
|
throw err;
|
|
@@ -546,10 +588,13 @@ async function firefoxLauncher({ activePath, isPersistent, dirToDelete, launch_l
|
|
|
546
588
|
// ==========================================================================
|
|
547
589
|
// 6. ENGINE: BRAVE
|
|
548
590
|
// ==========================================================================
|
|
549
|
-
async function braveLauncher({
|
|
591
|
+
async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humanize_options, spoof_fingerprint, cleanupMinutes }) {
|
|
592
|
+
const isPersistent = !!profilePath;
|
|
593
|
+
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
550
594
|
|
|
551
|
-
if (
|
|
595
|
+
if (_launchLogs) console.log(`░░░░░ Starting Brave: ${activePath}`);
|
|
552
596
|
fs.mkdirSync(activePath, { recursive: true });
|
|
597
|
+
writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
|
|
553
598
|
|
|
554
599
|
// ======================================================
|
|
555
600
|
// Persist spoof_fingerprint setting for persistent profiles
|
|
@@ -563,12 +608,12 @@ async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_log
|
|
|
563
608
|
// Load saved settings from first launch
|
|
564
609
|
const savedSettings = JSON.parse(fs.readFileSync(settingsFilePath, "utf-8"));
|
|
565
610
|
effectiveSpoofFingerprint = savedSettings.spoof_fingerprint;
|
|
566
|
-
if (
|
|
611
|
+
if (_launchLogs) console.log(`░░░░░ Using saved spoof_fingerprint: ${JSON.stringify(effectiveSpoofFingerprint)} (ignoring current: ${JSON.stringify(spoof_fingerprint)})`);
|
|
567
612
|
} else {
|
|
568
613
|
// First launch - save the current setting
|
|
569
614
|
const settings = { spoof_fingerprint: spoof_fingerprint };
|
|
570
615
|
fs.writeFileSync(settingsFilePath, JSON.stringify(settings, null, 2), "utf-8");
|
|
571
|
-
if (
|
|
616
|
+
if (_launchLogs) console.log(`░░░░░ Saved spoof_fingerprint setting: ${JSON.stringify(spoof_fingerprint)}`);
|
|
572
617
|
}
|
|
573
618
|
}
|
|
574
619
|
|
|
@@ -592,7 +637,7 @@ async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_log
|
|
|
592
637
|
|
|
593
638
|
if (isPersistent) {
|
|
594
639
|
fs.writeFileSync(fingerprintFilePath, JSON.stringify(fingerprintData, null, 2), "utf-8");
|
|
595
|
-
if (
|
|
640
|
+
if (_launchLogs) console.log("░░░░░ Generated and saved new fingerprint for Brave");
|
|
596
641
|
}
|
|
597
642
|
}
|
|
598
643
|
|
|
@@ -616,7 +661,7 @@ async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_log
|
|
|
616
661
|
prefs.brave.sidebar.sidebar_show_option = 3;
|
|
617
662
|
fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
|
|
618
663
|
} catch (e) {
|
|
619
|
-
|
|
664
|
+
console.warn("░░░░░ Could not modify Brave preferences:", e.message);
|
|
620
665
|
}
|
|
621
666
|
|
|
622
667
|
const args = [
|
|
@@ -703,11 +748,11 @@ async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_log
|
|
|
703
748
|
|
|
704
749
|
|
|
705
750
|
// ======================================================
|
|
706
|
-
// FIX: Close Extra Tabs (Brave Dashboard / Restore)
|
|
751
|
+
// ░░░░░ FIX: Close Extra Tabs (Brave Dashboard / Restore)
|
|
707
752
|
// ======================================================
|
|
708
753
|
const pages = context.pages();
|
|
709
754
|
if (pages.length > 1) {
|
|
710
|
-
if (
|
|
755
|
+
if (_launchLogs) console.log(`░░░░░ Found ${pages.length} tabs open in Brave. Closing extras...`);
|
|
711
756
|
// Close all tabs except the last one (which is usually the fresh active one)
|
|
712
757
|
for (let i = 0; i < pages.length - 1; i++) {
|
|
713
758
|
await pages[i].close();
|
|
@@ -715,17 +760,21 @@ async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_log
|
|
|
715
760
|
}
|
|
716
761
|
const page = context.pages()[0];
|
|
717
762
|
|
|
763
|
+
const dirToDelete = isPersistent ? null : activePath;
|
|
718
764
|
// Return the remaining open page (context.pages() might change, so we grab index 0 after cleanup)
|
|
719
|
-
return createBrowserController(context, context, page, dirToDelete, humanize_options
|
|
765
|
+
return createBrowserController(context, context, page, dirToDelete, humanize_options);
|
|
720
766
|
}
|
|
721
767
|
|
|
722
768
|
// ==========================================================================
|
|
723
769
|
// 7. ENGINE: CAMOUFOX
|
|
724
770
|
// ==========================================================================
|
|
725
|
-
async function camoufoxLauncher({
|
|
771
|
+
async function camoufoxLauncher({ profilePath, proxy, timezoneId, camoufox_options = {}, cleanupMinutes }) {
|
|
772
|
+
const isPersistent = !!profilePath;
|
|
773
|
+
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
726
774
|
|
|
727
|
-
if (
|
|
775
|
+
if (_launchLogs) console.log(`░░░░░ Starting camoufoxJs: ${activePath}`);
|
|
728
776
|
fs.mkdirSync(activePath, { recursive: true });
|
|
777
|
+
writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
|
|
729
778
|
|
|
730
779
|
const proxyObj = formatProxy(proxy);
|
|
731
780
|
const tz = timezoneId || undefined;
|
|
@@ -790,7 +839,7 @@ async function camoufoxLauncher({ activePath, isPersistent, dirToDelete, launch_
|
|
|
790
839
|
// Save persistent config
|
|
791
840
|
if (isPersistent) {
|
|
792
841
|
fs.writeFileSync(fingerprintFilePath, JSON.stringify(launchConfig, null, 2), "utf-8");
|
|
793
|
-
if (
|
|
842
|
+
if (_launchLogs) console.log("░░░░░ Generated and saved new launch config for Camoufox");
|
|
794
843
|
}
|
|
795
844
|
}
|
|
796
845
|
|
|
@@ -808,13 +857,14 @@ async function camoufoxLauncher({ activePath, isPersistent, dirToDelete, launch_
|
|
|
808
857
|
// Removed viewport: null, handled by Camoufox args
|
|
809
858
|
});
|
|
810
859
|
|
|
811
|
-
|
|
860
|
+
const dirToDelete = isPersistent ? null : activePath;
|
|
861
|
+
return createBrowserController(context, context, context.pages()[0], dirToDelete);
|
|
812
862
|
}
|
|
813
863
|
|
|
814
864
|
// ==========================================================================
|
|
815
865
|
// 8. ENGINE: MULTILOGIN
|
|
816
866
|
// ==========================================================================
|
|
817
|
-
async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_options = null
|
|
867
|
+
async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_options = null }) {
|
|
818
868
|
// Destructure defaults from multilogin_options
|
|
819
869
|
const {
|
|
820
870
|
profileId = null,
|
|
@@ -826,7 +876,7 @@ async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_opt
|
|
|
826
876
|
} = multilogin_options;
|
|
827
877
|
|
|
828
878
|
if (profileId) {
|
|
829
|
-
return await launchExistingMultiloginProfile(profileId, humanize_options
|
|
879
|
+
return await launchExistingMultiloginProfile(profileId, humanize_options);
|
|
830
880
|
} else {
|
|
831
881
|
return await launchQuickMultiloginProfile({
|
|
832
882
|
os_type,
|
|
@@ -835,12 +885,11 @@ async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_opt
|
|
|
835
885
|
media_masking,
|
|
836
886
|
audio_masking,
|
|
837
887
|
humanize_options,
|
|
838
|
-
launch_logs,
|
|
839
888
|
});
|
|
840
889
|
}
|
|
841
890
|
}
|
|
842
891
|
|
|
843
|
-
async function launchExistingMultiloginProfile(profileId, humanize_options = null
|
|
892
|
+
async function launchExistingMultiloginProfile(profileId, humanize_options = null) {
|
|
844
893
|
const startUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v2/profile/f/${MULTILOGIN_FOLDER_ID}/p/${profileId}/start?automation_type=playwright&headless_mode=false`;
|
|
845
894
|
let browser, context, page;
|
|
846
895
|
|
|
@@ -854,7 +903,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
|
|
|
854
903
|
if (response.status === 400) {
|
|
855
904
|
const errorJson = await response.json().catch(() => null);
|
|
856
905
|
if (errorJson?.status?.error_code === "PROFILE_ALREADY_RUNNING") {
|
|
857
|
-
if (
|
|
906
|
+
if (_launchLogs) console.log(`░░░░░ Profile ${profileId} is running. Restarting...`);
|
|
858
907
|
await stopMultiloginProfile(profileId);
|
|
859
908
|
await sleep(5000);
|
|
860
909
|
response = await fetch(startUrl, {
|
|
@@ -874,7 +923,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
|
|
|
874
923
|
|
|
875
924
|
const data = await response.json();
|
|
876
925
|
const port = data.data.port;
|
|
877
|
-
if (
|
|
926
|
+
if (_launchLogs) console.log(`░░░░░ Multilogin Profile ${profileId} started on port ${port}`);
|
|
878
927
|
|
|
879
928
|
browser = await chromium.connectOverCDP(`http://localhost:${port}`);
|
|
880
929
|
context = browser.contexts()[0];
|
|
@@ -900,7 +949,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
|
|
|
900
949
|
}
|
|
901
950
|
}
|
|
902
951
|
|
|
903
|
-
async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, humanize_options = null
|
|
952
|
+
async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, humanize_options = null }) {
|
|
904
953
|
const createUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v3/profile/quick`;
|
|
905
954
|
let browser, context, page, profileId;
|
|
906
955
|
|
|
@@ -948,7 +997,7 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
|
|
|
948
997
|
|
|
949
998
|
profileId = json.data.id;
|
|
950
999
|
const port = json.data.port;
|
|
951
|
-
if (
|
|
1000
|
+
if (_launchLogs) console.log(`░░░░░ Quick Profile Created: ${profileId} on port ${port}`);
|
|
952
1001
|
|
|
953
1002
|
browser = await chromium.connectOverCDP(`http://localhost:${port}`);
|
|
954
1003
|
context = browser.contexts()[0];
|
|
@@ -988,7 +1037,7 @@ async function stopMultiloginProfile(profileId) {
|
|
|
988
1037
|
}
|
|
989
1038
|
throw new Error(txt);
|
|
990
1039
|
}
|
|
991
|
-
if (
|
|
1040
|
+
if (_launchLogs) console.log(`░░░░░ Profile ${profileId} stopped.`);
|
|
992
1041
|
return { error: null };
|
|
993
1042
|
} catch (err) {
|
|
994
1043
|
console.error(`Error stopping ${profileId}:`, err);
|
|
@@ -1034,7 +1083,7 @@ function formatProxy(proxy) {
|
|
|
1034
1083
|
return p;
|
|
1035
1084
|
}
|
|
1036
1085
|
|
|
1037
|
-
function createBrowserController(browser, context, page, dirToDelete = null, humanize_options = null
|
|
1086
|
+
function createBrowserController(browser, context, page, dirToDelete = null, humanize_options = null) {
|
|
1038
1087
|
// Apply human cursor if options provided
|
|
1039
1088
|
let humanPage = page;
|
|
1040
1089
|
if (humanize_options && page) {
|
|
@@ -1043,7 +1092,7 @@ function createBrowserController(browser, context, page, dirToDelete = null, hum
|
|
|
1043
1092
|
|
|
1044
1093
|
const closeBrowser = async () => {
|
|
1045
1094
|
try {
|
|
1046
|
-
if (
|
|
1095
|
+
if (_launchLogs) console.log("░░░░░ Closing browser session...");
|
|
1047
1096
|
if (context) await context.close();
|
|
1048
1097
|
if (browser && typeof browser.close === "function" && browser !== context) {
|
|
1049
1098
|
try {
|
|
@@ -1051,8 +1100,12 @@ function createBrowserController(browser, context, page, dirToDelete = null, hum
|
|
|
1051
1100
|
} catch (e) { }
|
|
1052
1101
|
}
|
|
1053
1102
|
if (dirToDelete) {
|
|
1054
|
-
if (
|
|
1055
|
-
|
|
1103
|
+
if (dirToDelete.includes("persistent")) {
|
|
1104
|
+
console.warn(`░░░░░ Safety Block: Attempted to delete persistent profile: ${dirToDelete}`);
|
|
1105
|
+
} else {
|
|
1106
|
+
if (_launchLogs) console.log(`░░░░░ Deleting temp profile: ${dirToDelete}`);
|
|
1107
|
+
await deleteDirectoryWithRetries(dirToDelete);
|
|
1108
|
+
}
|
|
1056
1109
|
}
|
|
1057
1110
|
return true;
|
|
1058
1111
|
} catch (err) {
|
|
@@ -239,6 +239,9 @@ export async function startProxyServer({
|
|
|
239
239
|
const connectionMap = {}; // Maps connectionId -> { type: "..." }
|
|
240
240
|
let serverRunning = false;
|
|
241
241
|
|
|
242
|
+
// Track all open sockets so we can force-destroy them on close
|
|
243
|
+
const activeSockets = new Set();
|
|
244
|
+
|
|
242
245
|
// 6. Server
|
|
243
246
|
const server = new ProxyChain.Server({
|
|
244
247
|
port: selectedPort,
|
|
@@ -327,6 +330,23 @@ export async function startProxyServer({
|
|
|
327
330
|
try {
|
|
328
331
|
await server.listen();
|
|
329
332
|
serverRunning = true;
|
|
333
|
+
|
|
334
|
+
// Track sockets so closeServer() can destroy them all
|
|
335
|
+
server.server.on('connection', (socket) => {
|
|
336
|
+
activeSockets.add(socket);
|
|
337
|
+
socket.once('close', () => activeSockets.delete(socket));
|
|
338
|
+
|
|
339
|
+
// Intercept pipe to catch the upstream target sockets created by proxy-chain
|
|
340
|
+
const originalPipe = socket.pipe;
|
|
341
|
+
socket.pipe = function(destination, options) {
|
|
342
|
+
if (destination && typeof destination.destroy === 'function') {
|
|
343
|
+
activeSockets.add(destination);
|
|
344
|
+
destination.once('close', () => activeSockets.delete(destination));
|
|
345
|
+
}
|
|
346
|
+
return originalPipe.apply(this, arguments);
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
|
|
330
350
|
console.log(`░░ Local Proxy Started: http://127.0.0.1:${selectedPort}`);
|
|
331
351
|
} catch (err) {
|
|
332
352
|
console.error("░░ Failed to start proxy server:", err);
|
|
@@ -397,6 +417,12 @@ export async function startProxyServer({
|
|
|
397
417
|
await server.close(true);
|
|
398
418
|
serverRunning = false;
|
|
399
419
|
|
|
420
|
+
// Force-destroy any lingering sockets so Node can exit
|
|
421
|
+
for (const socket of activeSockets) {
|
|
422
|
+
socket.destroy();
|
|
423
|
+
}
|
|
424
|
+
activeSockets.clear();
|
|
425
|
+
|
|
400
426
|
// Auto console.log stats on close
|
|
401
427
|
if (proxy_stats) {
|
|
402
428
|
console.log("░░ Proxy Stats:", getProxyStatsFormatted());
|