arn-browser 0.0.2 → 0.0.4

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.
@@ -0,0 +1,868 @@
1
+ /**
2
+ * @file launchBrowser.js
3
+ * @description Main entry point for launching various browser engines.
4
+ * Supports: Chromium, Firefox (Playwright), Brave, Camoufox, and Multilogin.
5
+ * Handles: Persistent profiles (with prefixes), temp profiles, and tab cleanup.
6
+ */
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import os from "os";
11
+ import crypto from "node:crypto";
12
+ import { chromium, firefox } from "playwright";
13
+ import { setTimeout as sleep } from "timers/promises";
14
+
15
+ // Fingerprint management
16
+ import { newInjectedContext, FingerprintInjector } from "fingerprint-injector";
17
+ import { FingerprintGenerator } from "fingerprint-generator";
18
+
19
+ // Internal Utilities
20
+ import { getMultiloginToken } from "./multilogin_token_manager.js";
21
+ import { deleteDirectoryWithRetries } from "./deleteDirectory.js";
22
+
23
+ // Camoufox Special
24
+ import { launchOptions } from "camoufox-js";
25
+
26
+ // ==========================================================================
27
+ // 1. CONFIGURATION & CONSTANTS
28
+ // ==========================================================================
29
+
30
+ const osMap = {
31
+ win32: "windows",
32
+ darwin: "macos",
33
+ linux: "linux",
34
+ };
35
+ const detectedOs = osMap[process.platform] || "windows";
36
+
37
+ const MULTILOGIN_LAUNCHER_URL = "https://launcher.mlx.yt:45001";
38
+ const MULTILOGIN_FOLDER_ID = "bad9e7e1-cfab-4c8d-bd19-91aa82929711";
39
+
40
+ const PROJECT_ROOT = process.cwd();
41
+ const BASE_PROFILE_DIR = path.join(PROJECT_ROOT, ".data", "browser_profiles");
42
+ const PERSISTENT_DIR = path.join(BASE_PROFILE_DIR, "persistent");
43
+ const TEMP_DIR = path.join(BASE_PROFILE_DIR, "temp");
44
+
45
+ if (!fs.existsSync(PERSISTENT_DIR)) fs.mkdirSync(PERSISTENT_DIR, { recursive: true });
46
+ if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
47
+
48
+ // ==========================================================================
49
+ // 2. HELPER FUNCTIONS
50
+ // ==========================================================================
51
+
52
+ /**
53
+ * Resolves the persistent path with a browser-specific prefix.
54
+ */
55
+ function resolveProfilePath(nameOrPath, browserName) {
56
+ if (!nameOrPath) return null;
57
+
58
+ if (path.isAbsolute(nameOrPath)) return nameOrPath;
59
+
60
+ let prefix = browserName.toLowerCase();
61
+ if (prefix.includes("brave")) prefix = "brave";
62
+ else if (prefix.includes("chrome") || prefix.includes("chromium")) prefix = "chromium";
63
+ else if (prefix.includes("firefox")) prefix = "firefox";
64
+ else if (prefix.includes("camoufox")) prefix = "camoufox";
65
+
66
+ const folderName = `${prefix}_${nameOrPath}`;
67
+ return path.join(PERSISTENT_DIR, folderName);
68
+ }
69
+
70
+ /**
71
+ * Locates binaries for manual browsers (Brave).
72
+ */
73
+ function getBinaryPath(browserName) {
74
+ const isWindows = process.platform === "win32";
75
+ const homeDir = os.homedir();
76
+
77
+ let binaryPath = "";
78
+
79
+ if (browserName === "brave") {
80
+ if (isWindows) {
81
+ // Windows: Standard custom install path in Downloads
82
+ binaryPath = path.join(homeDir, "Downloads", "brave", "brave.exe");
83
+ } else {
84
+ // Linux: Primary check in .cache (common for script installs)
85
+ const cachePath = path.join(homeDir, ".cache", "brave", "brave");
86
+ const downloadsPath = path.join(homeDir, "Downloads", "brave", "brave");
87
+
88
+ if (fs.existsSync(cachePath)) {
89
+ binaryPath = cachePath;
90
+ } else if (fs.existsSync(downloadsPath)) {
91
+ binaryPath = downloadsPath;
92
+ } else {
93
+ // Default to cache path for the error message if neither is found
94
+ binaryPath = cachePath;
95
+ }
96
+ }
97
+ }
98
+
99
+ if (!binaryPath || !fs.existsSync(binaryPath)) {
100
+ throw new Error(
101
+ `❌ [LaunchBrowser] Binary not found for ${browserName} at: ${binaryPath}\n` +
102
+ ` Linux checked: ~/.cache/brave/brave AND ~/Downloads/brave/brave`
103
+ );
104
+ }
105
+
106
+ return binaryPath;
107
+ }
108
+
109
+ function cleanUpTempProfiles(ageLimitMinutes) {
110
+ if (!ageLimitMinutes || ageLimitMinutes < 10) return;
111
+
112
+ console.log(`🧹 [Cleanup] Checking for temp profiles older than ${ageLimitMinutes} mins...`);
113
+ const now = Date.now();
114
+ const limit = ageLimitMinutes * 60 * 1000;
115
+
116
+ try {
117
+ const files = fs.readdirSync(TEMP_DIR);
118
+ files.forEach((file) => {
119
+ const curPath = path.join(TEMP_DIR, file);
120
+ try {
121
+ const stats = fs.statSync(curPath);
122
+ if (stats.isDirectory() && now - stats.mtimeMs > limit) {
123
+ console.log(` Deleting expired temp profile: ${file}`);
124
+ deleteDirectoryWithRetries(curPath);
125
+ }
126
+ } catch (e) {
127
+ // Ignore file lock errors
128
+ }
129
+ });
130
+ } catch (err) {
131
+ console.error("Cleanup Error:", err);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Helper to generate consistent fingerprint options.
137
+ */
138
+ function getFingerprintConfig(browserType, maxWidth) {
139
+ const config = {
140
+ devices: ["desktop"],
141
+ operatingSystems: [detectedOs],
142
+ locales: ["en-US"],
143
+ browsers: [],
144
+ screen: {},
145
+ };
146
+
147
+ if (browserType === "chromium") {
148
+ config.browsers.push({ name: "chrome", minVersion: 140 });
149
+ } else if (browserType === "firefox") {
150
+ config.browsers.push({ name: "firefox", minVersion: 140 });
151
+ } else if (browserType === "brave") {
152
+ config.browsers.push({ name: "chrome", minVersion: 140 });
153
+ }
154
+
155
+ if (maxWidth) {
156
+ const widthVal = maxWidth <= 10 ? Math.round(maxWidth * 1920) : maxWidth;
157
+ config.screen.maxWidth = widthVal;
158
+ }
159
+
160
+ return config;
161
+ }
162
+
163
+ // ==========================================================================
164
+ // 3. MAIN LAUNCHER ENTRY POINT
165
+ // ==========================================================================
166
+
167
+ export async function launchBrowser({
168
+ // Common
169
+ timezoneId = null,
170
+ proxy,
171
+ maxWidth = 1,
172
+
173
+ // Path & Storage
174
+ profile_path = null,
175
+ custom_profile_path = null, // Backward (Just for throwing error)
176
+ cleanupMinutes = 0,
177
+
178
+ which_browser = "chromium",
179
+ CapSolver = false,
180
+
181
+ // Browser Specific Grouped Options
182
+ camoufox_options = {}, // { geoip, humanize, ... }
183
+ multilogin_options = {}, // { profileId, os_type, canvas_noise, ... }
184
+ }) {
185
+ try {
186
+ if (custom_profile_path) throw new Error("Please use profile_path");
187
+
188
+ // Resolve path using the browser type to ensure prefixes
189
+ const fullPath = resolveProfilePath(profile_path, which_browser);
190
+ cleanUpTempProfiles(cleanupMinutes);
191
+
192
+ let browserInstance;
193
+
194
+ switch (which_browser) {
195
+ case "chromium":
196
+ case "chrome":
197
+ browserInstance = await chromiumLauncher({
198
+ profilePath: fullPath,
199
+ proxy,
200
+ timezoneId,
201
+ CapSolver,
202
+ maxWidth,
203
+ });
204
+ break;
205
+ case "firefox":
206
+ browserInstance = await firefoxLauncher({
207
+ profilePath: fullPath,
208
+ proxy,
209
+ timezoneId,
210
+ maxWidth,
211
+ });
212
+ break;
213
+ case "brave":
214
+ case "braveLauncher":
215
+ browserInstance = await braveLauncher({
216
+ profilePath: fullPath,
217
+ proxy,
218
+ timezoneId,
219
+ CapSolver,
220
+ maxWidth,
221
+ });
222
+ break;
223
+ case "camoufox":
224
+ browserInstance = await camoufoxLauncher({
225
+ profilePath: fullPath,
226
+ proxy,
227
+ timezoneId,
228
+ maxWidth,
229
+ camoufox_options,
230
+ });
231
+ break;
232
+ case "multilogin":
233
+ browserInstance = await multiloginLauncher({
234
+ proxy,
235
+ multilogin_options,
236
+ });
237
+ break;
238
+ default:
239
+ throw new Error(`Unknown browser type: ${which_browser}`);
240
+ }
241
+
242
+ return browserInstance;
243
+ } catch (error) {
244
+ console.error("❌ [LaunchBrowser] Critical Error:", error.message || error);
245
+ return { data: null, error: error, closeBrowser: async () => {} };
246
+ }
247
+ }
248
+
249
+ // ==========================================================================
250
+ // 4. ENGINE: CHROMIUM
251
+ // ==========================================================================
252
+ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, maxWidth }) {
253
+ const isPersistent = !!profilePath;
254
+
255
+ // 1. Determine Path (Temp needs it for fingerprint storage, Persistent needs it for data)
256
+ const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
257
+ console.log(`🚀 Starting Chromium [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
258
+
259
+ // 2. Define Args
260
+ const args = [
261
+ "--test-type",
262
+ // --- Stealth & Anti-Detection ---
263
+ "--disable-blink-features=AutomationControlled",
264
+ // "--disable-features=UserAgentClientHint",
265
+ // --- PREVENT EXTRA TABS ---
266
+ // "--homepage=about:blank",
267
+ "--disable-restore-session-state",
268
+ // --- Error Suppression ---
269
+ "--disable-session-crashed-bubble",
270
+ "--hide-crash-restore-bubble",
271
+ // --- Silence & Networking ---
272
+ "--disable-background-networking",
273
+ "--disable-background-timer-throttling",
274
+ "--disable-breakpad",
275
+ "--disable-crash-reporter",
276
+ "--disable-component-update",
277
+ "--disable-sync",
278
+ "--no-default-browser-check",
279
+ "--no-first-run",
280
+ "--disable-domain-reliability",
281
+ "--disable-client-side-phishing-detection",
282
+ // --- UI ---
283
+ "--disable-infobars",
284
+ ];
285
+ const ignoreDefaultArgs = ["--enable-automation", "--no-sandbox"];
286
+
287
+ if (CapSolver) {
288
+ const extPath = path.resolve(`./utility/browser-fingerprint/CapSolver`);
289
+ args.push(`--disable-extensions-except=${extPath}`, `--load-extension=${extPath}`);
290
+ }
291
+
292
+ const proxyObj = formatProxy(proxy);
293
+ const tz = timezoneId || undefined;
294
+
295
+ // ==================================================================
296
+ // BRANCH A: TEMP PROFILE (launch + newInjectedContext)
297
+ // ==================================================================
298
+ if (!isPersistent) {
299
+ try {
300
+ const fpConfig = getFingerprintConfig("chromium", maxWidth);
301
+ const fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
302
+
303
+ // Launch standard browser (not persistent context)
304
+ const browser = await chromium.launch({
305
+ headless: false,
306
+ proxy: proxyObj,
307
+ args: args,
308
+ ignoreDefaultArgs: ignoreDefaultArgs,
309
+ });
310
+
311
+ // Inject the fingerprint
312
+ const context = await newInjectedContext(browser, {
313
+ fingerprint: fingerprintData,
314
+ timezoneId: tz,
315
+ });
316
+
317
+ const page = context.pages()[0] || (await context.newPage());
318
+
319
+ // Pass 'activePath' so the temp folder (containing fingerprint.json) gets deleted on close
320
+ return createBrowserController(browser, context, page, activePath);
321
+ } catch (err) {
322
+ console.error("Chromium Temp Launch Error:", err);
323
+ throw err;
324
+ }
325
+ }
326
+ // ==================================================================
327
+ // BRANCH B: PERSISTENT PROFILE (launchPersistentContext + Manual Script)
328
+ // ==================================================================
329
+ else {
330
+ try {
331
+ if (!fs.existsSync(activePath)) fs.mkdirSync(activePath, { recursive: true });
332
+
333
+ // Logic: Native Persistent Launch. No fingerprint-injector.
334
+ const context = await chromium.launchPersistentContext(activePath, {
335
+ headless: false,
336
+ proxy: proxyObj,
337
+ args: args,
338
+ timezoneId: tz,
339
+ ignoreDefaultArgs: ignoreDefaultArgs,
340
+ viewport: null,
341
+ });
342
+
343
+ // Manual Stealth Script
344
+ await addStealthScript(context);
345
+
346
+ const page = context.pages()[0];
347
+
348
+ return createBrowserController(context, context, page, null);
349
+ } catch (err) {
350
+ console.error("Chromium Persistent Launch Error:", err);
351
+ throw err;
352
+ }
353
+ }
354
+ }
355
+
356
+ // ==========================================================================
357
+ // 5. ENGINE: FIREFOX
358
+ // ==========================================================================
359
+ async function firefoxLauncher({ profilePath, proxy, timezoneId, maxWidth }) {
360
+ const isPersistent = !!profilePath;
361
+
362
+ // 1. Determine Path
363
+ const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
364
+ console.log(`🚀 Starting Firefox [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
365
+
366
+ const proxyObj = formatProxy(proxy);
367
+ const tz = timezoneId || undefined;
368
+
369
+ // Firefox specific preferences
370
+ const firefoxUserPrefs = {
371
+ "dom.webdriver.enabled": false,
372
+ useAutomationExtension: false,
373
+ "media.peerconnection.enabled": false,
374
+ };
375
+
376
+ // ==================================================================
377
+ // BRANCH A: TEMP PROFILE (launch + newInjectedContext)
378
+ // ==================================================================
379
+ if (!isPersistent) {
380
+ try {
381
+ const fpConfig = getFingerprintConfig("firefox", maxWidth);
382
+ const fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
383
+
384
+ const browser = await firefox.launch({
385
+ headless: false,
386
+ proxy: proxyObj,
387
+ ignoreDefaultArgs: ["--enable-automation"],
388
+ firefoxUserPrefs: firefoxUserPrefs,
389
+ });
390
+
391
+ const context = await newInjectedContext(browser, {
392
+ fingerprint: fingerprintData,
393
+ timezoneId: tz,
394
+ });
395
+
396
+ const page = context.pages()[0] || (await context.newPage());
397
+
398
+ // Pass 'activePath' to delete temp folder later
399
+ return createBrowserController(browser, context, page, activePath);
400
+ } catch (err) {
401
+ console.error("Firefox Temp Launch Error:", err);
402
+ throw err;
403
+ }
404
+ }
405
+ // ==================================================================
406
+ // BRANCH B: PERSISTENT PROFILE (launchPersistentContext + Manual Script)
407
+ // ==================================================================
408
+ else {
409
+ try {
410
+ if (!fs.existsSync(activePath)) fs.mkdirSync(activePath, { recursive: true });
411
+
412
+ const context = await firefox.launchPersistentContext(activePath, {
413
+ headless: false,
414
+ proxy: proxyObj,
415
+ timezoneId: tz,
416
+ ignoreDefaultArgs: ["--enable-automation"],
417
+ // firefoxUserPrefs: firefoxUserPrefs,
418
+ viewport: null,
419
+ });
420
+
421
+ // Manual Stealth Script
422
+ await addStealthScript(context);
423
+
424
+ const page = context.pages()[0];
425
+
426
+ return createBrowserController(context, context, page, null);
427
+ } catch (err) {
428
+ console.error("Firefox Persistent Launch Error:", err);
429
+ throw err;
430
+ }
431
+ }
432
+ }
433
+
434
+ // ==========================================================================
435
+ // 6. ENGINE: BRAVE
436
+ // ==========================================================================
437
+ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, maxWidth }) {
438
+ const isPersistent = !!profilePath;
439
+ const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
440
+
441
+ console.log(`🚀 Starting Brave: ${activePath}`);
442
+ fs.mkdirSync(activePath, { recursive: true });
443
+
444
+ let fingerprintData;
445
+ const fingerprintFilePath = path.join(activePath, "fingerprint.json");
446
+
447
+ if (fs.existsSync(fingerprintFilePath)) {
448
+ fingerprintData = JSON.parse(fs.readFileSync(fingerprintFilePath, "utf-8"));
449
+ } else {
450
+ const fpConfig = getFingerprintConfig("brave", maxWidth);
451
+ fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
452
+
453
+ if (isPersistent) {
454
+ fs.writeFileSync(fingerprintFilePath, JSON.stringify(fingerprintData, null, 2), "utf-8");
455
+ console.log("📝 Generated and saved new fingerprint for Brave");
456
+ }
457
+ }
458
+
459
+ const braveBin = getBinaryPath("brave");
460
+
461
+ const args = [
462
+ "--test-type",
463
+ // --- Stealth & Anti-Detection ---
464
+ "--disable-blink-features=AutomationControlled",
465
+ //"--disable-features=UserAgentClientHint",
466
+
467
+ // --- PREVENT EXTRA TABS (New Flags) ---
468
+ // "--no-startup-window",
469
+ "--homepage=about:blank",
470
+ "--disable-restore-session-state", // Prevents restoring old tabs
471
+
472
+ // --- Error Suppression ---
473
+ "--disable-session-crashed-bubble",
474
+ "--hide-crash-restore-bubble",
475
+
476
+ // --- Silence & Networking ---
477
+ "--disable-background-networking",
478
+ "--disable-background-timer-throttling",
479
+ "--disable-breakpad",
480
+ "--disable-crash-reporter",
481
+ "--disable-component-update",
482
+ "--disable-sync",
483
+ "--no-default-browser-check",
484
+ "--no-first-run",
485
+ "--disable-domain-reliability",
486
+ "--disable-client-side-phishing-detection",
487
+
488
+ // --- Brave Specific Silence ---
489
+ "--disable-features=Translate,BraveRewards,BraveWallet,BraveNews",
490
+ "--disable-infobars",
491
+ ];
492
+ const ignoreDefaultArgs = ["--enable-automation", "--no-sandbox"];
493
+ if (CapSolver) {
494
+ const extPath = path.resolve(`./utility/browser-fingerprint/CapSolver`);
495
+ args.push(`--load-extension=${extPath}`);
496
+ }
497
+
498
+ const proxyObj = formatProxy(proxy);
499
+ const tz = timezoneId || undefined;
500
+
501
+ const context = await chromium.launchPersistentContext(activePath, {
502
+ headless: false,
503
+ executablePath: braveBin,
504
+ proxy: proxyObj,
505
+ timezoneId: tz,
506
+ ignoreDefaultArgs: ignoreDefaultArgs,
507
+ args: args,
508
+ userAgent: fingerprintData.fingerprint.navigator.userAgent,
509
+ viewport: fingerprintData.fingerprint.screen,
510
+ locale: fingerprintData.fingerprint.navigator.language,
511
+ });
512
+
513
+ await new FingerprintInjector().attachFingerprintToPlaywright(context, fingerprintData);
514
+
515
+ // ======================================================
516
+ // 🛠️ FIX: Close Extra Tabs (Brave Dashboard / Restore)
517
+ // ======================================================
518
+ const pages = context.pages();
519
+ if (pages.length > 1) {
520
+ console.log(`🧹 Found ${pages.length} tabs open in Brave. Closing extras...`);
521
+ // Close all tabs except the last one (which is usually the fresh active one)
522
+ for (let i = 0; i < pages.length - 1; i++) {
523
+ await pages[i].close();
524
+ }
525
+ }
526
+ const page = context.pages()[0];
527
+
528
+ const dirToDelete = isPersistent ? null : activePath;
529
+ // Return the remaining open page (context.pages() might change, so we grab index 0 after cleanup)
530
+ return createBrowserController(context, context, page, dirToDelete);
531
+ }
532
+
533
+ // ==========================================================================
534
+ // 7. ENGINE: CAMOUFOX
535
+ // ==========================================================================
536
+ async function camoufoxLauncher({ profilePath, proxy, timezoneId, maxWidth, camoufox_options = {} }) {
537
+ const isPersistent = !!profilePath;
538
+ const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
539
+
540
+ console.log(`🚀 Starting camoufoxJs: ${activePath}`);
541
+ fs.mkdirSync(activePath, { recursive: true });
542
+
543
+ const proxyObj = formatProxy(proxy);
544
+ const tz = timezoneId || undefined;
545
+
546
+ // Check if GeoIP is enabled in options (Default to false if undefined)
547
+ const isGeoIpEnabled = camoufox_options.geoip === true;
548
+
549
+ let launchConfig;
550
+ const fingerprintFilePath = path.join(activePath, "fingerprint.json");
551
+
552
+ // 1. Load existing or Generate new config
553
+ if (fs.existsSync(fingerprintFilePath)) {
554
+ // Load the previously generated launch options
555
+ launchConfig = JSON.parse(fs.readFileSync(fingerprintFilePath, "utf-8"));
556
+
557
+ // IMPORTANT: For persistent profiles, if we are NOT using GeoIP (static behavior),
558
+ // we usually want to ensure the proxy for *this* run is applied, as it might have changed.
559
+ if (!isGeoIpEnabled && proxyObj) {
560
+ launchConfig.proxy = proxyObj;
561
+ }
562
+ } else {
563
+ function getRandomValidMajorVersion() {
564
+ // Valid Major Versions (135 - 145)
565
+ const validMajorVersions = [135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145];
566
+ const randomIndex = Math.floor(Math.random() * validMajorVersions.length);
567
+ return validMajorVersions[randomIndex];
568
+ }
569
+ const screenWidth = maxWidth ? (maxWidth <= 10 ? Math.round(maxWidth * 1920) : maxWidth) : 1920;
570
+
571
+ // Prepare the base options
572
+ const optionsGenerator = {
573
+ ff_version: camoufox_options.ff_version || getRandomValidMajorVersion(),
574
+ headless: camoufox_options.headless || false,
575
+ humanize: camoufox_options.humanize ?? true,
576
+ block_images: camoufox_options.block_images ?? false,
577
+ block_webrtc: camoufox_options.block_webrtc ?? false,
578
+ i_know_what_im_doing: true, // Suppress warnings if manual config intentional
579
+ geoip: isGeoIpEnabled,
580
+
581
+ // Apply the strictly validated OS
582
+ os: camoufox_options.os || detectedOs,
583
+
584
+ ...camoufox_options, // Spread user overrides
585
+ };
586
+
587
+ if (camoufox_options.screen) {
588
+ optionsGenerator.screen = camoufox_options.screen;
589
+ } else {
590
+ optionsGenerator.screen = {
591
+ maxWidth: screenWidth,
592
+ };
593
+ }
594
+ // If GeoIP is TRUE, pass proxy inside launchOptions
595
+ if (isGeoIpEnabled && proxyObj) {
596
+ optionsGenerator.proxy = proxyObj;
597
+ }
598
+
599
+ // If GeoIP is TRUE, pass proxy inside launchOptions
600
+ if (isGeoIpEnabled && proxyObj) {
601
+ optionsGenerator.proxy = proxyObj;
602
+ }
603
+
604
+ // Generate full launch config via camoufox-js
605
+ launchConfig = await launchOptions(optionsGenerator);
606
+
607
+ // Save persistent config
608
+ if (isPersistent) {
609
+ fs.writeFileSync(fingerprintFilePath, JSON.stringify(launchConfig, null, 2), "utf-8");
610
+ console.log("📝 Generated and saved new launch config for Camoufox");
611
+ }
612
+ }
613
+
614
+ // Post-Generation Adjustments for Playwright Connection
615
+ if (!isGeoIpEnabled && proxyObj) {
616
+ launchConfig.proxy = proxyObj;
617
+ }
618
+
619
+ if (!isGeoIpEnabled && tz) {
620
+ launchConfig.timezoneId = tz;
621
+ }
622
+
623
+ const context = await firefox.launchPersistentContext(activePath, {
624
+ ...launchConfig,
625
+ // Removed viewport: null, handled by Camoufox args
626
+ });
627
+
628
+ const dirToDelete = isPersistent ? null : activePath;
629
+ return createBrowserController(context, context, context.pages()[0], dirToDelete);
630
+ }
631
+
632
+ // ==========================================================================
633
+ // 8. ENGINE: MULTILOGIN
634
+ // ==========================================================================
635
+ async function multiloginLauncher({ proxy, multilogin_options = {} }) {
636
+ // Destructure defaults from multilogin_options
637
+ const {
638
+ profileId = null,
639
+ os_type = detectedOs,
640
+ canvas_noise = true,
641
+ media_masking = true,
642
+ audio_masking = true,
643
+ custom_screen = false,
644
+ } = multilogin_options;
645
+
646
+ if (profileId) {
647
+ return await launchExistingMultiloginProfile(profileId);
648
+ } else {
649
+ return await launchQuickMultiloginProfile({
650
+ os_type,
651
+ proxy,
652
+ canvas_noise,
653
+ media_masking,
654
+ audio_masking,
655
+ });
656
+ }
657
+ }
658
+
659
+ async function launchExistingMultiloginProfile(profileId) {
660
+ const startUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v2/profile/f/${MULTILOGIN_FOLDER_ID}/p/${profileId}/start?automation_type=playwright&headless_mode=false`;
661
+ let browser, context, page;
662
+
663
+ try {
664
+ const token = await getMultiloginToken();
665
+ let response = await fetch(startUrl, {
666
+ method: "GET",
667
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json", "X-Strict-Mode": "true" },
668
+ });
669
+
670
+ if (response.status === 400) {
671
+ const errorJson = await response.json().catch(() => null);
672
+ if (errorJson?.status?.error_code === "PROFILE_ALREADY_RUNNING") {
673
+ console.log(`⚠️ Profile ${profileId} is running. Restarting...`);
674
+ await stopMultiloginProfile(profileId);
675
+ await sleep(5000);
676
+ response = await fetch(startUrl, {
677
+ method: "GET",
678
+ headers: {
679
+ Authorization: `Bearer ${await getMultiloginToken()}`,
680
+ Accept: "application/json",
681
+ "X-Strict-Mode": "true",
682
+ },
683
+ });
684
+ } else {
685
+ throw new Error(`Multilogin 400 Error: ${JSON.stringify(errorJson)}`);
686
+ }
687
+ }
688
+
689
+ if (!response.ok) throw new Error(`Failed to start profile: ${await response.text()}`);
690
+
691
+ const data = await response.json();
692
+ const port = data.data.port;
693
+ console.log(`✅ Multilogin Profile ${profileId} started on port ${port}`);
694
+
695
+ browser = await chromium.connectOverCDP(`http://localhost:${port}`);
696
+ context = browser.contexts()[0];
697
+ page = context.pages()[0];
698
+
699
+ const closeBrowser = async () => {
700
+ if (browser) await browser.close().catch(() => {});
701
+ return await stopMultiloginProfile(profileId);
702
+ };
703
+
704
+ return { browser, context, page, isBrowserRunning: () => !!browser, closeBrowser, launchError: null };
705
+ } catch (error) {
706
+ console.error("Multilogin Launch Error:", error.message);
707
+ try {
708
+ await stopMultiloginProfile(profileId);
709
+ } catch (e) {}
710
+ return { data: null, error, closeBrowser: async () => {} };
711
+ }
712
+ }
713
+
714
+ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking }) {
715
+ const createUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v3/profile/quick`;
716
+ let browser, context, page, profileId;
717
+
718
+ const requestBody = {
719
+ browser_type: "mimic",
720
+ os_type,
721
+ automation: "playwright",
722
+ is_headless: false,
723
+ parameters: {
724
+ flags: {
725
+ audio_masking: audio_masking ? "mask" : "natural",
726
+ media_devices_masking: media_masking ? "mask" : "natural",
727
+ screen_masking: "natural",
728
+ canvas_noise: canvas_noise ? "mask" : "natural",
729
+ proxy_masking: proxy ? "custom" : "disabled",
730
+ },
731
+ },
732
+ quickProfilesCount: 1,
733
+ };
734
+
735
+ if (proxy) {
736
+ const pObj = formatProxy(proxy);
737
+ if (pObj) {
738
+ const u = new URL(pObj.server);
739
+ requestBody.parameters.proxy = {
740
+ type: "http",
741
+ host: u.hostname,
742
+ port: Number(u.port),
743
+ username: pObj.username || "",
744
+ password: pObj.password || "",
745
+ };
746
+ }
747
+ }
748
+
749
+ try {
750
+ const token = await getMultiloginToken();
751
+ const response = await fetch(createUrl, {
752
+ method: "POST",
753
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
754
+ body: JSON.stringify(requestBody),
755
+ });
756
+
757
+ if (!response.ok) throw new Error(await response.text());
758
+ const json = await response.json();
759
+
760
+ profileId = json.data.id;
761
+ const port = json.data.port;
762
+ console.log(`✅ Quick Profile Created: ${profileId} on port ${port}`);
763
+
764
+ browser = await chromium.connectOverCDP(`http://localhost:${port}`);
765
+ context = browser.contexts()[0];
766
+ page = context.pages()[0];
767
+
768
+ const closeBrowser = async () => {
769
+ if (browser) await browser.close().catch(() => {});
770
+ return await stopMultiloginProfile(profileId);
771
+ };
772
+
773
+ return { browser, context, page, isBrowserRunning: () => !!browser, closeBrowser, launchError: null };
774
+ } catch (error) {
775
+ console.error("Quick Profile Error:", error);
776
+ if (profileId) await stopMultiloginProfile(profileId);
777
+ return { data: null, error, closeBrowser: async () => {} };
778
+ }
779
+ }
780
+
781
+ async function stopMultiloginProfile(profileId) {
782
+ const stopUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v1/profile/stop/p/${profileId}`;
783
+ try {
784
+ const token = await getMultiloginToken();
785
+ const response = await fetch(stopUrl, {
786
+ method: "GET",
787
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
788
+ });
789
+
790
+ if (!response.ok) {
791
+ const txt = await response.text();
792
+ if (response.status === 500 && txt.includes("profile already stopped")) {
793
+ return { error: null };
794
+ }
795
+ throw new Error(txt);
796
+ }
797
+ console.log(`🛑 Profile ${profileId} stopped.`);
798
+ return { error: null };
799
+ } catch (err) {
800
+ console.error(`Error stopping ${profileId}:`, err);
801
+ return { error: err };
802
+ }
803
+ }
804
+ // ==========================================================================
805
+ // 10. SHARED UTILITIES
806
+ // ==========================================================================
807
+
808
+ async function addStealthScript(context) {
809
+ await context.addInitScript(() => {
810
+ // Keep these deletions, they are Playwright-specific bindings
811
+ delete window.__pwInitScripts;
812
+ // delete window.playwright;
813
+ delete window.__playwright__binding__;
814
+ delete window.__playwright__binding__controller__;
815
+
816
+ // Check if navigator.webdriver exists and is true (indicating automation)
817
+ if (navigator.webdriver) {
818
+ // console.log("33333333333333333333333 - Automation detected, patching to false");
819
+
820
+ // Define a new getter for the property on the prototype
821
+ // This overrides the native 'true' with 'false'
822
+ Object.defineProperty(Object.getPrototypeOf(navigator), "webdriver", {
823
+ get: () => false,
824
+ });
825
+ } else {
826
+ // If it's already false or undefined, we leave it alone to look natural
827
+ // console.log("Navigator.webdriver is already safe (" + navigator.webdriver + ")");
828
+ }
829
+ });
830
+ }
831
+ function formatProxy(proxy) {
832
+ if (!proxy) return undefined;
833
+ if (typeof proxy === "string") return { server: proxy };
834
+
835
+ const p = { server: `${proxy.type}://${proxy.host}:${proxy.port}` };
836
+ if (proxy.user) {
837
+ p.username = proxy.user;
838
+ p.password = proxy.pass;
839
+ }
840
+ return p;
841
+ }
842
+
843
+ function createBrowserController(browser, context, page, dirToDelete = null) {
844
+ const closeBrowser = async () => {
845
+ try {
846
+ console.log("🔒 Closing browser session...");
847
+ if (context) await context.close();
848
+ if (browser && typeof browser.close === "function" && browser !== context) {
849
+ try {
850
+ await browser.close();
851
+ } catch (e) {}
852
+ }
853
+ if (dirToDelete) {
854
+ if (dirToDelete.includes("persistent")) {
855
+ console.warn(`⚠️ Safety Block: Attempted to delete persistent profile: ${dirToDelete}`);
856
+ } else {
857
+ console.log(`🗑️ Deleting temp profile: ${dirToDelete}`);
858
+ await deleteDirectoryWithRetries(dirToDelete);
859
+ }
860
+ }
861
+ return true;
862
+ } catch (err) {
863
+ console.error("Error closing browser:", err);
864
+ return false;
865
+ }
866
+ };
867
+ return { browser, context, page, isBrowserRunning: () => !!context, closeBrowser, launchError: null };
868
+ }