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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arn-browser",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A lightweight, browser autmation helper.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -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(`░░ Failed to delete directory after ${maxRetries} attempts: ${targetPath}`);
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
- `░░ Delete failed (Attempt ${attempt + 1}/${maxRetries}). File might be locked. Retrying in ${retryDelayMs / 1000
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 browser profile.
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/browser_profiles/` with a browser prefix (e.g., `brave_myProfile`).
156
- * - If null: Creates a temporary, random profile that is deleted on browser close.
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-delete schedule for named profiles (in minutes).
165
- * - `0` or not set: Profile is permanent (never auto-deleted).
166
- * - `> 0`: Profile is auto-deleted after this many minutes of inactivity.
167
- * The inactivity timer resets on each launch.
168
- * - Only applies to named profiles (with `profile_path`).
169
- * Unnamed profiles are always deleted on close regardless.
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
- // 7. LOGGING
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", "browser_profiles");
44
- if (!fs.existsSync(BASE_PROFILE_DIR)) fs.mkdirSync(BASE_PROFILE_DIR, { recursive: true });
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(BASE_PROFILE_DIR, folderName);
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
- `░░ [LaunchBrowser] Binary not found for ${browserName} at: ${binaryPath}\n` +
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
- * Write or update _profile_meta.json inside a profile directory.
109
- * Tracks creation time, last use, and auto-deletion schedule.
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(profileDir, deleteAfterMinutes) {
112
- fs.mkdirSync(profileDir, { recursive: true });
113
- const metaPath = path.join(profileDir, "_profile_meta.json");
114
-
115
- let meta = {};
116
- if (fs.existsSync(metaPath)) {
117
- try { meta = JSON.parse(fs.readFileSync(metaPath, "utf-8")); } catch (e) { meta = {}; }
118
- } else {
119
- meta.created_at = new Date().toISOString();
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
- * Scan all profiles in BASE_PROFILE_DIR and delete expired ones
129
- * based on their _profile_meta.json metadata.
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(cleanup_logs = false) {
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
- const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
146
- if (!meta.delete_after_minutes || meta.delete_after_minutes <= 0) continue;
147
-
148
- const lastUsed = new Date(meta.last_used_at).getTime();
149
- const expiresAt = lastUsed + meta.delete_after_minutes * 60 * 1000;
150
-
151
- if (now > expiresAt) {
152
- if (cleanup_logs) console.log(`░░ [Cleanup] Deleting expired profile: ${entry}`);
153
- deleteDirectoryWithRetries(profileDir);
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
- // ====== Centralized Path Resolution ======
234
- const isNamed = !!profile_path;
235
- const resolvedPath = resolveProfilePath(profile_path, which_browser);
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
- // Write/update metadata for all profiles
239
- // For unnamed profiles: use provided cleanupMinutes, or default to 15 min
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
- // Run cleanup (scans all profiles, deletes expired ones)
244
- cleanUpProfiles(cleanup_logs);
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
- // Only delete on close for unnamed (throwaway) profiles
247
- const dirToDelete = isNamed ? null : activePath;
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
- activePath,
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
- activePath,
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
- activePath,
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
- activePath,
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("░░ [LaunchBrowser] Critical Error:", error.message || 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({ activePath, isPersistent, dirToDelete, launch_logs, proxy, timezoneId, CapSolver, humanize_options, spoof_fingerprint }) {
367
+ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, humanize_options, spoof_fingerprint, cleanupMinutes }) {
368
+ const isPersistent = !!profilePath;
336
369
 
337
- if (launch_logs) console.log(`░░ Starting Chromium [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
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 dirToDelete so the temp folder gets deleted on close
418
- return createBrowserController(browser, context, page, dirToDelete, humanize_options, launch_logs);
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, dirToDelete, humanize_options, launch_logs);
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({ activePath, isPersistent, dirToDelete, launch_logs, proxy, timezoneId, humanize_options, spoof_fingerprint }) {
495
+ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_options, spoof_fingerprint, cleanupMinutes }) {
496
+ const isPersistent = !!profilePath;
459
497
 
460
- if (launch_logs) console.log(`░░ Starting Firefox [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
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 dirToDelete so the temp folder gets deleted on close
510
- return createBrowserController(browser, context, page, dirToDelete, humanize_options, launch_logs);
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, dirToDelete, humanize_options, launch_logs);
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({ activePath, isPersistent, dirToDelete, launch_logs, proxy, CapSolver, timezoneId, humanize_options, spoof_fingerprint }) {
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 (launch_logs) console.log(`░░ Starting Brave: ${activePath}`);
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 (launch_logs) console.log(`░░ Using saved spoof_fingerprint: ${JSON.stringify(effectiveSpoofFingerprint)} (ignoring current: ${JSON.stringify(spoof_fingerprint)})`);
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 (launch_logs) console.log(`░░ Saved spoof_fingerprint setting: ${JSON.stringify(spoof_fingerprint)}`);
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 (launch_logs) console.log("░░ Generated and saved new fingerprint for Brave");
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
- if (launch_logs) console.warn("░░ Could not modify Brave preferences:", e.message);
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 (launch_logs) console.log(`░░ Found ${pages.length} tabs open in Brave. Closing extras...`);
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, launch_logs);
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({ activePath, isPersistent, dirToDelete, launch_logs, proxy, timezoneId, camoufox_options = {} }) {
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 (launch_logs) console.log(`░░ Starting camoufoxJs: ${activePath}`);
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 (launch_logs) console.log("░░ Generated and saved new launch config for Camoufox");
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
- return createBrowserController(context, context, context.pages()[0], dirToDelete, null, launch_logs);
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, launch_logs = false }) {
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, launch_logs);
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, launch_logs = false) {
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 (launch_logs) console.log(`░░ Profile ${profileId} is running. Restarting...`);
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 (launch_logs) console.log(`░░ Multilogin Profile ${profileId} started on port ${port}`);
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, launch_logs = false }) {
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 (launch_logs) console.log(`░░ Quick Profile Created: ${profileId} on port ${port}`);
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 (launch_logs) console.log(`░░ Profile ${profileId} stopped.`);
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, launch_logs = false) {
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 (launch_logs) console.log("░░ Closing browser session...");
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 (launch_logs) console.log(`░░ Deleting temp profile: ${dirToDelete}`);
1055
- await deleteDirectoryWithRetries(dirToDelete);
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());