arn-browser 0.0.3 → 0.0.5

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