arn-browser 0.1.3 → 0.1.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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arn-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A lightweight, browser autmation helper.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"@aws-sdk/client-ec2": "^3.953.0",
|
|
15
15
|
"@ghostery/adblocker": "^2.13.0",
|
|
16
16
|
"arn-knexjs": "^0.0.3",
|
|
17
|
-
"camoufox-js": "^0.
|
|
17
|
+
"camoufox-js": "^0.9.1",
|
|
18
18
|
"dotenv": "^17.2.3",
|
|
19
19
|
"fingerprint-generator": "^2.1.78",
|
|
20
20
|
"fingerprint-injector": "^2.1.78",
|
|
@@ -31,15 +31,14 @@ export async function deleteDirectoryWithRetries(targetPath, maxRetries = 5, ret
|
|
|
31
31
|
|
|
32
32
|
// If it's the last attempt, log error
|
|
33
33
|
if (isLastAttempt) {
|
|
34
|
-
console.error(
|
|
34
|
+
console.error(`░░ Failed to delete directory after ${maxRetries} attempts: ${targetPath}`);
|
|
35
35
|
console.error(` Error: ${error.message}`);
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Log warning and wait
|
|
40
40
|
console.warn(
|
|
41
|
-
|
|
42
|
-
retryDelayMs / 1000
|
|
41
|
+
`░░ Delete failed (Attempt ${attempt + 1}/${maxRetries}). File might be locked. Retrying in ${retryDelayMs / 1000
|
|
43
42
|
}s...`
|
|
44
43
|
);
|
|
45
44
|
await sleep(retryDelayMs);
|
|
@@ -150,16 +150,24 @@ export interface LaunchOptions {
|
|
|
150
150
|
// ========================================================================
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
|
-
* Path to a
|
|
153
|
+
* Path to a browser profile.
|
|
154
154
|
* - If absolute path: Uses that path exactly (no prefix added).
|
|
155
|
-
* - If folder name: Creates inside
|
|
156
|
-
* - If null: Creates a temporary, random profile
|
|
155
|
+
* - If folder name: Creates inside `~/.arn-browser/browser_profiles/` with a browser prefix (e.g., `brave_myProfile`).
|
|
156
|
+
* - If null: Creates a temporary, random profile that is deleted on browser close.
|
|
157
|
+
*
|
|
158
|
+
* When combined with `cleanupMinutes`, named profiles can be auto-deleted
|
|
159
|
+
* after a period of inactivity (based on `_profile_meta.json`).
|
|
157
160
|
*/
|
|
158
161
|
profile_path?: string | null;
|
|
159
162
|
|
|
160
163
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
164
|
+
* Auto-delete schedule for named profiles (in minutes).
|
|
165
|
+
* - `0` or not set: Profile is permanent (never auto-deleted).
|
|
166
|
+
* - `> 0`: Profile is auto-deleted after this many minutes of inactivity.
|
|
167
|
+
* The inactivity timer resets on each launch.
|
|
168
|
+
* - Only applies to named profiles (with `profile_path`).
|
|
169
|
+
* Unnamed profiles are always deleted on close regardless.
|
|
170
|
+
* Default: 0
|
|
163
171
|
*/
|
|
164
172
|
cleanupMinutes?: number;
|
|
165
173
|
|
|
@@ -222,6 +230,22 @@ export interface LaunchOptions {
|
|
|
222
230
|
* Camoufox uses its own humanize option in camoufox_options.
|
|
223
231
|
*/
|
|
224
232
|
humanize_options?: HumanizeOptions;
|
|
233
|
+
|
|
234
|
+
// ========================================================================
|
|
235
|
+
// 7. LOGGING
|
|
236
|
+
// ========================================================================
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Enable launch-related console logs (browser start, fingerprint, tab cleanup, close).
|
|
240
|
+
* Default: false
|
|
241
|
+
*/
|
|
242
|
+
launch_logs?: boolean;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Enable cleanup-related console logs (expired profile deletion).
|
|
246
|
+
* Default: false
|
|
247
|
+
*/
|
|
248
|
+
cleanup_logs?: boolean;
|
|
225
249
|
}
|
|
226
250
|
|
|
227
251
|
/**
|
|
@@ -40,13 +40,8 @@ const detectedOs = osMap[process.platform] || "windows";
|
|
|
40
40
|
const MULTILOGIN_LAUNCHER_URL = "https://launcher.mlx.yt:45001";
|
|
41
41
|
const MULTILOGIN_FOLDER_ID = "bad9e7e1-cfab-4c8d-bd19-91aa82929711";
|
|
42
42
|
|
|
43
|
-
const
|
|
44
|
-
|
|
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 });
|
|
43
|
+
const BASE_PROFILE_DIR = path.join(os.homedir(), ".arn-browser", "browser_profiles");
|
|
44
|
+
if (!fs.existsSync(BASE_PROFILE_DIR)) fs.mkdirSync(BASE_PROFILE_DIR, { recursive: true });
|
|
50
45
|
|
|
51
46
|
// ==========================================================================
|
|
52
47
|
// 2. HELPER FUNCTIONS
|
|
@@ -67,7 +62,7 @@ function resolveProfilePath(nameOrPath, browserName) {
|
|
|
67
62
|
else if (prefix.includes("camoufox")) prefix = "camoufox";
|
|
68
63
|
|
|
69
64
|
const folderName = `${prefix}_${nameOrPath}`;
|
|
70
|
-
return path.join(
|
|
65
|
+
return path.join(BASE_PROFILE_DIR, folderName);
|
|
71
66
|
}
|
|
72
67
|
|
|
73
68
|
/**
|
|
@@ -101,7 +96,7 @@ function getBinaryPath(browserName) {
|
|
|
101
96
|
|
|
102
97
|
if (!binaryPath || !fs.existsSync(binaryPath)) {
|
|
103
98
|
throw new Error(
|
|
104
|
-
|
|
99
|
+
`░░ [LaunchBrowser] Binary not found for ${browserName} at: ${binaryPath}\n` +
|
|
105
100
|
` Linux checked: ~/.cache/brave/brave AND ~/Downloads/brave/brave`
|
|
106
101
|
);
|
|
107
102
|
}
|
|
@@ -109,27 +104,58 @@ function getBinaryPath(browserName) {
|
|
|
109
104
|
return binaryPath;
|
|
110
105
|
}
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Write or update _profile_meta.json inside a profile directory.
|
|
109
|
+
* Tracks creation time, last use, and auto-deletion schedule.
|
|
110
|
+
*/
|
|
111
|
+
function writeProfileMeta(profileDir, deleteAfterMinutes) {
|
|
112
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
113
|
+
const metaPath = path.join(profileDir, "_profile_meta.json");
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
let meta = {};
|
|
116
|
+
if (fs.existsSync(metaPath)) {
|
|
117
|
+
try { meta = JSON.parse(fs.readFileSync(metaPath, "utf-8")); } catch (e) { meta = {}; }
|
|
118
|
+
} else {
|
|
119
|
+
meta.created_at = new Date().toISOString();
|
|
120
|
+
}
|
|
118
121
|
|
|
122
|
+
meta.last_used_at = new Date().toISOString();
|
|
123
|
+
meta.delete_after_minutes = deleteAfterMinutes || null;
|
|
124
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), "utf-8");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Scan all profiles in BASE_PROFILE_DIR and delete expired ones
|
|
129
|
+
* based on their _profile_meta.json metadata.
|
|
130
|
+
*/
|
|
131
|
+
function cleanUpProfiles(cleanup_logs = false) {
|
|
132
|
+
if (!fs.existsSync(BASE_PROFILE_DIR)) return;
|
|
133
|
+
|
|
134
|
+
const now = Date.now();
|
|
119
135
|
try {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
const
|
|
136
|
+
const entries = fs.readdirSync(BASE_PROFILE_DIR);
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const profileDir = path.join(BASE_PROFILE_DIR, entry);
|
|
123
139
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
if (!fs.statSync(profileDir).isDirectory()) continue;
|
|
141
|
+
|
|
142
|
+
const metaPath = path.join(profileDir, "_profile_meta.json");
|
|
143
|
+
if (!fs.existsSync(metaPath)) continue; // No metadata = skip
|
|
144
|
+
|
|
145
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
146
|
+
if (!meta.delete_after_minutes || meta.delete_after_minutes <= 0) continue;
|
|
147
|
+
|
|
148
|
+
const lastUsed = new Date(meta.last_used_at).getTime();
|
|
149
|
+
const expiresAt = lastUsed + meta.delete_after_minutes * 60 * 1000;
|
|
150
|
+
|
|
151
|
+
if (now > expiresAt) {
|
|
152
|
+
if (cleanup_logs) console.log(`░░ [Cleanup] Deleting expired profile: ${entry}`);
|
|
153
|
+
deleteDirectoryWithRetries(profileDir);
|
|
128
154
|
}
|
|
129
155
|
} catch (e) {
|
|
130
|
-
// Ignore
|
|
156
|
+
// Ignore errors for individual profiles
|
|
131
157
|
}
|
|
132
|
-
}
|
|
158
|
+
}
|
|
133
159
|
} catch (err) {
|
|
134
160
|
console.error("Cleanup Error:", err);
|
|
135
161
|
}
|
|
@@ -196,13 +222,29 @@ export async function launchBrowser({
|
|
|
196
222
|
camoufox_options = {}, // { geoip, humanize, ... }
|
|
197
223
|
multilogin_options = {}, // { profileId, os_type, canvas_noise, ... }
|
|
198
224
|
|
|
225
|
+
// Logging
|
|
226
|
+
launch_logs = false,
|
|
227
|
+
cleanup_logs = false,
|
|
228
|
+
|
|
199
229
|
}) {
|
|
200
230
|
try {
|
|
201
231
|
if (custom_profile_path) throw new Error("Please use profile_path");
|
|
202
232
|
|
|
203
|
-
//
|
|
204
|
-
const
|
|
205
|
-
|
|
233
|
+
// ====== Centralized Path Resolution ======
|
|
234
|
+
const isNamed = !!profile_path;
|
|
235
|
+
const resolvedPath = resolveProfilePath(profile_path, which_browser);
|
|
236
|
+
const activePath = resolvedPath || path.join(BASE_PROFILE_DIR, crypto.randomUUID());
|
|
237
|
+
|
|
238
|
+
// Write/update metadata for all profiles
|
|
239
|
+
// For unnamed profiles: use provided cleanupMinutes, or default to 15 min
|
|
240
|
+
// (safety net — if process crashes before closeBrowser, cleanup catches it)
|
|
241
|
+
writeProfileMeta(activePath, isNamed ? cleanupMinutes : (cleanupMinutes || 15));
|
|
242
|
+
|
|
243
|
+
// Run cleanup (scans all profiles, deletes expired ones)
|
|
244
|
+
cleanUpProfiles(cleanup_logs);
|
|
245
|
+
|
|
246
|
+
// Only delete on close for unnamed (throwaway) profiles
|
|
247
|
+
const dirToDelete = isNamed ? null : activePath;
|
|
206
248
|
|
|
207
249
|
let browserInstance;
|
|
208
250
|
|
|
@@ -217,7 +259,10 @@ export async function launchBrowser({
|
|
|
217
259
|
case "chromium":
|
|
218
260
|
case "chrome":
|
|
219
261
|
browserInstance = await chromiumLauncher({
|
|
220
|
-
|
|
262
|
+
activePath,
|
|
263
|
+
isPersistent: isNamed,
|
|
264
|
+
dirToDelete,
|
|
265
|
+
launch_logs,
|
|
221
266
|
proxy,
|
|
222
267
|
timezoneId,
|
|
223
268
|
CapSolver,
|
|
@@ -227,7 +272,10 @@ export async function launchBrowser({
|
|
|
227
272
|
break;
|
|
228
273
|
case "firefox":
|
|
229
274
|
browserInstance = await firefoxLauncher({
|
|
230
|
-
|
|
275
|
+
activePath,
|
|
276
|
+
isPersistent: isNamed,
|
|
277
|
+
dirToDelete,
|
|
278
|
+
launch_logs,
|
|
231
279
|
proxy,
|
|
232
280
|
timezoneId,
|
|
233
281
|
humanize_options: effectiveHumanizeOptions,
|
|
@@ -237,7 +285,10 @@ export async function launchBrowser({
|
|
|
237
285
|
case "brave":
|
|
238
286
|
case "braveLauncher":
|
|
239
287
|
browserInstance = await braveLauncher({
|
|
240
|
-
|
|
288
|
+
activePath,
|
|
289
|
+
isPersistent: isNamed,
|
|
290
|
+
dirToDelete,
|
|
291
|
+
launch_logs,
|
|
241
292
|
proxy,
|
|
242
293
|
timezoneId,
|
|
243
294
|
CapSolver,
|
|
@@ -248,7 +299,10 @@ export async function launchBrowser({
|
|
|
248
299
|
case "camoufox":
|
|
249
300
|
// Camoufox already has its own humanize in camoufox_options
|
|
250
301
|
browserInstance = await camoufoxLauncher({
|
|
251
|
-
|
|
302
|
+
activePath,
|
|
303
|
+
isPersistent: isNamed,
|
|
304
|
+
dirToDelete,
|
|
305
|
+
launch_logs,
|
|
252
306
|
proxy,
|
|
253
307
|
timezoneId,
|
|
254
308
|
camoufox_options,
|
|
@@ -260,6 +314,7 @@ export async function launchBrowser({
|
|
|
260
314
|
proxy,
|
|
261
315
|
multilogin_options,
|
|
262
316
|
humanize_options: effectiveHumanizeOptions,
|
|
317
|
+
launch_logs,
|
|
263
318
|
// Spoof Fingerprint not needed for Multilogin
|
|
264
319
|
});
|
|
265
320
|
break;
|
|
@@ -269,7 +324,7 @@ export async function launchBrowser({
|
|
|
269
324
|
|
|
270
325
|
return browserInstance;
|
|
271
326
|
} catch (error) {
|
|
272
|
-
console.error("
|
|
327
|
+
console.error("░░ [LaunchBrowser] Critical Error:", error.message || error);
|
|
273
328
|
return { browser: null, context: null, page: null, isBrowserRunning: () => false, closeBrowser: async () => false, launchError: error };
|
|
274
329
|
}
|
|
275
330
|
}
|
|
@@ -277,12 +332,9 @@ export async function launchBrowser({
|
|
|
277
332
|
// ==========================================================================
|
|
278
333
|
// 4. ENGINE: CHROMIUM
|
|
279
334
|
// ==========================================================================
|
|
280
|
-
async function chromiumLauncher({
|
|
281
|
-
const isPersistent = !!profilePath;
|
|
335
|
+
async function chromiumLauncher({ activePath, isPersistent, dirToDelete, launch_logs, proxy, timezoneId, CapSolver, humanize_options, spoof_fingerprint }) {
|
|
282
336
|
|
|
283
|
-
|
|
284
|
-
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
285
|
-
console.log(`🚀 Starting Chromium [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
|
|
337
|
+
if (launch_logs) console.log(`░░ Starting Chromium [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
|
|
286
338
|
|
|
287
339
|
// 2. Define Args
|
|
288
340
|
const args = [
|
|
@@ -362,8 +414,8 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
|
|
|
362
414
|
|
|
363
415
|
const page = context.pages()[0] || (await context.newPage());
|
|
364
416
|
|
|
365
|
-
// Pass
|
|
366
|
-
return createBrowserController(browser, context, page,
|
|
417
|
+
// Pass dirToDelete so the temp folder gets deleted on close
|
|
418
|
+
return createBrowserController(browser, context, page, dirToDelete, humanize_options, launch_logs);
|
|
367
419
|
} catch (err) {
|
|
368
420
|
console.error("Chromium Temp Launch Error:", err);
|
|
369
421
|
throw err;
|
|
@@ -392,7 +444,7 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
|
|
|
392
444
|
|
|
393
445
|
const page = context.pages()[0];
|
|
394
446
|
|
|
395
|
-
return createBrowserController(context, context, page,
|
|
447
|
+
return createBrowserController(context, context, page, dirToDelete, humanize_options, launch_logs);
|
|
396
448
|
} catch (err) {
|
|
397
449
|
console.error("Chromium Persistent Launch Error:", err);
|
|
398
450
|
throw err;
|
|
@@ -403,12 +455,9 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
|
|
|
403
455
|
// ==========================================================================
|
|
404
456
|
// 5. ENGINE: FIREFOX
|
|
405
457
|
// ==========================================================================
|
|
406
|
-
async function firefoxLauncher({
|
|
407
|
-
const isPersistent = !!profilePath;
|
|
458
|
+
async function firefoxLauncher({ activePath, isPersistent, dirToDelete, launch_logs, proxy, timezoneId, humanize_options, spoof_fingerprint }) {
|
|
408
459
|
|
|
409
|
-
|
|
410
|
-
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
411
|
-
console.log(`🚀 Starting Firefox [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
|
|
460
|
+
if (launch_logs) console.log(`░░ Starting Firefox [${isPersistent ? "Persistent" : "Temp"}]: ${activePath}`);
|
|
412
461
|
|
|
413
462
|
const proxyObj = formatProxy(proxy);
|
|
414
463
|
const tz = timezoneId || undefined;
|
|
@@ -457,8 +506,8 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
|
|
|
457
506
|
|
|
458
507
|
const page = context.pages()[0] || (await context.newPage());
|
|
459
508
|
|
|
460
|
-
// Pass
|
|
461
|
-
return createBrowserController(browser, context, page,
|
|
509
|
+
// Pass dirToDelete so the temp folder gets deleted on close
|
|
510
|
+
return createBrowserController(browser, context, page, dirToDelete, humanize_options, launch_logs);
|
|
462
511
|
} catch (err) {
|
|
463
512
|
console.error("Firefox Temp Launch Error:", err);
|
|
464
513
|
throw err;
|
|
@@ -486,7 +535,7 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
|
|
|
486
535
|
|
|
487
536
|
const page = context.pages()[0];
|
|
488
537
|
|
|
489
|
-
return createBrowserController(context, context, page,
|
|
538
|
+
return createBrowserController(context, context, page, dirToDelete, humanize_options, launch_logs);
|
|
490
539
|
} catch (err) {
|
|
491
540
|
console.error("Firefox Persistent Launch Error:", err);
|
|
492
541
|
throw err;
|
|
@@ -497,11 +546,9 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
|
|
|
497
546
|
// ==========================================================================
|
|
498
547
|
// 6. ENGINE: BRAVE
|
|
499
548
|
// ==========================================================================
|
|
500
|
-
async function braveLauncher({
|
|
501
|
-
const isPersistent = !!profilePath;
|
|
502
|
-
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
549
|
+
async function braveLauncher({ activePath, isPersistent, dirToDelete, launch_logs, proxy, CapSolver, timezoneId, humanize_options, spoof_fingerprint }) {
|
|
503
550
|
|
|
504
|
-
console.log(
|
|
551
|
+
if (launch_logs) console.log(`░░ Starting Brave: ${activePath}`);
|
|
505
552
|
fs.mkdirSync(activePath, { recursive: true });
|
|
506
553
|
|
|
507
554
|
// ======================================================
|
|
@@ -516,12 +563,12 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
516
563
|
// Load saved settings from first launch
|
|
517
564
|
const savedSettings = JSON.parse(fs.readFileSync(settingsFilePath, "utf-8"));
|
|
518
565
|
effectiveSpoofFingerprint = savedSettings.spoof_fingerprint;
|
|
519
|
-
console.log(
|
|
566
|
+
if (launch_logs) console.log(`░░ Using saved spoof_fingerprint: ${JSON.stringify(effectiveSpoofFingerprint)} (ignoring current: ${JSON.stringify(spoof_fingerprint)})`);
|
|
520
567
|
} else {
|
|
521
568
|
// First launch - save the current setting
|
|
522
569
|
const settings = { spoof_fingerprint: spoof_fingerprint };
|
|
523
570
|
fs.writeFileSync(settingsFilePath, JSON.stringify(settings, null, 2), "utf-8");
|
|
524
|
-
console.log(
|
|
571
|
+
if (launch_logs) console.log(`░░ Saved spoof_fingerprint setting: ${JSON.stringify(spoof_fingerprint)}`);
|
|
525
572
|
}
|
|
526
573
|
}
|
|
527
574
|
|
|
@@ -545,7 +592,7 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
545
592
|
|
|
546
593
|
if (isPersistent) {
|
|
547
594
|
fs.writeFileSync(fingerprintFilePath, JSON.stringify(fingerprintData, null, 2), "utf-8");
|
|
548
|
-
console.log("
|
|
595
|
+
if (launch_logs) console.log("░░ Generated and saved new fingerprint for Brave");
|
|
549
596
|
}
|
|
550
597
|
}
|
|
551
598
|
|
|
@@ -569,7 +616,7 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
569
616
|
prefs.brave.sidebar.sidebar_show_option = 3;
|
|
570
617
|
fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
|
|
571
618
|
} catch (e) {
|
|
572
|
-
console.warn("
|
|
619
|
+
if (launch_logs) console.warn("░░ Could not modify Brave preferences:", e.message);
|
|
573
620
|
}
|
|
574
621
|
|
|
575
622
|
const args = [
|
|
@@ -656,11 +703,11 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
656
703
|
|
|
657
704
|
|
|
658
705
|
// ======================================================
|
|
659
|
-
//
|
|
706
|
+
// FIX: Close Extra Tabs (Brave Dashboard / Restore)
|
|
660
707
|
// ======================================================
|
|
661
708
|
const pages = context.pages();
|
|
662
709
|
if (pages.length > 1) {
|
|
663
|
-
console.log(
|
|
710
|
+
if (launch_logs) console.log(`░░ Found ${pages.length} tabs open in Brave. Closing extras...`);
|
|
664
711
|
// Close all tabs except the last one (which is usually the fresh active one)
|
|
665
712
|
for (let i = 0; i < pages.length - 1; i++) {
|
|
666
713
|
await pages[i].close();
|
|
@@ -668,19 +715,16 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
668
715
|
}
|
|
669
716
|
const page = context.pages()[0];
|
|
670
717
|
|
|
671
|
-
const dirToDelete = isPersistent ? null : activePath;
|
|
672
718
|
// Return the remaining open page (context.pages() might change, so we grab index 0 after cleanup)
|
|
673
|
-
return createBrowserController(context, context, page, dirToDelete, humanize_options);
|
|
719
|
+
return createBrowserController(context, context, page, dirToDelete, humanize_options, launch_logs);
|
|
674
720
|
}
|
|
675
721
|
|
|
676
722
|
// ==========================================================================
|
|
677
723
|
// 7. ENGINE: CAMOUFOX
|
|
678
724
|
// ==========================================================================
|
|
679
|
-
async function camoufoxLauncher({
|
|
680
|
-
const isPersistent = !!profilePath;
|
|
681
|
-
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
725
|
+
async function camoufoxLauncher({ activePath, isPersistent, dirToDelete, launch_logs, proxy, timezoneId, camoufox_options = {} }) {
|
|
682
726
|
|
|
683
|
-
console.log(
|
|
727
|
+
if (launch_logs) console.log(`░░ Starting camoufoxJs: ${activePath}`);
|
|
684
728
|
fs.mkdirSync(activePath, { recursive: true });
|
|
685
729
|
|
|
686
730
|
const proxyObj = formatProxy(proxy);
|
|
@@ -746,7 +790,7 @@ async function camoufoxLauncher({ profilePath, proxy, timezoneId, camoufox_optio
|
|
|
746
790
|
// Save persistent config
|
|
747
791
|
if (isPersistent) {
|
|
748
792
|
fs.writeFileSync(fingerprintFilePath, JSON.stringify(launchConfig, null, 2), "utf-8");
|
|
749
|
-
console.log("
|
|
793
|
+
if (launch_logs) console.log("░░ Generated and saved new launch config for Camoufox");
|
|
750
794
|
}
|
|
751
795
|
}
|
|
752
796
|
|
|
@@ -764,14 +808,13 @@ async function camoufoxLauncher({ profilePath, proxy, timezoneId, camoufox_optio
|
|
|
764
808
|
// Removed viewport: null, handled by Camoufox args
|
|
765
809
|
});
|
|
766
810
|
|
|
767
|
-
|
|
768
|
-
return createBrowserController(context, context, context.pages()[0], dirToDelete);
|
|
811
|
+
return createBrowserController(context, context, context.pages()[0], dirToDelete, null, launch_logs);
|
|
769
812
|
}
|
|
770
813
|
|
|
771
814
|
// ==========================================================================
|
|
772
815
|
// 8. ENGINE: MULTILOGIN
|
|
773
816
|
// ==========================================================================
|
|
774
|
-
async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_options = null }) {
|
|
817
|
+
async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_options = null, launch_logs = false }) {
|
|
775
818
|
// Destructure defaults from multilogin_options
|
|
776
819
|
const {
|
|
777
820
|
profileId = null,
|
|
@@ -783,7 +826,7 @@ async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_opt
|
|
|
783
826
|
} = multilogin_options;
|
|
784
827
|
|
|
785
828
|
if (profileId) {
|
|
786
|
-
return await launchExistingMultiloginProfile(profileId, humanize_options);
|
|
829
|
+
return await launchExistingMultiloginProfile(profileId, humanize_options, launch_logs);
|
|
787
830
|
} else {
|
|
788
831
|
return await launchQuickMultiloginProfile({
|
|
789
832
|
os_type,
|
|
@@ -792,11 +835,12 @@ async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_opt
|
|
|
792
835
|
media_masking,
|
|
793
836
|
audio_masking,
|
|
794
837
|
humanize_options,
|
|
838
|
+
launch_logs,
|
|
795
839
|
});
|
|
796
840
|
}
|
|
797
841
|
}
|
|
798
842
|
|
|
799
|
-
async function launchExistingMultiloginProfile(profileId, humanize_options = null) {
|
|
843
|
+
async function launchExistingMultiloginProfile(profileId, humanize_options = null, launch_logs = false) {
|
|
800
844
|
const startUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v2/profile/f/${MULTILOGIN_FOLDER_ID}/p/${profileId}/start?automation_type=playwright&headless_mode=false`;
|
|
801
845
|
let browser, context, page;
|
|
802
846
|
|
|
@@ -810,7 +854,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
|
|
|
810
854
|
if (response.status === 400) {
|
|
811
855
|
const errorJson = await response.json().catch(() => null);
|
|
812
856
|
if (errorJson?.status?.error_code === "PROFILE_ALREADY_RUNNING") {
|
|
813
|
-
console.log(
|
|
857
|
+
if (launch_logs) console.log(`░░ Profile ${profileId} is running. Restarting...`);
|
|
814
858
|
await stopMultiloginProfile(profileId);
|
|
815
859
|
await sleep(5000);
|
|
816
860
|
response = await fetch(startUrl, {
|
|
@@ -830,7 +874,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
|
|
|
830
874
|
|
|
831
875
|
const data = await response.json();
|
|
832
876
|
const port = data.data.port;
|
|
833
|
-
console.log(
|
|
877
|
+
if (launch_logs) console.log(`░░ Multilogin Profile ${profileId} started on port ${port}`);
|
|
834
878
|
|
|
835
879
|
browser = await chromium.connectOverCDP(`http://localhost:${port}`);
|
|
836
880
|
context = browser.contexts()[0];
|
|
@@ -856,7 +900,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
|
|
|
856
900
|
}
|
|
857
901
|
}
|
|
858
902
|
|
|
859
|
-
async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, humanize_options = null }) {
|
|
903
|
+
async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, humanize_options = null, launch_logs = false }) {
|
|
860
904
|
const createUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v3/profile/quick`;
|
|
861
905
|
let browser, context, page, profileId;
|
|
862
906
|
|
|
@@ -904,7 +948,7 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
|
|
|
904
948
|
|
|
905
949
|
profileId = json.data.id;
|
|
906
950
|
const port = json.data.port;
|
|
907
|
-
console.log(
|
|
951
|
+
if (launch_logs) console.log(`░░ Quick Profile Created: ${profileId} on port ${port}`);
|
|
908
952
|
|
|
909
953
|
browser = await chromium.connectOverCDP(`http://localhost:${port}`);
|
|
910
954
|
context = browser.contexts()[0];
|
|
@@ -944,7 +988,7 @@ async function stopMultiloginProfile(profileId) {
|
|
|
944
988
|
}
|
|
945
989
|
throw new Error(txt);
|
|
946
990
|
}
|
|
947
|
-
console.log(
|
|
991
|
+
if (launch_logs) console.log(`░░ Profile ${profileId} stopped.`);
|
|
948
992
|
return { error: null };
|
|
949
993
|
} catch (err) {
|
|
950
994
|
console.error(`Error stopping ${profileId}:`, err);
|
|
@@ -990,7 +1034,7 @@ function formatProxy(proxy) {
|
|
|
990
1034
|
return p;
|
|
991
1035
|
}
|
|
992
1036
|
|
|
993
|
-
function createBrowserController(browser, context, page, dirToDelete = null, humanize_options = null) {
|
|
1037
|
+
function createBrowserController(browser, context, page, dirToDelete = null, humanize_options = null, launch_logs = false) {
|
|
994
1038
|
// Apply human cursor if options provided
|
|
995
1039
|
let humanPage = page;
|
|
996
1040
|
if (humanize_options && page) {
|
|
@@ -999,7 +1043,7 @@ function createBrowserController(browser, context, page, dirToDelete = null, hum
|
|
|
999
1043
|
|
|
1000
1044
|
const closeBrowser = async () => {
|
|
1001
1045
|
try {
|
|
1002
|
-
console.log("
|
|
1046
|
+
if (launch_logs) console.log("░░ Closing browser session...");
|
|
1003
1047
|
if (context) await context.close();
|
|
1004
1048
|
if (browser && typeof browser.close === "function" && browser !== context) {
|
|
1005
1049
|
try {
|
|
@@ -1007,12 +1051,8 @@ function createBrowserController(browser, context, page, dirToDelete = null, hum
|
|
|
1007
1051
|
} catch (e) { }
|
|
1008
1052
|
}
|
|
1009
1053
|
if (dirToDelete) {
|
|
1010
|
-
if (
|
|
1011
|
-
|
|
1012
|
-
} else {
|
|
1013
|
-
console.log(`🗑️ Deleting temp profile: ${dirToDelete}`);
|
|
1014
|
-
await deleteDirectoryWithRetries(dirToDelete);
|
|
1015
|
-
}
|
|
1054
|
+
if (launch_logs) console.log(`░░ Deleting temp profile: ${dirToDelete}`);
|
|
1055
|
+
await deleteDirectoryWithRetries(dirToDelete);
|
|
1016
1056
|
}
|
|
1017
1057
|
return true;
|
|
1018
1058
|
} catch (err) {
|
|
@@ -114,7 +114,7 @@ export interface ProxyServerController {
|
|
|
114
114
|
getProxyStats: () => Record<string, TrafficStats>;
|
|
115
115
|
|
|
116
116
|
/** Returns formatted statistics for hostnames per proxy channel */
|
|
117
|
-
getHostStats: () => Record<string, Record<string,
|
|
117
|
+
getHostStats: () => Record<string, Record<string, TrafficStats>>;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
@@ -182,7 +182,7 @@ export async function startProxyServer({
|
|
|
182
182
|
}) {
|
|
183
183
|
// 1. Matchers
|
|
184
184
|
const matchers = {
|
|
185
|
-
noProxy: createHostMatcher([...NO_PROXY_HOSTS, "brave.com", "gvt1.com"]),
|
|
185
|
+
noProxy: createHostMatcher([...NO_PROXY_HOSTS, "%brave.com%", "%gvt1.com%"]),
|
|
186
186
|
proxy1: createHostMatcher([...PROXY_1_HOSTS, "proxy.multilogin.com", "multilogin.com"]),
|
|
187
187
|
proxy2: createHostMatcher([...PROXY_2_HOSTS, "proxy.multilogin.com", "multilogin.com"]),
|
|
188
188
|
};
|
|
@@ -190,7 +190,7 @@ export async function startProxyServer({
|
|
|
190
190
|
// 2. Port
|
|
191
191
|
const selectedPort = await findAvailablePort(50001, 50010);
|
|
192
192
|
if (!selectedPort) {
|
|
193
|
-
console.error("
|
|
193
|
+
console.error("░░ Critical Error: No available ports.");
|
|
194
194
|
return null;
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -218,8 +218,8 @@ export async function startProxyServer({
|
|
|
218
218
|
]);
|
|
219
219
|
|
|
220
220
|
// Warn only if proxy was configured (URL exists) but details came back null (Connection failed)
|
|
221
|
-
if (upstreamProxies.p1 && !p1Details) console.warn("
|
|
222
|
-
if (upstreamProxies.p2 && !p2Details) console.warn("
|
|
221
|
+
if (upstreamProxies.p1 && !p1Details) console.warn("░░ Warning: PROXY_1 configured but unreachable.");
|
|
222
|
+
if (upstreamProxies.p2 && !p2Details) console.warn("░░ Warning: PROXY_2 configured but unreachable.");
|
|
223
223
|
|
|
224
224
|
// 5. Stats
|
|
225
225
|
const stats = {
|
|
@@ -284,11 +284,14 @@ export async function startProxyServer({
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
// Record Stats
|
|
287
|
-
connectionMap[connectionId] = { type: proxyType };
|
|
287
|
+
connectionMap[connectionId] = { type: proxyType, hostname: hostname };
|
|
288
288
|
if (host_stats && hostname) {
|
|
289
289
|
// Ensure the type exists in map (it should, but safety first)
|
|
290
290
|
if (!hostStatsMap[proxyType]) hostStatsMap[proxyType] = {};
|
|
291
|
-
|
|
291
|
+
if (!hostStatsMap[proxyType][hostname]) {
|
|
292
|
+
hostStatsMap[proxyType][hostname] = { req: 0, Tx: 0, Rx: 0 };
|
|
293
|
+
}
|
|
294
|
+
hostStatsMap[proxyType][hostname].req++;
|
|
292
295
|
}
|
|
293
296
|
|
|
294
297
|
// Return Decision
|
|
@@ -305,13 +308,18 @@ export async function startProxyServer({
|
|
|
305
308
|
|
|
306
309
|
server.on("connectionClosed", ({ connectionId, stats: connStats }) => {
|
|
307
310
|
const connectionInfo = connectionMap[connectionId];
|
|
308
|
-
if (connectionInfo
|
|
309
|
-
const { type } = connectionInfo;
|
|
310
|
-
if (stats[type]) {
|
|
311
|
+
if (connectionInfo) {
|
|
312
|
+
const { type, hostname } = connectionInfo;
|
|
313
|
+
if (proxy_stats && stats[type]) {
|
|
311
314
|
stats[type].request++;
|
|
312
315
|
stats[type].Tx += connStats.srcTxBytes;
|
|
313
316
|
stats[type].Rx += connStats.srcRxBytes;
|
|
314
317
|
}
|
|
318
|
+
// Update host stats with Tx/Rx on connection close
|
|
319
|
+
if (host_stats && hostname && hostStatsMap[type] && hostStatsMap[type][hostname]) {
|
|
320
|
+
hostStatsMap[type][hostname].Tx += connStats.srcTxBytes;
|
|
321
|
+
hostStatsMap[type][hostname].Rx += connStats.srcRxBytes;
|
|
322
|
+
}
|
|
315
323
|
}
|
|
316
324
|
delete connectionMap[connectionId];
|
|
317
325
|
});
|
|
@@ -319,13 +327,13 @@ export async function startProxyServer({
|
|
|
319
327
|
try {
|
|
320
328
|
await server.listen();
|
|
321
329
|
serverRunning = true;
|
|
322
|
-
console.log(
|
|
330
|
+
console.log(`░░ Local Proxy Started: http://127.0.0.1:${selectedPort}`);
|
|
323
331
|
} catch (err) {
|
|
324
|
-
console.error("
|
|
332
|
+
console.error("░░ Failed to start proxy server:", err);
|
|
325
333
|
return null;
|
|
326
334
|
}
|
|
327
335
|
|
|
328
|
-
const formatBytes = (bytes) => (bytes / 1024 / 1024).toFixed(
|
|
336
|
+
const formatBytes = (bytes) => (bytes / 1024 / 1024).toFixed(3);
|
|
329
337
|
|
|
330
338
|
const getProxyStatsFormatted = () => {
|
|
331
339
|
const formatted = {};
|
|
@@ -344,9 +352,13 @@ export async function startProxyServer({
|
|
|
344
352
|
// Iterate over each proxy category
|
|
345
353
|
for (const [type, hosts] of Object.entries(hostStatsMap)) {
|
|
346
354
|
const sortedHosts = Object.entries(hosts)
|
|
347
|
-
.sort((a, b) => b[1] - a[1]) // Sort by count descending
|
|
348
|
-
.reduce((acc, [host,
|
|
349
|
-
acc[host] =
|
|
355
|
+
.sort((a, b) => b[1].req - a[1].req) // Sort by request count descending
|
|
356
|
+
.reduce((acc, [host, hostData]) => {
|
|
357
|
+
acc[host] = {
|
|
358
|
+
req: hostData.req,
|
|
359
|
+
Tx: formatBytes(hostData.Tx) + " MB",
|
|
360
|
+
Rx: formatBytes(hostData.Rx) + " MB",
|
|
361
|
+
};
|
|
350
362
|
return acc;
|
|
351
363
|
}, {});
|
|
352
364
|
|
|
@@ -387,13 +399,13 @@ export async function startProxyServer({
|
|
|
387
399
|
|
|
388
400
|
// Auto console.log stats on close
|
|
389
401
|
if (proxy_stats) {
|
|
390
|
-
console.log("
|
|
402
|
+
console.log("░░ Proxy Stats:", getProxyStatsFormatted());
|
|
391
403
|
}
|
|
392
404
|
if (host_stats) {
|
|
393
|
-
console.log("
|
|
405
|
+
console.log("░░ Host Stats:", getHostStatsFormatted());
|
|
394
406
|
}
|
|
395
407
|
|
|
396
|
-
console.log("
|
|
408
|
+
console.log("░░ Proxy server closed.");
|
|
397
409
|
},
|
|
398
410
|
|
|
399
411
|
getProxyStats: getProxyStatsFormatted,
|