arn-browser 0.1.5 → 0.1.7

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.
Files changed (25) hide show
  1. package/bin/cli.js +43 -0
  2. package/bin/install.js +412 -0
  3. package/package.json +8 -3
  4. package/src/index.d.ts +16 -6
  5. package/src/index.js +7 -4
  6. package/src/utility/deleteDirectory.js +2 -2
  7. package/src/utility/{multilogin_token_manager.js → mlx_token.js} +32 -43
  8. package/src/utility/{launchBrowser.d.ts → playwright/pwLaunch.d.ts} +25 -18
  9. package/src/utility/{launchBrowser.js → playwright/pwLaunch.js} +221 -137
  10. package/src/{all_routes/routeWithSuperagent.d.ts → utility/playwright/routes/pwRoute.d.ts} +4 -4
  11. package/src/{all_routes/routeWithSuperagent.js → utility/playwright/routes/pwRoute.js} +2 -2
  12. package/src/utility/proxy-utility/proxy-chain.js +30 -3
  13. package/src/utility/proxy-utility/proxy-helper.js +1 -1
  14. package/src/utility/puppeteer/ppLaunch.d.ts +199 -0
  15. package/src/utility/puppeteer/ppLaunch.js +723 -0
  16. package/src/utility/puppeteer/routes/ppRoute.d.ts +64 -0
  17. package/src/utility/puppeteer/routes/ppRoute.js +326 -0
  18. /package/src/{human-cursor → utility/playwright/human-cursor}/HumanCursor.js +0 -0
  19. /package/src/{human-cursor → utility/playwright/human-cursor}/bezier.js +0 -0
  20. /package/src/{human-cursor → utility/playwright/human-cursor}/index.d.ts +0 -0
  21. /package/src/{human-cursor → utility/playwright/human-cursor}/index.js +0 -0
  22. /package/src/{human-cursor → utility/playwright/human-cursor}/randomizer.js +0 -0
  23. /package/src/{human-cursor → utility/playwright/human-cursor}/tweening.js +0 -0
  24. /package/src/utility/{playwright-helper.d.ts → playwright/playwright-helper.d.ts} +0 -0
  25. /package/src/utility/{playwright-helper.js → playwright/playwright-helper.js} +0 -0
@@ -17,11 +17,11 @@ import { newInjectedContext, FingerprintInjector } from "fingerprint-injector";
17
17
  import { FingerprintGenerator } from "fingerprint-generator";
18
18
 
19
19
  // Internal Utilities
20
- import { getMultiloginToken } from "./multilogin_token_manager.js";
21
- import { deleteDirectoryWithRetries } from "./deleteDirectory.js";
20
+ import { getMultiloginToken } from "../mlx_token.js";
21
+ import { deleteDirectoryWithRetries } from "../deleteDirectory.js";
22
22
 
23
23
  // Human Cursor - for human-like mouse movements
24
- import { createCursor } from "../human-cursor/index.js";
24
+ import { createCursor } from "./human-cursor/index.js";
25
25
 
26
26
  // Camoufox Special
27
27
  import { launchOptions } from "camoufox-js";
@@ -40,8 +40,17 @@ 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 ARN_BROWSER_DIR = path.join(os.homedir(), ".arn-browser");
44
+ const BASE_PROFILE_DIR = path.join(ARN_BROWSER_DIR, "profiles", "pw");
45
+ const PERSISTENT_DIR = path.join(BASE_PROFILE_DIR, "persistent");
46
+ const TEMP_DIR = path.join(BASE_PROFILE_DIR, "temp");
47
+
48
+ if (!fs.existsSync(PERSISTENT_DIR)) fs.mkdirSync(PERSISTENT_DIR, { recursive: true });
49
+ if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
50
+
51
+ // Module-level log flags (set per-launch via launchBrowser options)
52
+ let _launchLogs = false;
53
+ let _cleanupLogs = false;
45
54
 
46
55
  // ==========================================================================
47
56
  // 2. HELPER FUNCTIONS
@@ -62,42 +71,34 @@ function resolveProfilePath(nameOrPath, browserName) {
62
71
  else if (prefix.includes("camoufox")) prefix = "camoufox";
63
72
 
64
73
  const folderName = `${prefix}_${nameOrPath}`;
65
- return path.join(BASE_PROFILE_DIR, folderName);
74
+ return path.join(PERSISTENT_DIR, folderName);
66
75
  }
67
76
 
68
77
  /**
69
78
  * Locates binaries for manual browsers (Brave).
79
+ * Uses ~/.arn-browser/browsers/{browser}/{executable}
70
80
  */
71
81
  function getBinaryPath(browserName) {
72
82
  const isWindows = process.platform === "win32";
73
- const homeDir = os.homedir();
83
+ const browsersDir = path.join(ARN_BROWSER_DIR, "browsers");
74
84
 
75
- let binaryPath = "";
85
+ const binaryMap = {
86
+ brave: isWindows
87
+ ? path.join(browsersDir, "brave", "brave.exe")
88
+ : path.join(browsersDir, "brave", "brave"),
89
+ };
76
90
 
77
- if (browserName === "brave") {
78
- if (isWindows) {
79
- // Windows: Standard custom install path in Downloads
80
- binaryPath = path.join(homeDir, "Downloads", "brave", "brave.exe");
81
- } else {
82
- // Linux: Primary check in .cache (common for script installs)
83
- const cachePath = path.join(homeDir, ".cache", "brave", "brave");
84
- const downloadsPath = path.join(homeDir, "Downloads", "brave", "brave");
85
-
86
- if (fs.existsSync(cachePath)) {
87
- binaryPath = cachePath;
88
- } else if (fs.existsSync(downloadsPath)) {
89
- binaryPath = downloadsPath;
90
- } else {
91
- // Default to cache path for the error message if neither is found
92
- binaryPath = cachePath;
93
- }
94
- }
91
+ const binaryPath = binaryMap[browserName];
92
+ if (!binaryPath) {
93
+ throw new Error(
94
+ `░░░░░ [LaunchBrowser] Unknown browser for getBinaryPath: ${browserName}`
95
+ );
95
96
  }
96
97
 
97
- if (!binaryPath || !fs.existsSync(binaryPath)) {
98
+ if (!fs.existsSync(binaryPath)) {
98
99
  throw new Error(
99
- `░░ [LaunchBrowser] Binary not found for ${browserName} at: ${binaryPath}\n` +
100
- ` Linux checked: ~/.cache/brave/brave AND ~/Downloads/brave/brave`
100
+ `░░░░░ [LaunchBrowser] ${browserName} binary not found at: ${binaryPath}\n` +
101
+ ` Run "node bin/cli.js install" to install browsers.`
101
102
  );
102
103
  }
103
104
 
@@ -105,60 +106,125 @@ function getBinaryPath(browserName) {
105
106
  }
106
107
 
107
108
  /**
108
- * Write or update _profile_meta.json inside a profile directory.
109
- * Tracks creation time, last use, and auto-deletion schedule.
109
+ * Gets the executable path for Playwright-managed browsers (Chromium, Firefox).
110
+ * Uses ~/.arn-browser/browsers/{browser}/{executable}
110
111
  */
111
- function writeProfileMeta(profileDir, deleteAfterMinutes) {
112
- fs.mkdirSync(profileDir, { recursive: true });
113
- const metaPath = path.join(profileDir, "_profile_meta.json");
112
+ function getPlaywrightExePath(browserName) {
113
+ const isWindows = process.platform === "win32";
114
+ const browsersDir = path.join(ARN_BROWSER_DIR, "browsers");
115
+
116
+ const binaryMap = {
117
+ chromium: isWindows
118
+ ? path.join(browsersDir, "chromium", "chrome.exe")
119
+ : path.join(browsersDir, "chromium", "chrome"),
120
+ firefox: isWindows
121
+ ? path.join(browsersDir, "firefox", "firefox.exe")
122
+ : path.join(browsersDir, "firefox", "firefox"),
123
+ };
114
124
 
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();
125
+ const binaryPath = binaryMap[browserName];
126
+ if (!binaryPath) {
127
+ throw new Error(
128
+ `░░░░░ [LaunchBrowser] Unknown browser: ${browserName}`
129
+ );
130
+ }
131
+
132
+ if (!fs.existsSync(binaryPath)) {
133
+ throw new Error(
134
+ `░░░░░ [LaunchBrowser] ${browserName} binary not found at: ${binaryPath}\n` +
135
+ ` Run "node bin/cli.js install" to install browsers.`
136
+ );
120
137
  }
121
138
 
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");
139
+ return binaryPath;
125
140
  }
126
141
 
142
+ const PROFILE_META_FILE = "_profile_meta.json";
143
+
127
144
  /**
128
- * Scan all profiles in BASE_PROFILE_DIR and delete expired ones
129
- * based on their _profile_meta.json metadata.
145
+ * Writes or updates the profile metadata file in a profile directory.
146
+ * On first creation: sets all fields.
147
+ * On subsequent launches: only updates `last_launched_at`.
130
148
  */
131
- function cleanUpProfiles(cleanup_logs = false) {
132
- if (!fs.existsSync(BASE_PROFILE_DIR)) return;
133
-
134
- const now = Date.now();
149
+ function writeProfileMeta(dirPath, type, cleanupMinutes) {
135
150
  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
-
145
- const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
146
- if (!meta.delete_after_minutes || meta.delete_after_minutes <= 0) continue;
151
+ const metaPath = path.join(dirPath, PROFILE_META_FILE);
152
+ const now = new Date().toISOString();
153
+
154
+ if (fs.existsSync(metaPath)) {
155
+ // Update last_launched_at only
156
+ const existing = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
157
+ existing.last_launched_at = now;
158
+ fs.writeFileSync(metaPath, JSON.stringify(existing, null, 2), "utf-8");
159
+ } else {
160
+ // Create new meta
161
+ const meta = {
162
+ type,
163
+ created_at: now,
164
+ last_launched_at: now,
165
+ cleanup_minutes: cleanupMinutes,
166
+ };
167
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
168
+ }
169
+ } catch (e) {
170
+ // Non-critical — don't break the launch
171
+ }
172
+ }
147
173
 
148
- const lastUsed = new Date(meta.last_used_at).getTime();
149
- const expiresAt = lastUsed + meta.delete_after_minutes * 60 * 1000;
174
+ /**
175
+ * Scans both persistent and temp profile directories.
176
+ * Deletes profiles whose `_profile_meta.json` indicates they are expired.
177
+ * Skips the profile currently being launched (skipPath).
178
+ */
179
+ function cleanUpProfiles(skipPath) {
180
+ const now = Date.now();
150
181
 
151
- if (now > expiresAt) {
152
- if (cleanup_logs) console.log(`░░ [Cleanup] Deleting expired profile: ${entry}`);
153
- deleteDirectoryWithRetries(profileDir);
182
+ function scanDir(dir) {
183
+ if (!fs.existsSync(dir)) return;
184
+ try {
185
+ const folders = fs.readdirSync(dir);
186
+ for (const folder of folders) {
187
+ const folderPath = path.join(dir, folder);
188
+ try {
189
+ const stats = fs.statSync(folderPath);
190
+ if (!stats.isDirectory()) continue;
191
+
192
+ // Skip the profile being launched right now
193
+ if (skipPath && path.resolve(folderPath) === path.resolve(skipPath)) continue;
194
+
195
+ const metaPath = path.join(folderPath, PROFILE_META_FILE);
196
+ if (!fs.existsSync(metaPath)) {
197
+ // Legacy temp profile without meta — use mtime, default 15 min
198
+ if (dir === TEMP_DIR && now - stats.mtimeMs > 15 * 60 * 1000) {
199
+ if (_cleanupLogs) console.log(`░░░░░ [Cleanup] Deleting legacy temp profile: ${folder}`);
200
+ deleteDirectoryWithRetries(folderPath);
201
+ }
202
+ continue;
203
+ }
204
+
205
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
206
+ const cleanupMs = meta.cleanup_minutes;
207
+
208
+ // Skip if cleanup is disabled (null or 0)
209
+ if (!cleanupMs || cleanupMs <= 0) continue;
210
+
211
+ const lastLaunch = new Date(meta.last_launched_at).getTime();
212
+ if (now - lastLaunch > cleanupMs * 60 * 1000) {
213
+ if (_cleanupLogs) console.log(`░░░░░ [Cleanup] Deleting expired ${meta.type} profile: ${folder} (last launched ${Math.round((now - lastLaunch) / 60000)} mins ago)`);
214
+ deleteDirectoryWithRetries(folderPath);
215
+ }
216
+ } catch (e) {
217
+ // Ignore per-folder errors (locks, permission, corrupted meta)
154
218
  }
155
- } catch (e) {
156
- // Ignore errors for individual profiles
157
219
  }
220
+ } catch (err) {
221
+ console.error("Cleanup scan error:", err);
158
222
  }
159
- } catch (err) {
160
- console.error("Cleanup Error:", err);
161
223
  }
224
+
225
+ if (_cleanupLogs) console.log("░░░░░ [Cleanup] Scanning for expired profiles...");
226
+ scanDir(PERSISTENT_DIR);
227
+ scanDir(TEMP_DIR);
162
228
  }
163
229
 
164
230
  /**
@@ -198,7 +264,7 @@ function getFingerprintConfig(browserType, screenOptions = null) {
198
264
  // 3. MAIN LAUNCHER ENTRY POINT
199
265
  // ==========================================================================
200
266
 
201
- export async function launchBrowser({
267
+ export async function pwLaunch({
202
268
  // Common
203
269
  timezoneId = null,
204
270
  proxy,
@@ -230,21 +296,23 @@ export async function launchBrowser({
230
296
  try {
231
297
  if (custom_profile_path) throw new Error("Please use profile_path");
232
298
 
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());
299
+ // Set module-level log flags
300
+ _launchLogs = launch_logs;
301
+ _cleanupLogs = cleanup_logs;
237
302
 
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));
303
+ // Resolve path using the browser type to ensure prefixes
304
+ const fullPath = resolveProfilePath(profile_path, which_browser);
242
305
 
243
- // Run cleanup (scans all profiles, deletes expired ones)
244
- cleanUpProfiles(cleanup_logs);
306
+ // Calculate effective cleanup minutes:
307
+ // Persistent: user value if > 0, otherwise null (never clean)
308
+ // Temp: user value if > 0, otherwise 15 min default
309
+ const isPersistent = !!profile_path;
310
+ const effectiveCleanupMinutes = cleanupMinutes > 0
311
+ ? cleanupMinutes
312
+ : (isPersistent ? null : 15);
245
313
 
246
- // Only delete on close for unnamed (throwaway) profiles
247
- const dirToDelete = isNamed ? null : activePath;
314
+ // Run cleanup scan skips the profile being launched
315
+ cleanUpProfiles(fullPath);
248
316
 
249
317
  let browserInstance;
250
318
 
@@ -259,53 +327,45 @@ export async function launchBrowser({
259
327
  case "chromium":
260
328
  case "chrome":
261
329
  browserInstance = await chromiumLauncher({
262
- activePath,
263
- isPersistent: isNamed,
264
- dirToDelete,
265
- launch_logs,
330
+ profilePath: fullPath,
266
331
  proxy,
267
332
  timezoneId,
268
333
  CapSolver,
269
334
  humanize_options: effectiveHumanizeOptions,
270
335
  spoof_fingerprint,
336
+ cleanupMinutes: effectiveCleanupMinutes,
271
337
  });
272
338
  break;
273
339
  case "firefox":
274
340
  browserInstance = await firefoxLauncher({
275
- activePath,
276
- isPersistent: isNamed,
277
- dirToDelete,
278
- launch_logs,
341
+ profilePath: fullPath,
279
342
  proxy,
280
343
  timezoneId,
281
344
  humanize_options: effectiveHumanizeOptions,
282
345
  spoof_fingerprint,
346
+ cleanupMinutes: effectiveCleanupMinutes,
283
347
  });
284
348
  break;
285
349
  case "brave":
286
350
  case "braveLauncher":
287
351
  browserInstance = await braveLauncher({
288
- activePath,
289
- isPersistent: isNamed,
290
- dirToDelete,
291
- launch_logs,
352
+ profilePath: fullPath,
292
353
  proxy,
293
354
  timezoneId,
294
355
  CapSolver,
295
356
  humanize_options: effectiveHumanizeOptions,
296
357
  spoof_fingerprint,
358
+ cleanupMinutes: effectiveCleanupMinutes,
297
359
  });
298
360
  break;
299
361
  case "camoufox":
300
362
  // Camoufox already has its own humanize in camoufox_options
301
363
  browserInstance = await camoufoxLauncher({
302
- activePath,
303
- isPersistent: isNamed,
304
- dirToDelete,
305
- launch_logs,
364
+ profilePath: fullPath,
306
365
  proxy,
307
366
  timezoneId,
308
367
  camoufox_options,
368
+ cleanupMinutes: effectiveCleanupMinutes,
309
369
  // Spoof Fingerprint not needed for Camoufox
310
370
  });
311
371
  break;
@@ -314,7 +374,6 @@ export async function launchBrowser({
314
374
  proxy,
315
375
  multilogin_options,
316
376
  humanize_options: effectiveHumanizeOptions,
317
- launch_logs,
318
377
  // Spoof Fingerprint not needed for Multilogin
319
378
  });
320
379
  break;
@@ -324,7 +383,7 @@ export async function launchBrowser({
324
383
 
325
384
  return browserInstance;
326
385
  } catch (error) {
327
- console.error("░░ [LaunchBrowser] Critical Error:", error.message || error);
386
+ console.error("░░░░░ [LaunchBrowser] Critical Error:", error.message || error);
328
387
  return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
329
388
  }
330
389
  }
@@ -332,9 +391,14 @@ export async function launchBrowser({
332
391
  // ==========================================================================
333
392
  // 4. ENGINE: CHROMIUM
334
393
  // ==========================================================================
335
- async function chromiumLauncher({ activePath, isPersistent, dirToDelete, launch_logs, proxy, timezoneId, CapSolver, humanize_options, spoof_fingerprint }) {
394
+ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, humanize_options, spoof_fingerprint, cleanupMinutes }) {
395
+ const isPersistent = !!profilePath;
336
396
 
337
- if (launch_logs) console.log(`░░ Starting Chromium [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
397
+ // 1. Determine Path (Temp needs it for fingerprint storage, Persistent needs it for data)
398
+ const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
399
+ if (!fs.existsSync(activePath)) fs.mkdirSync(activePath, { recursive: true });
400
+ writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
401
+ if (_launchLogs) console.log(`░░░░░ Starting Chromium [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
338
402
 
339
403
  // 2. Define Args
340
404
  const args = [
@@ -370,7 +434,7 @@ async function chromiumLauncher({ activePath, isPersistent, dirToDelete, launch_
370
434
  const ignoreDefaultArgs = ["--enable-automation"];
371
435
 
372
436
  if (CapSolver) {
373
- const extPath = path.resolve(`./utility/browser-fingerprint/CapSolver`);
437
+ const extPath = path.resolve(`./src/utility/browser-fingerprint/CapSolver`);
374
438
  args.push(`--disable-extensions-except=${extPath}`, `--load-extension=${extPath}`);
375
439
  }
376
440
 
@@ -391,6 +455,7 @@ async function chromiumLauncher({ activePath, isPersistent, dirToDelete, launch_
391
455
  // Launch standard browser (not persistent context)
392
456
  const browser = await chromium.launch({
393
457
  headless: false,
458
+ executablePath: getPlaywrightExePath("chromium"),
394
459
  proxy: proxyObj,
395
460
  args: args,
396
461
  ignoreDefaultArgs: ignoreDefaultArgs,
@@ -414,8 +479,8 @@ async function chromiumLauncher({ activePath, isPersistent, dirToDelete, launch_
414
479
 
415
480
  const page = context.pages()[0] || (await context.newPage());
416
481
 
417
- // Pass dirToDelete so the temp folder gets deleted on close
418
- return createBrowserController(browser, context, page, dirToDelete, humanize_options, launch_logs);
482
+ // Pass 'activePath' so the temp folder (containing fingerprint.json) gets deleted on close
483
+ return createBrowserController(browser, context, page, activePath, humanize_options);
419
484
  } catch (err) {
420
485
  console.error("Chromium Temp Launch Error:", err);
421
486
  throw err;
@@ -432,6 +497,7 @@ async function chromiumLauncher({ activePath, isPersistent, dirToDelete, launch_
432
497
  // Logic: Native Persistent Launch. No fingerprint-injector.
433
498
  const context = await chromium.launchPersistentContext(activePath, {
434
499
  headless: false,
500
+ executablePath: getPlaywrightExePath("chromium"),
435
501
  proxy: proxyObj,
436
502
  args: args,
437
503
  timezoneId: tz,
@@ -444,7 +510,7 @@ async function chromiumLauncher({ activePath, isPersistent, dirToDelete, launch_
444
510
 
445
511
  const page = context.pages()[0];
446
512
 
447
- return createBrowserController(context, context, page, dirToDelete, humanize_options, launch_logs);
513
+ return createBrowserController(context, context, page, null, humanize_options);
448
514
  } catch (err) {
449
515
  console.error("Chromium Persistent Launch Error:", err);
450
516
  throw err;
@@ -455,9 +521,14 @@ async function chromiumLauncher({ activePath, isPersistent, dirToDelete, launch_
455
521
  // ==========================================================================
456
522
  // 5. ENGINE: FIREFOX
457
523
  // ==========================================================================
458
- async function firefoxLauncher({ activePath, isPersistent, dirToDelete, launch_logs, proxy, timezoneId, humanize_options, spoof_fingerprint }) {
524
+ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_options, spoof_fingerprint, cleanupMinutes }) {
525
+ const isPersistent = !!profilePath;
459
526
 
460
- if (launch_logs) console.log(`░░ Starting Firefox [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
527
+ // 1. Determine Path
528
+ const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
529
+ if (!fs.existsSync(activePath)) fs.mkdirSync(activePath, { recursive: true });
530
+ writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
531
+ if (_launchLogs) console.log(`░░░░░ Starting Firefox [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
461
532
 
462
533
  const proxyObj = formatProxy(proxy);
463
534
  const tz = timezoneId || undefined;
@@ -482,6 +553,7 @@ async function firefoxLauncher({ activePath, isPersistent, dirToDelete, launch_l
482
553
 
483
554
  const browser = await firefox.launch({
484
555
  headless: false,
556
+ executablePath: getPlaywrightExePath("firefox"),
485
557
  proxy: proxyObj,
486
558
  ignoreDefaultArgs: ["--enable-automation"],
487
559
  firefoxUserPrefs: firefoxUserPrefs,
@@ -506,8 +578,8 @@ async function firefoxLauncher({ activePath, isPersistent, dirToDelete, launch_l
506
578
 
507
579
  const page = context.pages()[0] || (await context.newPage());
508
580
 
509
- // Pass dirToDelete so the temp folder gets deleted on close
510
- return createBrowserController(browser, context, page, dirToDelete, humanize_options, launch_logs);
581
+ // Pass 'activePath' to delete temp folder later
582
+ return createBrowserController(browser, context, page, activePath, humanize_options);
511
583
  } catch (err) {
512
584
  console.error("Firefox Temp Launch Error:", err);
513
585
  throw err;
@@ -523,6 +595,7 @@ async function firefoxLauncher({ activePath, isPersistent, dirToDelete, launch_l
523
595
 
524
596
  const context = await firefox.launchPersistentContext(activePath, {
525
597
  headless: false,
598
+ executablePath: getPlaywrightExePath("firefox"),
526
599
  proxy: proxyObj,
527
600
  timezoneId: tz,
528
601
  ignoreDefaultArgs: ["--enable-automation"],
@@ -535,7 +608,7 @@ async function firefoxLauncher({ activePath, isPersistent, dirToDelete, launch_l
535
608
 
536
609
  const page = context.pages()[0];
537
610
 
538
- return createBrowserController(context, context, page, dirToDelete, humanize_options, launch_logs);
611
+ return createBrowserController(context, context, page, null, humanize_options);
539
612
  } catch (err) {
540
613
  console.error("Firefox Persistent Launch Error:", err);
541
614
  throw err;
@@ -546,10 +619,13 @@ async function firefoxLauncher({ activePath, isPersistent, dirToDelete, launch_l
546
619
  // ==========================================================================
547
620
  // 6. ENGINE: BRAVE
548
621
  // ==========================================================================
549
- async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_logs, proxy, CapSolver, timezoneId, humanize_options, spoof_fingerprint }) {
622
+ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humanize_options, spoof_fingerprint, cleanupMinutes }) {
623
+ const isPersistent = !!profilePath;
624
+ const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
550
625
 
551
- if (launch_logs) console.log(`░░ Starting Brave: ${activePath}`);
626
+ if (_launchLogs) console.log(`░░░░░ Starting Brave: ${activePath}`);
552
627
  fs.mkdirSync(activePath, { recursive: true });
628
+ writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
553
629
 
554
630
  // ======================================================
555
631
  // Persist spoof_fingerprint setting for persistent profiles
@@ -563,12 +639,12 @@ async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_log
563
639
  // Load saved settings from first launch
564
640
  const savedSettings = JSON.parse(fs.readFileSync(settingsFilePath, "utf-8"));
565
641
  effectiveSpoofFingerprint = savedSettings.spoof_fingerprint;
566
- if (launch_logs) console.log(`░░ Using saved spoof_fingerprint: ${JSON.stringify(effectiveSpoofFingerprint)} (ignoring current: ${JSON.stringify(spoof_fingerprint)})`);
642
+ if (_launchLogs) console.log(`░░░░░ Using saved spoof_fingerprint: ${JSON.stringify(effectiveSpoofFingerprint)} (ignoring current: ${JSON.stringify(spoof_fingerprint)})`);
567
643
  } else {
568
644
  // First launch - save the current setting
569
645
  const settings = { spoof_fingerprint: spoof_fingerprint };
570
646
  fs.writeFileSync(settingsFilePath, JSON.stringify(settings, null, 2), "utf-8");
571
- if (launch_logs) console.log(`░░ Saved spoof_fingerprint setting: ${JSON.stringify(spoof_fingerprint)}`);
647
+ if (_launchLogs) console.log(`░░░░░ Saved spoof_fingerprint setting: ${JSON.stringify(spoof_fingerprint)}`);
572
648
  }
573
649
  }
574
650
 
@@ -592,7 +668,7 @@ async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_log
592
668
 
593
669
  if (isPersistent) {
594
670
  fs.writeFileSync(fingerprintFilePath, JSON.stringify(fingerprintData, null, 2), "utf-8");
595
- if (launch_logs) console.log("░░ Generated and saved new fingerprint for Brave");
671
+ if (_launchLogs) console.log("░░░░░ Generated and saved new fingerprint for Brave");
596
672
  }
597
673
  }
598
674
 
@@ -616,7 +692,7 @@ async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_log
616
692
  prefs.brave.sidebar.sidebar_show_option = 3;
617
693
  fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
618
694
  } catch (e) {
619
- if (launch_logs) console.warn("░░ Could not modify Brave preferences:", e.message);
695
+ console.warn("░░░░░ Could not modify Brave preferences:", e.message);
620
696
  }
621
697
 
622
698
  const args = [
@@ -656,7 +732,7 @@ async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_log
656
732
  ];
657
733
  const ignoreDefaultArgs = ["--enable-automation"];
658
734
  if (CapSolver) {
659
- const extPath = path.resolve(`./utility/browser-fingerprint/CapSolver`);
735
+ const extPath = path.resolve(`./src/utility/browser-fingerprint/CapSolver`);
660
736
  args.push(`--load-extension=${extPath}`);
661
737
  }
662
738
 
@@ -703,11 +779,11 @@ async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_log
703
779
 
704
780
 
705
781
  // ======================================================
706
- // FIX: Close Extra Tabs (Brave Dashboard / Restore)
782
+ // ░░░░░ FIX: Close Extra Tabs (Brave Dashboard / Restore)
707
783
  // ======================================================
708
784
  const pages = context.pages();
709
785
  if (pages.length > 1) {
710
- if (launch_logs) console.log(`░░ Found ${pages.length} tabs open in Brave. Closing extras...`);
786
+ if (_launchLogs) console.log(`░░░░░ Found ${pages.length} tabs open in Brave. Closing extras...`);
711
787
  // Close all tabs except the last one (which is usually the fresh active one)
712
788
  for (let i = 0; i < pages.length - 1; i++) {
713
789
  await pages[i].close();
@@ -715,17 +791,21 @@ async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_log
715
791
  }
716
792
  const page = context.pages()[0];
717
793
 
794
+ const dirToDelete = isPersistent ? null : activePath;
718
795
  // 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);
796
+ return createBrowserController(context, context, page, dirToDelete, humanize_options);
720
797
  }
721
798
 
722
799
  // ==========================================================================
723
800
  // 7. ENGINE: CAMOUFOX
724
801
  // ==========================================================================
725
- async function camoufoxLauncher({ activePath, isPersistent, dirToDelete, launch_logs, proxy, timezoneId, camoufox_options = {} }) {
802
+ async function camoufoxLauncher({ profilePath, proxy, timezoneId, camoufox_options = {}, cleanupMinutes }) {
803
+ const isPersistent = !!profilePath;
804
+ const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
726
805
 
727
- if (launch_logs) console.log(`░░ Starting camoufoxJs: ${activePath}`);
806
+ if (_launchLogs) console.log(`░░░░░ Starting camoufoxJs: ${activePath}`);
728
807
  fs.mkdirSync(activePath, { recursive: true });
808
+ writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
729
809
 
730
810
  const proxyObj = formatProxy(proxy);
731
811
  const tz = timezoneId || undefined;
@@ -790,7 +870,7 @@ async function camoufoxLauncher({ activePath, isPersistent, dirToDelete, launch_
790
870
  // Save persistent config
791
871
  if (isPersistent) {
792
872
  fs.writeFileSync(fingerprintFilePath, JSON.stringify(launchConfig, null, 2), "utf-8");
793
- if (launch_logs) console.log("░░ Generated and saved new launch config for Camoufox");
873
+ if (_launchLogs) console.log("░░░░░ Generated and saved new launch config for Camoufox");
794
874
  }
795
875
  }
796
876
 
@@ -808,13 +888,14 @@ async function camoufoxLauncher({ activePath, isPersistent, dirToDelete, launch_
808
888
  // Removed viewport: null, handled by Camoufox args
809
889
  });
810
890
 
811
- return createBrowserController(context, context, context.pages()[0], dirToDelete, null, launch_logs);
891
+ const dirToDelete = isPersistent ? null : activePath;
892
+ return createBrowserController(context, context, context.pages()[0], dirToDelete);
812
893
  }
813
894
 
814
895
  // ==========================================================================
815
896
  // 8. ENGINE: MULTILOGIN
816
897
  // ==========================================================================
817
- async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_options = null, launch_logs = false }) {
898
+ async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_options = null }) {
818
899
  // Destructure defaults from multilogin_options
819
900
  const {
820
901
  profileId = null,
@@ -826,7 +907,7 @@ async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_opt
826
907
  } = multilogin_options;
827
908
 
828
909
  if (profileId) {
829
- return await launchExistingMultiloginProfile(profileId, humanize_options, launch_logs);
910
+ return await launchExistingMultiloginProfile(profileId, humanize_options);
830
911
  } else {
831
912
  return await launchQuickMultiloginProfile({
832
913
  os_type,
@@ -835,12 +916,11 @@ async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_opt
835
916
  media_masking,
836
917
  audio_masking,
837
918
  humanize_options,
838
- launch_logs,
839
919
  });
840
920
  }
841
921
  }
842
922
 
843
- async function launchExistingMultiloginProfile(profileId, humanize_options = null, launch_logs = false) {
923
+ async function launchExistingMultiloginProfile(profileId, humanize_options = null) {
844
924
  const startUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v2/profile/f/${MULTILOGIN_FOLDER_ID}/p/${profileId}/start?automation_type=playwright&headless_mode=false`;
845
925
  let browser, context, page;
846
926
 
@@ -854,7 +934,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
854
934
  if (response.status === 400) {
855
935
  const errorJson = await response.json().catch(() => null);
856
936
  if (errorJson?.status?.error_code === "PROFILE_ALREADY_RUNNING") {
857
- if (launch_logs) console.log(`░░ Profile ${profileId} is running. Restarting...`);
937
+ if (_launchLogs) console.log(`░░░░░ Profile ${profileId} is running. Restarting...`);
858
938
  await stopMultiloginProfile(profileId);
859
939
  await sleep(5000);
860
940
  response = await fetch(startUrl, {
@@ -874,7 +954,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
874
954
 
875
955
  const data = await response.json();
876
956
  const port = data.data.port;
877
- if (launch_logs) console.log(`░░ Multilogin Profile ${profileId} started on port ${port}`);
957
+ if (_launchLogs) console.log(`░░░░░ Multilogin Profile ${profileId} started on port ${port}`);
878
958
 
879
959
  browser = await chromium.connectOverCDP(`http://localhost:${port}`);
880
960
  context = browser.contexts()[0];
@@ -900,7 +980,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
900
980
  }
901
981
  }
902
982
 
903
- async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, humanize_options = null, launch_logs = false }) {
983
+ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, humanize_options = null }) {
904
984
  const createUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v3/profile/quick`;
905
985
  let browser, context, page, profileId;
906
986
 
@@ -948,7 +1028,7 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
948
1028
 
949
1029
  profileId = json.data.id;
950
1030
  const port = json.data.port;
951
- if (launch_logs) console.log(`░░ Quick Profile Created: ${profileId} on port ${port}`);
1031
+ if (_launchLogs) console.log(`░░░░░ Quick Profile Created: ${profileId} on port ${port}`);
952
1032
 
953
1033
  browser = await chromium.connectOverCDP(`http://localhost:${port}`);
954
1034
  context = browser.contexts()[0];
@@ -988,7 +1068,7 @@ async function stopMultiloginProfile(profileId) {
988
1068
  }
989
1069
  throw new Error(txt);
990
1070
  }
991
- if (launch_logs) console.log(`░░ Profile ${profileId} stopped.`);
1071
+ if (_launchLogs) console.log(`░░░░░ Profile ${profileId} stopped.`);
992
1072
  return { error: null };
993
1073
  } catch (err) {
994
1074
  console.error(`Error stopping ${profileId}:`, err);
@@ -1034,7 +1114,7 @@ function formatProxy(proxy) {
1034
1114
  return p;
1035
1115
  }
1036
1116
 
1037
- function createBrowserController(browser, context, page, dirToDelete = null, humanize_options = null, launch_logs = false) {
1117
+ function createBrowserController(browser, context, page, dirToDelete = null, humanize_options = null) {
1038
1118
  // Apply human cursor if options provided
1039
1119
  let humanPage = page;
1040
1120
  if (humanize_options && page) {
@@ -1043,7 +1123,7 @@ function createBrowserController(browser, context, page, dirToDelete = null, hum
1043
1123
 
1044
1124
  const closeBrowser = async () => {
1045
1125
  try {
1046
- if (launch_logs) console.log("░░ Closing browser session...");
1126
+ if (_launchLogs) console.log("░░░░░ Closing browser session...");
1047
1127
  if (context) await context.close();
1048
1128
  if (browser && typeof browser.close === "function" && browser !== context) {
1049
1129
  try {
@@ -1051,8 +1131,12 @@ function createBrowserController(browser, context, page, dirToDelete = null, hum
1051
1131
  } catch (e) { }
1052
1132
  }
1053
1133
  if (dirToDelete) {
1054
- if (launch_logs) console.log(`░░ Deleting temp profile: ${dirToDelete}`);
1055
- await deleteDirectoryWithRetries(dirToDelete);
1134
+ if (dirToDelete.includes("persistent")) {
1135
+ console.warn(`░░░░░ Safety Block: Attempted to delete persistent profile: ${dirToDelete}`);
1136
+ } else {
1137
+ if (_launchLogs) console.log(`░░░░░ Deleting temp profile: ${dirToDelete}`);
1138
+ await deleteDirectoryWithRetries(dirToDelete);
1139
+ }
1056
1140
  }
1057
1141
  return true;
1058
1142
  } catch (err) {