arn-browser 0.1.6 → 0.1.8

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 (24) hide show
  1. package/bin/cli.js +43 -0
  2. package/bin/install.js +420 -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/{multilogin_token_manager.js → mlx_token.js} +32 -43
  7. package/src/utility/{launchBrowser.d.ts → playwright/pwLaunch.d.ts} +15 -7
  8. package/src/utility/{launchBrowser.js → playwright/pwLaunch.js} +61 -30
  9. package/src/{all_routes/routeWithSuperagent.d.ts → utility/playwright/routes/pwRoute.d.ts} +4 -4
  10. package/src/{all_routes/routeWithSuperagent.js → utility/playwright/routes/pwRoute.js} +2 -2
  11. package/src/utility/proxy-utility/proxy-chain.js +4 -3
  12. package/src/utility/proxy-utility/proxy-helper.js +1 -1
  13. package/src/utility/puppeteer/ppLaunch.d.ts +199 -0
  14. package/src/utility/puppeteer/ppLaunch.js +723 -0
  15. package/src/utility/puppeteer/routes/ppRoute.d.ts +64 -0
  16. package/src/utility/puppeteer/routes/ppRoute.js +326 -0
  17. /package/src/{human-cursor → utility/playwright/human-cursor}/HumanCursor.js +0 -0
  18. /package/src/{human-cursor → utility/playwright/human-cursor}/bezier.js +0 -0
  19. /package/src/{human-cursor → utility/playwright/human-cursor}/index.d.ts +0 -0
  20. /package/src/{human-cursor → utility/playwright/human-cursor}/index.js +0 -0
  21. /package/src/{human-cursor → utility/playwright/human-cursor}/randomizer.js +0 -0
  22. /package/src/{human-cursor → utility/playwright/human-cursor}/tweening.js +0 -0
  23. /package/src/utility/{playwright-helper.d.ts → playwright/playwright-helper.d.ts} +0 -0
  24. /package/src/utility/{playwright-helper.js → playwright/playwright-helper.js} +0 -0
@@ -0,0 +1,723 @@
1
+ /**
2
+ * @file launchPuppeteer.js
3
+ * @description Puppeteer CDP-only browser launcher.
4
+ * Supports: Chrome, Brave, and Multilogin (via CDP connect).
5
+ * Handles: Persistent profiles (with prefixes), temp profiles, and 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 { spawn } from "child_process";
13
+ import puppeteer from "puppeteer-core";
14
+ import { setTimeout as sleep } from "timers/promises";
15
+ import { FingerprintInjector } from "fingerprint-injector";
16
+ import { FingerprintGenerator } from "fingerprint-generator";
17
+
18
+ // Internal Utilities
19
+ import { getMultiloginToken } from "../mlx_token.js";
20
+ import { deleteDirectoryWithRetries } from "../deleteDirectory.js";
21
+
22
+ // ==========================================================================
23
+ // 1. CONFIGURATION & CONSTANTS
24
+ // ==========================================================================
25
+
26
+ const MULTILOGIN_LAUNCHER_URL = "https://launcher.mlx.yt:45001";
27
+ const MULTILOGIN_FOLDER_ID = "bad9e7e1-cfab-4c8d-bd19-91aa82929711";
28
+
29
+ const ARN_BROWSER_DIR = path.join(os.homedir(), ".arn-browser");
30
+ const BASE_PROFILE_DIR = path.join(ARN_BROWSER_DIR, "profiles", "pp");
31
+ const PERSISTENT_DIR = path.join(BASE_PROFILE_DIR, "persistent");
32
+ const TEMP_DIR = path.join(BASE_PROFILE_DIR, "temp");
33
+
34
+ if (!fs.existsSync(PERSISTENT_DIR)) fs.mkdirSync(PERSISTENT_DIR, { recursive: true });
35
+ if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
36
+
37
+ // Module-level log flags (set per-launch via launchPuppeteer options)
38
+ let _launchLogs = false;
39
+ let _cleanupLogs = false;
40
+
41
+ // Detect OS for fingerprint generation
42
+ const detectedOs = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
43
+
44
+ /**
45
+ * Helper to generate consistent fingerprint options for Puppeteer.
46
+ * @param {object|null} screenOptions - Optional screen constraints { minWidth, maxWidth, minHeight, maxHeight }
47
+ */
48
+ function getFingerprintConfig(screenOptions = null) {
49
+ const config = {
50
+ devices: ["desktop"],
51
+ operatingSystems: [detectedOs],
52
+ locales: ["en-US"],
53
+ browsers: [{ name: "chrome", minVersion: 141 }],
54
+ screen: {},
55
+ };
56
+
57
+ if (screenOptions && typeof screenOptions === 'object') {
58
+ if (screenOptions.minWidth) config.screen.minWidth = screenOptions.minWidth;
59
+ if (screenOptions.maxWidth) config.screen.maxWidth = screenOptions.maxWidth;
60
+ if (screenOptions.minHeight) config.screen.minHeight = screenOptions.minHeight;
61
+ if (screenOptions.maxHeight) config.screen.maxHeight = screenOptions.maxHeight;
62
+ }
63
+
64
+ return config;
65
+ }
66
+
67
+ // ==========================================================================
68
+ // 2. HELPER FUNCTIONS
69
+ // ==========================================================================
70
+
71
+ const PROFILE_META_FILE = "_profile_meta.json";
72
+
73
+ /**
74
+ * Resolves the persistent path with a browser-specific prefix.
75
+ */
76
+ function resolveProfilePath(nameOrPath, browserName) {
77
+ if (!nameOrPath) return null;
78
+
79
+ if (path.isAbsolute(nameOrPath)) return nameOrPath;
80
+
81
+ let prefix = browserName.toLowerCase();
82
+ if (prefix.includes("brave")) prefix = "brave";
83
+ else if (prefix.includes("chrome") || prefix.includes("chromium")) prefix = "chrome";
84
+
85
+ const folderName = `${prefix}_${nameOrPath}`;
86
+ return path.join(PERSISTENT_DIR, folderName);
87
+ }
88
+
89
+ /**
90
+ * Locates binaries for Chrome and Brave from the installed browsers.
91
+ * Uses ~/.arn-browser/browsers/{browser}/{executable}
92
+ */
93
+ function getBinaryPath(browserName) {
94
+ const isWindows = process.platform === "win32";
95
+ const browsersDir = path.join(ARN_BROWSER_DIR, "browsers");
96
+
97
+ const binaryMap = {
98
+ brave: isWindows
99
+ ? path.join(browsersDir, "brave", "brave.exe")
100
+ : path.join(browsersDir, "brave", "brave"),
101
+ chrome: isWindows
102
+ ? path.join(browsersDir, "chromium", "chrome.exe")
103
+ : path.join(browsersDir, "chromium", "chrome"),
104
+ };
105
+
106
+ const binaryPath = binaryMap[browserName];
107
+ if (!binaryPath) {
108
+ throw new Error(`░░░░░ [LaunchPuppeteer] Unknown browser: ${browserName}`);
109
+ }
110
+
111
+ if (!fs.existsSync(binaryPath)) {
112
+ throw new Error(
113
+ `░░░░░ [LaunchPuppeteer] ${browserName} binary not found at: ${binaryPath}\n` +
114
+ ` Run "node bin/cli.js install" to install browsers.`
115
+ );
116
+ }
117
+
118
+ return binaryPath;
119
+ }
120
+
121
+ /**
122
+ * Writes or updates the profile metadata file.
123
+ */
124
+ function writeProfileMeta(dirPath, type, cleanupMinutes) {
125
+ try {
126
+ const metaPath = path.join(dirPath, PROFILE_META_FILE);
127
+ const now = new Date().toISOString();
128
+
129
+ if (fs.existsSync(metaPath)) {
130
+ const existing = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
131
+ existing.last_launched_at = now;
132
+ fs.writeFileSync(metaPath, JSON.stringify(existing, null, 2), "utf-8");
133
+ } else {
134
+ const meta = {
135
+ type,
136
+ created_at: now,
137
+ last_launched_at: now,
138
+ cleanup_minutes: cleanupMinutes,
139
+ };
140
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
141
+ }
142
+ } catch (e) {
143
+ // Non-critical
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Scans profile directories and deletes expired profiles.
149
+ */
150
+ function cleanUpProfiles(skipPath) {
151
+ const now = Date.now();
152
+
153
+ function scanDir(dir) {
154
+ if (!fs.existsSync(dir)) return;
155
+ try {
156
+ const folders = fs.readdirSync(dir);
157
+ for (const folder of folders) {
158
+ const folderPath = path.join(dir, folder);
159
+ try {
160
+ const stats = fs.statSync(folderPath);
161
+ if (!stats.isDirectory()) continue;
162
+
163
+ if (skipPath && path.resolve(folderPath) === path.resolve(skipPath)) continue;
164
+
165
+ const metaPath = path.join(folderPath, PROFILE_META_FILE);
166
+ if (!fs.existsSync(metaPath)) {
167
+ if (dir === TEMP_DIR && now - stats.mtimeMs > 15 * 60 * 1000) {
168
+ if (_cleanupLogs) console.log(`░░░░░ [Cleanup] Deleting legacy temp profile: ${folder}`);
169
+ deleteDirectoryWithRetries(folderPath);
170
+ }
171
+ continue;
172
+ }
173
+
174
+ const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
175
+ const cleanupMs = meta.cleanup_minutes;
176
+
177
+ if (!cleanupMs || cleanupMs <= 0) continue;
178
+
179
+ const lastLaunch = new Date(meta.last_launched_at).getTime();
180
+ if (now - lastLaunch > cleanupMs * 60 * 1000) {
181
+ if (_cleanupLogs) console.log(`░░░░░ [Cleanup] Deleting expired ${meta.type} profile: ${folder} (last launched ${Math.round((now - lastLaunch) / 60000)} mins ago)`);
182
+ deleteDirectoryWithRetries(folderPath);
183
+ }
184
+ } catch (e) {
185
+ // Ignore per-folder errors
186
+ }
187
+ }
188
+ } catch (err) {
189
+ console.error("Cleanup scan error:", err);
190
+ }
191
+ }
192
+
193
+ if (_cleanupLogs) console.log("░░░░░ [Cleanup] Scanning for expired profiles...");
194
+ scanDir(PERSISTENT_DIR);
195
+ scanDir(TEMP_DIR);
196
+ }
197
+
198
+ /**
199
+ * Formats proxy into standard object.
200
+ */
201
+ function formatProxy(proxy) {
202
+ if (!proxy) return undefined;
203
+ if (typeof proxy === "string") return { server: proxy };
204
+
205
+ const p = { server: `${proxy.type || "http"}://${proxy.host}:${proxy.port}` };
206
+ if (proxy.user) {
207
+ p.username = proxy.user;
208
+ p.password = proxy.pass;
209
+ }
210
+ return p;
211
+ }
212
+
213
+ // ==========================================================================
214
+ // 3. MAIN LAUNCHER ENTRY POINT
215
+ // ==========================================================================
216
+
217
+ export async function ppLaunch({
218
+ // Browser selection
219
+ which_browser = "chrome",
220
+
221
+ // Path & Storage
222
+ profile_path = null,
223
+ cleanupMinutes = 0,
224
+
225
+ // Network
226
+ proxy = null,
227
+
228
+ // Extra browser CLI args
229
+ extraArgs = [],
230
+
231
+ // Spoof Fingerprint
232
+ // - false: No spoofing
233
+ // - true: Spoof with default screen
234
+ // - { minWidth?, maxWidth?, minHeight?, maxHeight? }: Spoof with screen constraints
235
+ spoof_fingerprint = false,
236
+
237
+ // Multilogin
238
+ multilogin_options = {},
239
+
240
+ // Logging
241
+ launch_logs = false,
242
+ cleanup_logs = false,
243
+ } = {}) {
244
+ try {
245
+ // Set module-level log flags
246
+ _launchLogs = launch_logs;
247
+ _cleanupLogs = cleanup_logs;
248
+
249
+ // Resolve path using the browser type to ensure prefixes
250
+ const fullPath = resolveProfilePath(profile_path, which_browser);
251
+
252
+ // Calculate effective cleanup minutes
253
+ const isPersistent = !!profile_path;
254
+ const effectiveCleanupMinutes = cleanupMinutes > 0
255
+ ? cleanupMinutes
256
+ : (isPersistent ? null : 15);
257
+
258
+ // Run cleanup scan
259
+ cleanUpProfiles(fullPath);
260
+
261
+ let result;
262
+
263
+ switch (which_browser) {
264
+ case "chrome":
265
+ case "chromium":
266
+ result = await chromeLauncher({
267
+ profilePath: fullPath,
268
+ proxy,
269
+ extraArgs,
270
+ spoof_fingerprint,
271
+ cleanupMinutes: effectiveCleanupMinutes,
272
+ });
273
+ break;
274
+ case "brave":
275
+ result = await braveLauncher({
276
+ profilePath: fullPath,
277
+ proxy,
278
+ extraArgs,
279
+ spoof_fingerprint,
280
+ cleanupMinutes: effectiveCleanupMinutes,
281
+ });
282
+ break;
283
+ case "multilogin":
284
+ result = await multiloginLauncher({
285
+ proxy,
286
+ multilogin_options,
287
+ });
288
+ break;
289
+ default:
290
+ throw new Error(`[LaunchPuppeteer] Unknown browser type: ${which_browser}. Supported: chrome, brave, multilogin`);
291
+ }
292
+
293
+ return result;
294
+ } catch (error) {
295
+ console.error("░░░░░ [LaunchPuppeteer] Critical Error:", error.message || error);
296
+ return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
297
+ }
298
+ }
299
+
300
+ // ==========================================================================
301
+ // 4. ENGINE: CHROME (CDP)
302
+ // ==========================================================================
303
+
304
+ async function chromeLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint, cleanupMinutes }) {
305
+ const isPersistent = !!profilePath;
306
+ const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
307
+
308
+ fs.mkdirSync(activePath, { recursive: true });
309
+ writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
310
+ if (_launchLogs) console.log(`░░░░░ Starting Chrome [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
311
+
312
+ const binaryPath = getBinaryPath("chrome");
313
+ return await spawnAndConnect({
314
+ binaryPath,
315
+ profilePath: activePath,
316
+ isPersistent,
317
+ proxy,
318
+ extraArgs,
319
+ spoof_fingerprint,
320
+ browserLabel: "Chrome",
321
+ });
322
+ }
323
+
324
+ // ==========================================================================
325
+ // 5. ENGINE: BRAVE (CDP)
326
+ // ==========================================================================
327
+
328
+ async function braveLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint, cleanupMinutes }) {
329
+ const isPersistent = !!profilePath;
330
+ const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
331
+
332
+ fs.mkdirSync(activePath, { recursive: true });
333
+ writeProfileMeta(activePath, isPersistent ? "persistent" : "temp", cleanupMinutes);
334
+ if (_launchLogs) console.log(`░░░░░ Starting Brave [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
335
+
336
+ // ======================================================
337
+ // Disable Brave Sidebar via Preferences
338
+ // ======================================================
339
+ const prefsFilePath = path.join(activePath, "Default", "Preferences");
340
+ const prefsDir = path.dirname(prefsFilePath);
341
+ if (!fs.existsSync(prefsDir)) fs.mkdirSync(prefsDir, { recursive: true });
342
+
343
+ try {
344
+ let prefs = {};
345
+ if (fs.existsSync(prefsFilePath)) {
346
+ prefs = JSON.parse(fs.readFileSync(prefsFilePath, "utf-8"));
347
+ }
348
+ if (!prefs.brave) prefs.brave = {};
349
+ if (!prefs.brave.sidebar) prefs.brave.sidebar = {};
350
+ prefs.brave.sidebar.sidebar_show_option = 3;
351
+ fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
352
+ } catch (e) {
353
+ console.warn("░░░░░ Could not modify Brave preferences:", e.message);
354
+ }
355
+
356
+ const binaryPath = getBinaryPath("brave");
357
+ const braveArgs = [
358
+ "--disable-features=Translate,BraveRewards,BraveWallet,BraveNews,Sidebar,SidePanel",
359
+ "--homepage=about:blank",
360
+ ];
361
+
362
+ return await spawnAndConnect({
363
+ binaryPath,
364
+ profilePath: activePath,
365
+ isPersistent,
366
+ proxy,
367
+ extraArgs: [...braveArgs, ...extraArgs],
368
+ spoof_fingerprint,
369
+ browserLabel: "Brave",
370
+ });
371
+ }
372
+
373
+ // ==========================================================================
374
+ // 6. CDP SPAWN & CONNECT (Shared between Chrome & Brave)
375
+ // ==========================================================================
376
+
377
+ async function spawnAndConnect({ binaryPath, profilePath, isPersistent, proxy, extraArgs = [], spoof_fingerprint = false, browserLabel = "Browser" }) {
378
+ let browser;
379
+ let closing = false;
380
+ let signalHandler;
381
+
382
+ // Random 5-digit port (10000–65535)
383
+ const debugPort = Math.floor(Math.random() * 55535) + 10000;
384
+
385
+ const proxyObj = formatProxy(proxy);
386
+
387
+ const args = [
388
+ `--remote-debugging-port=${debugPort}`,
389
+ `--user-data-dir=${profilePath}`,
390
+ "--no-first-run",
391
+ "--no-default-browser-check",
392
+ "--start-maximized",
393
+
394
+ // --- Stealth & Anti-Detection ---
395
+ "--test-type",
396
+ "--disable-blink-features=AutomationControlled",
397
+
398
+ // --- PREVENT EXTRA TABS ---
399
+ "--disable-restore-session-state",
400
+
401
+ // --- Error Suppression ---
402
+ "--disable-session-crashed-bubble",
403
+ "--hide-crash-restore-bubble",
404
+
405
+ // --- Silence & Networking ---
406
+ "--disable-background-networking",
407
+ "--disable-background-timer-throttling",
408
+ "--disable-breakpad",
409
+ "--disable-crash-reporter",
410
+ "--disable-component-update",
411
+ "--disable-sync",
412
+ "--disable-domain-reliability",
413
+ "--disable-client-side-phishing-detection",
414
+ "--disable-infobars",
415
+
416
+ ...extraArgs,
417
+ ];
418
+
419
+ if (proxyObj) {
420
+ args.push(`--proxy-server=${proxyObj.server}`);
421
+ }
422
+
423
+ // Spawn browser
424
+ const chrome = spawn(binaryPath, args, {
425
+ stdio: "ignore",
426
+ detached: true,
427
+ });
428
+
429
+ // Unref so Node.js can exit independently
430
+ chrome.unref();
431
+
432
+ // Cleanup function
433
+ const cleanup = async () => {
434
+ if (closing) return false;
435
+ closing = true;
436
+
437
+ if (signalHandler) {
438
+ process.off("SIGINT", signalHandler);
439
+ process.off("SIGTERM", signalHandler);
440
+ process.off("SIGHUP", signalHandler);
441
+ }
442
+
443
+ try {
444
+ if (browser?.connected) await browser.close();
445
+ } catch { }
446
+
447
+ try {
448
+ if (chrome.pid) process.kill(-chrome.pid, "SIGTERM");
449
+ } catch { }
450
+
451
+ // Delete temp profile
452
+ if (!isPersistent && profilePath) {
453
+ try {
454
+ if (_launchLogs) console.log(`░░░░░ Deleting temp profile: ${profilePath}`);
455
+ await deleteDirectoryWithRetries(profilePath);
456
+ } catch { }
457
+ }
458
+
459
+ return true;
460
+ };
461
+
462
+ // Register signal handlers
463
+ signalHandler = async () => {
464
+ await cleanup();
465
+ process.exit(0);
466
+ };
467
+ process.once("SIGINT", signalHandler);
468
+ process.once("SIGTERM", signalHandler);
469
+ process.once("SIGHUP", signalHandler);
470
+
471
+ if (_launchLogs) console.log(`░░░░░ ${browserLabel} started on port ${debugPort}`);
472
+ await sleep(2000);
473
+
474
+ // Connect via CDP
475
+ try {
476
+ browser = await puppeteer.connect({
477
+ browserURL: `http://127.0.0.1:${debugPort}`,
478
+ defaultViewport: null,
479
+ });
480
+
481
+ if (_launchLogs) console.log(`░░░░░ Connected to ${browserLabel} via CDP (port ${debugPort})`);
482
+
483
+ // Get page — apply fingerprint injection if spoof_fingerprint is enabled
484
+ const pages = await browser.pages();
485
+ const page = pages[0] ?? (await browser.newPage());
486
+
487
+ // Fingerprint injection logic:
488
+ // 1. If persistent profile has a saved fingerprint.json → ALWAYS use it (even if spoof_fingerprint is false)
489
+ // 2. If spoof_fingerprint is truthy → generate new fingerprint (save it for persistent profiles)
490
+ // 3. Otherwise → no fingerprint injection
491
+ const fingerprintFilePath = path.join(profilePath, "fingerprint.json");
492
+ const hasSavedFingerprint = isPersistent && fs.existsSync(fingerprintFilePath);
493
+
494
+ if (hasSavedFingerprint || spoof_fingerprint) {
495
+ let fingerprintData;
496
+
497
+ if (hasSavedFingerprint) {
498
+ // Persistent: reuse saved fingerprint (regardless of spoof_fingerprint setting)
499
+ fingerprintData = JSON.parse(fs.readFileSync(fingerprintFilePath, "utf-8"));
500
+ if (_launchLogs) console.log(`░░░░░ Loaded saved fingerprint for ${browserLabel}`);
501
+ } else {
502
+ // Generate new fingerprint
503
+ const screenOptions = (typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
504
+ const fpConfig = getFingerprintConfig(screenOptions);
505
+ fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
506
+
507
+ // Save for persistent profiles
508
+ if (isPersistent) {
509
+ fs.writeFileSync(fingerprintFilePath, JSON.stringify(fingerprintData, null, 2), "utf-8");
510
+ if (_launchLogs) console.log(`░░░░░ Generated and saved new fingerprint for ${browserLabel}`);
511
+ }
512
+ }
513
+
514
+ // Inject onto existing page
515
+ const injector = new FingerprintInjector();
516
+ await injector.attachFingerprintToPuppeteer(page, fingerprintData);
517
+
518
+ if (_launchLogs) console.log(`░░░░░ Fingerprint injected into ${browserLabel} page`);
519
+ }
520
+
521
+ return {
522
+ browser,
523
+ context: browser,
524
+ page,
525
+ isBrowserRunning: () => !!browser?.connected,
526
+ closeBrowser: cleanup,
527
+ launchError: null,
528
+ };
529
+ } catch (error) {
530
+ console.error(`░░░░░ Failed to connect to ${browserLabel} on port ${debugPort}:`, error.message);
531
+ await cleanup();
532
+ return {
533
+ browser: null,
534
+ context: null,
535
+ page: null,
536
+ isBrowserRunning: () => false,
537
+ closeBrowser: async () => false,
538
+ launchError: error,
539
+ };
540
+ }
541
+ }
542
+
543
+ // ==========================================================================
544
+ // 7. ENGINE: MULTILOGIN (CDP via Puppeteer)
545
+ // ==========================================================================
546
+
547
+ async function multiloginLauncher({ proxy, multilogin_options = {} }) {
548
+ const {
549
+ profileId = null,
550
+ os_type = (process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux"),
551
+ canvas_noise = true,
552
+ media_masking = true,
553
+ audio_masking = true,
554
+ } = multilogin_options;
555
+
556
+ if (profileId) {
557
+ return await launchExistingMultiloginProfile(profileId);
558
+ } else {
559
+ return await launchQuickMultiloginProfile({
560
+ os_type,
561
+ proxy,
562
+ canvas_noise,
563
+ media_masking,
564
+ audio_masking,
565
+ });
566
+ }
567
+ }
568
+
569
+ async function launchExistingMultiloginProfile(profileId) {
570
+ const startUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v2/profile/f/${MULTILOGIN_FOLDER_ID}/p/${profileId}/start?automation_type=playwright&headless_mode=false`;
571
+ let browser;
572
+
573
+ try {
574
+ const token = await getMultiloginToken();
575
+ let response = await fetch(startUrl, {
576
+ method: "GET",
577
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json", "X-Strict-Mode": "true" },
578
+ });
579
+
580
+ if (response.status === 400) {
581
+ const errorJson = await response.json().catch(() => null);
582
+ if (errorJson?.status?.error_code === "PROFILE_ALREADY_RUNNING") {
583
+ if (_launchLogs) console.log(`░░░░░ Profile ${profileId} is running. Restarting...`);
584
+ await stopMultiloginProfile(profileId);
585
+ await sleep(5000);
586
+ response = await fetch(startUrl, {
587
+ method: "GET",
588
+ headers: {
589
+ Authorization: `Bearer ${await getMultiloginToken()}`,
590
+ Accept: "application/json",
591
+ "X-Strict-Mode": "true",
592
+ },
593
+ });
594
+ } else {
595
+ throw new Error(`Multilogin 400 Error: ${JSON.stringify(errorJson)}`);
596
+ }
597
+ }
598
+
599
+ if (!response.ok) throw new Error(`Failed to start profile: ${await response.text()}`);
600
+
601
+ const data = await response.json();
602
+ const port = data.data.port;
603
+ if (_launchLogs) console.log(`░░░░░ Multilogin Profile ${profileId} started on port ${port}`);
604
+
605
+ browser = await puppeteer.connect({
606
+ browserURL: `http://127.0.0.1:${port}`,
607
+ defaultViewport: null,
608
+ });
609
+
610
+ const pages = await browser.pages();
611
+ const page = pages[0] ?? (await browser.newPage());
612
+
613
+ const closeBrowser = async () => {
614
+ try {
615
+ if (browser?.connected) await browser.close();
616
+ } catch { }
617
+ return await stopMultiloginProfile(profileId);
618
+ };
619
+
620
+ return { browser, context: browser, page, isBrowserRunning: () => !!browser?.connected, closeBrowser, launchError: null };
621
+ } catch (error) {
622
+ console.error("Multilogin Launch Error:", error.message);
623
+ try { await stopMultiloginProfile(profileId); } catch { }
624
+ return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
625
+ }
626
+ }
627
+
628
+ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking }) {
629
+ const createUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v3/profile/quick`;
630
+ let browser, profileId;
631
+
632
+ const requestBody = {
633
+ browser_type: "mimic",
634
+ os_type,
635
+ automation: "playwright",
636
+ is_headless: false,
637
+ parameters: {
638
+ flags: {
639
+ audio_masking: audio_masking ? "mask" : "natural",
640
+ media_devices_masking: media_masking ? "mask" : "natural",
641
+ screen_masking: "natural",
642
+ canvas_noise: canvas_noise ? "mask" : "natural",
643
+ proxy_masking: proxy ? "custom" : "disabled",
644
+ },
645
+ },
646
+ quickProfilesCount: 1,
647
+ };
648
+
649
+ if (proxy) {
650
+ const pObj = formatProxy(proxy);
651
+ if (pObj) {
652
+ const u = new URL(pObj.server);
653
+ requestBody.parameters.proxy = {
654
+ type: "http",
655
+ host: u.hostname,
656
+ port: Number(u.port),
657
+ username: pObj.username || "",
658
+ password: pObj.password || "",
659
+ };
660
+ }
661
+ }
662
+
663
+ try {
664
+ const token = await getMultiloginToken();
665
+ const response = await fetch(createUrl, {
666
+ method: "POST",
667
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
668
+ body: JSON.stringify(requestBody),
669
+ });
670
+
671
+ if (!response.ok) throw new Error(await response.text());
672
+ const json = await response.json();
673
+
674
+ profileId = json.data.id;
675
+ const port = json.data.port;
676
+ if (_launchLogs) console.log(`░░░░░ Quick Profile Created: ${profileId} on port ${port}`);
677
+
678
+ browser = await puppeteer.connect({
679
+ browserURL: `http://127.0.0.1:${port}`,
680
+ defaultViewport: null,
681
+ });
682
+
683
+ const pages = await browser.pages();
684
+ const page = pages[0] ?? (await browser.newPage());
685
+
686
+ const closeBrowser = async () => {
687
+ try {
688
+ if (browser?.connected) await browser.close();
689
+ } catch { }
690
+ return await stopMultiloginProfile(profileId);
691
+ };
692
+
693
+ return { browser, context: browser, page, isBrowserRunning: () => !!browser?.connected, closeBrowser, launchError: null };
694
+ } catch (error) {
695
+ console.error("Quick Profile Error:", error);
696
+ if (profileId) await stopMultiloginProfile(profileId);
697
+ return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
698
+ }
699
+ }
700
+
701
+ async function stopMultiloginProfile(profileId) {
702
+ const stopUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v1/profile/stop/p/${profileId}`;
703
+ try {
704
+ const token = await getMultiloginToken();
705
+ const response = await fetch(stopUrl, {
706
+ method: "GET",
707
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
708
+ });
709
+
710
+ if (!response.ok) {
711
+ const txt = await response.text();
712
+ if (response.status === 500 && txt.includes("profile already stopped")) {
713
+ return { error: null };
714
+ }
715
+ throw new Error(txt);
716
+ }
717
+ if (_launchLogs) console.log(`░░░░░ Profile ${profileId} stopped.`);
718
+ return { error: null };
719
+ } catch (err) {
720
+ console.error(`Error stopping ${profileId}:`, err);
721
+ return { error: err };
722
+ }
723
+ }