arn-browser 0.1.13 → 0.1.14

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.13",
3
+ "version": "0.1.14",
4
4
  "description": "A lightweight, browser autmation helper.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
package/src/index.d.ts CHANGED
@@ -4,8 +4,8 @@ import { ppLaunch } from "./utility/puppeteer/ppLaunch";
4
4
  import { ppRoute, ppCacheLogs } from "./utility/puppeteer/routes/ppRoute";
5
5
  import { pwLaunch } from "./utility/playwright/pwLaunch";
6
6
  import { pwRoute, pwCacheLogs } from "./utility/playwright/routes/pwRoute";
7
- import { retryNavigation, retryClick, checkPageConditions } from "./utility/playwright/pwHelper";
8
- import { retryNavigation as ppRetryNavigation } from "./utility/puppeteer/ppHelper";
7
+ import { retryNavigation, retryClick, checkPageConditions, exportSession, importSession } from "./utility/playwright/pwHelper";
8
+ import { retryNavigation as ppRetryNavigation, retryClick as ppRetryClick, checkPageConditions as ppCheckPageConditions, exportSession as ppExportSession, importSession as ppImportSession } from "./utility/puppeteer/ppHelper";
9
9
  import { startProxyServer, fetchPublicIP, fetchProxyDetails } from "./utility/proxy-utility/proxy-chain";
10
10
  import {
11
11
  fetchAwsProxy, getInstanceStatus, getPublicIpAddress,
@@ -21,16 +21,22 @@ export declare const ppBrowser: {
21
21
  launch: typeof ppLaunch;
22
22
  route: typeof ppRoute;
23
23
  cacheLogs: typeof ppCacheLogs;
24
- ppGoto: typeof ppRetryNavigation;
24
+ Goto: typeof ppRetryNavigation;
25
+ Click: typeof ppRetryClick;
26
+ Conditions: typeof ppCheckPageConditions;
27
+ exportSession: typeof ppExportSession;
28
+ importSession: typeof ppImportSession;
25
29
  };
26
30
 
27
31
  export declare const pwBrowser: {
28
32
  launch: typeof pwLaunch;
29
33
  route: typeof pwRoute;
30
34
  cacheLogs: typeof pwCacheLogs;
31
- pwGoto: typeof retryNavigation;
32
- pwClick: typeof retryClick;
33
- pwConditions: typeof checkPageConditions;
35
+ Goto: typeof retryNavigation;
36
+ Click: typeof retryClick;
37
+ Conditions: typeof checkPageConditions;
38
+ exportSession: typeof exportSession;
39
+ importSession: typeof importSession;
34
40
  };
35
41
 
36
42
  export declare const proxyUtil: {
package/src/index.js CHANGED
@@ -5,27 +5,33 @@
5
5
  // --- Puppeteer ---
6
6
  import { ppLaunch } from "./utility/puppeteer/ppLaunch.js";
7
7
  import { ppRoute, ppCacheLogs } from "./utility/puppeteer/routes/ppRoute.js";
8
- import { retryNavigation as ppRetryNavigation } from "./utility/puppeteer/ppHelper.js";
8
+ import { retryNavigation as ppRetryNavigation, retryClick as ppRetryClick, checkPageConditions as ppCheckPageConditions, exportSession as ppExportSession, importSession as ppImportSession } from "./utility/puppeteer/ppHelper.js";
9
9
 
10
10
  export const ppBrowser = {
11
11
  launch: ppLaunch,
12
12
  route: ppRoute,
13
13
  cacheLogs: ppCacheLogs,
14
- ppGoto: ppRetryNavigation,
14
+ Goto: ppRetryNavigation,
15
+ Click: ppRetryClick,
16
+ Conditions: ppCheckPageConditions,
17
+ exportSession: ppExportSession,
18
+ importSession: ppImportSession,
15
19
  };
16
20
 
17
21
  // --- Playwright ---
18
22
  import { pwLaunch } from "./utility/playwright/pwLaunch.js";
19
23
  import { pwRoute, pwCacheLogs } from "./utility/playwright/routes/pwRoute.js";
20
- import { retryNavigation, retryClick, checkPageConditions } from "./utility/playwright/pwHelper.js";
24
+ import { retryNavigation, retryClick, checkPageConditions, exportSession, importSession } from "./utility/playwright/pwHelper.js";
21
25
 
22
26
  export const pwBrowser = {
23
27
  launch: pwLaunch,
24
28
  route: pwRoute,
25
29
  cacheLogs: pwCacheLogs,
26
- pwGoto: retryNavigation,
27
- pwClick: retryClick,
28
- pwConditions: checkPageConditions,
30
+ Goto: retryNavigation,
31
+ Click: retryClick,
32
+ Conditions: checkPageConditions,
33
+ exportSession,
34
+ importSession,
29
35
  };
30
36
 
31
37
  // --- Proxy ---
@@ -43,21 +43,7 @@ function gaussianRandom(mean = 0.5, stdDev = 0.15) {
43
43
  return Math.max(0.1, Math.min(0.9, z0 * stdDev + mean));
44
44
  }
45
45
 
46
- /**
47
- * Simple mutex for serializing keyboard operations
48
- */
49
- let typingLock = Promise.resolve();
50
- async function withTypingLock(fn) {
51
- const previousLock = typingLock;
52
- let releaseLock;
53
- typingLock = new Promise(resolve => { releaseLock = resolve; });
54
- await previousLock;
55
- try {
56
- return await fn();
57
- } finally {
58
- releaseLock();
59
- }
60
- }
46
+
61
47
 
62
48
  /**
63
49
  * Creates a HumanLocator that wraps a Playwright Locator
@@ -144,7 +130,7 @@ function createHumanLocator(cursor, locator) {
144
130
  if (!cursor.config.humanize) {
145
131
  return await locator.fill(value, options);
146
132
  }
147
- return await withTypingLock(async () => {
133
+ return await cursor._withTypingLock(async () => {
148
134
  await this.click(options);
149
135
  // Clear field reliably using fill('') before human typing
150
136
  await locator.fill('');
@@ -158,7 +144,7 @@ function createHumanLocator(cursor, locator) {
158
144
  if (!cursor.config.humanize) {
159
145
  return await locator.type(text, options);
160
146
  }
161
- return await withTypingLock(async () => {
147
+ return await cursor._withTypingLock(async () => {
162
148
  await this.click(options);
163
149
  await cursor._type(text, options);
164
150
  });
@@ -212,7 +198,7 @@ function createHumanLocator(cursor, locator) {
212
198
  if (!cursor.config.humanize) {
213
199
  return await locator.pressSequentially(text, options);
214
200
  }
215
- return await withTypingLock(async () => {
201
+ return await cursor._withTypingLock(async () => {
216
202
  await this.click(options);
217
203
  await locator.clear();
218
204
  await cursor._type(text, options);
@@ -292,9 +278,26 @@ export function createCursor(page, options = {}) {
292
278
  // Internal state
293
279
  const cursor = {
294
280
  _page: page,
281
+ _typingLock: Promise.resolve(),
282
+ _viewportSize: null,
295
283
  originCoordinates: [0, 0],
296
284
  config,
297
285
 
286
+ /**
287
+ * Per-cursor mutex for serializing keyboard operations
288
+ */
289
+ async _withTypingLock(fn) {
290
+ const previousLock = this._typingLock;
291
+ let releaseLock;
292
+ this._typingLock = new Promise(resolve => { releaseLock = resolve; });
293
+ await previousLock;
294
+ try {
295
+ return await fn();
296
+ } finally {
297
+ releaseLock();
298
+ }
299
+ },
300
+
298
301
  /**
299
302
  * Move cursor to locator with human-like behavior
300
303
  * Includes overshoot/correction and pre-movement jitter
@@ -367,7 +370,11 @@ export function createCursor(page, options = {}) {
367
370
  * Move cursor to a specific point with variable speed and micro-pauses
368
371
  */
369
372
  async _moveToPoint(destination, options = {}) {
370
- const viewport = page.viewportSize() || { width: 1280, height: 720 };
373
+ if (!this._viewportSize) {
374
+ this._viewportSize = page.viewportSize()
375
+ || await page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
376
+ }
377
+ const viewport = this._viewportSize;
371
378
 
372
379
  const params = generateRandomCurveParameters(
373
380
  viewport,
@@ -94,7 +94,7 @@ export const checkPageConditions = async (page, checksToPerform, timeout) => {
94
94
  try {
95
95
  const promises = Object.entries(checksToPerform)
96
96
  .filter(([_, value]) => value !== null) // Needed when setting value null after match
97
- .map(([key, value]) => {
97
+ .map(async ([key, value]) => {
98
98
  if (typeof value === "string") {
99
99
  // Handle URL check
100
100
  return page
@@ -127,3 +127,83 @@ export const checkPageConditions = async (page, checksToPerform, timeout) => {
127
127
  return null; // Return null if no match is found
128
128
  }
129
129
  };
130
+
131
+ /**
132
+ * Exports browser session (cookies + localStorage) via CDP.
133
+ * Silent operation — no browser blinking.
134
+ *
135
+ * @param {import('playwright-core').BrowserContext} context - Playwright BrowserContext
136
+ * @returns {Promise<{cookies: Array, localStorage: Array}>} Session data for JSONB storage
137
+ */
138
+ export const exportSession = async (context) => {
139
+ const pages = context.pages();
140
+ const page = pages[0];
141
+ if (!page) throw new Error("No page available for exportSession");
142
+
143
+ const client = await context.newCDPSession(page);
144
+ const { cookies } = await client.send("Network.getAllCookies");
145
+ await client.detach();
146
+
147
+ const localStorage = [];
148
+ for (const p of pages) {
149
+ const url = p.url();
150
+ if (!url || url === "about:blank" || url.startsWith("chrome")) continue;
151
+
152
+ try {
153
+ const origin = new URL(url).origin;
154
+ const items = await p.evaluate(() => {
155
+ const data = [];
156
+ for (let i = 0; i < window.localStorage.length; i++) {
157
+ const key = window.localStorage.key(i);
158
+ data.push({ name: key, value: window.localStorage.getItem(key) });
159
+ }
160
+ return data;
161
+ });
162
+
163
+ if (items.length > 0 && !localStorage.some((o) => o.origin === origin)) {
164
+ localStorage.push({ origin, items });
165
+ }
166
+ } catch (e) {
167
+ // Skip pages that can't be evaluated
168
+ }
169
+ }
170
+
171
+ return { cookies, localStorage };
172
+ };
173
+
174
+ /**
175
+ * Imports browser session (cookies + localStorage) via CDP.
176
+ * Silent operation — no browser blinking.
177
+ *
178
+ * @param {import('playwright-core').BrowserContext} context - Playwright BrowserContext
179
+ * @param {Object} data - Session data { cookies, localStorage }
180
+ */
181
+ export const importSession = async (context, data) => {
182
+ const pages = context.pages();
183
+ const page = pages[0];
184
+ if (!page) throw new Error("No page available for importSession");
185
+
186
+ const client = await context.newCDPSession(page);
187
+
188
+ if (data.cookies?.length) {
189
+ await client.send("Network.setCookies", { cookies: data.cookies });
190
+ }
191
+
192
+ if (data.localStorage?.length) {
193
+ await client.send("DOMStorage.enable");
194
+ for (const entry of data.localStorage) {
195
+ if (!entry.items?.length) continue;
196
+
197
+ const storageId = { securityOrigin: entry.origin, isLocalStorage: true };
198
+ for (const { name, value } of entry.items) {
199
+ await client.send("DOMStorage.setDOMStorageItem", {
200
+ storageId,
201
+ key: name,
202
+ value: value,
203
+ });
204
+ }
205
+ }
206
+ }
207
+
208
+ await client.detach();
209
+ };
@@ -494,6 +494,31 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
494
494
  try {
495
495
  if (!fs.existsSync(activePath)) fs.mkdirSync(activePath, { recursive: true });
496
496
 
497
+ // ======================================================
498
+ // Prevent Tab Restore via Preferences
499
+ // ======================================================
500
+ try {
501
+ const prefsFilePath = path.join(activePath, "Default", "Preferences");
502
+ const prefsDir = path.dirname(prefsFilePath);
503
+ if (!fs.existsSync(prefsDir)) fs.mkdirSync(prefsDir, { recursive: true });
504
+
505
+ let prefs = {};
506
+ if (fs.existsSync(prefsFilePath)) {
507
+ prefs = JSON.parse(fs.readFileSync(prefsFilePath, "utf-8"));
508
+ }
509
+
510
+ if (!prefs.profile) prefs.profile = {};
511
+ prefs.profile.exit_type = "Normal";
512
+
513
+ if (!prefs.session) prefs.session = {};
514
+ prefs.session.restore_on_startup = 4;
515
+ prefs.session.startup_urls = ["about:blank"];
516
+
517
+ fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
518
+ } catch (e) {
519
+ // Non-critical
520
+ }
521
+
497
522
  // Logic: Native Persistent Launch. No fingerprint-injector.
498
523
  const context = await chromium.launchPersistentContext(activePath, {
499
524
  headless: false,
@@ -690,6 +715,15 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
690
715
  if (!prefs.brave) prefs.brave = {};
691
716
  if (!prefs.brave.sidebar) prefs.brave.sidebar = {};
692
717
  prefs.brave.sidebar.sidebar_show_option = 3;
718
+
719
+ // Prevent tab restore (saves proxy bandwidth)
720
+ if (!prefs.profile) prefs.profile = {};
721
+ prefs.profile.exit_type = "Normal";
722
+
723
+ if (!prefs.session) prefs.session = {};
724
+ prefs.session.restore_on_startup = 4;
725
+ prefs.session.startup_urls = ["about:blank"];
726
+
693
727
  fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
694
728
  } catch (e) {
695
729
  console.warn("░░░░░ Could not modify Brave preferences:", e.message);
@@ -729,7 +763,6 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
729
763
  // --- Brave Specific Silence ---
730
764
  "--disable-features=Translate,BraveRewards,BraveWallet,BraveNews,Sidebar,SidePanel,BraveNTPBrandedWallpaper,NTPBackgroundImages",
731
765
  "--disable-infobars",
732
- "about:blank",
733
766
  ];
734
767
  const ignoreDefaultArgs = ["--enable-automation"];
735
768
  if (CapSolver) {
@@ -1,4 +1,4 @@
1
- import { Page } from "puppeteer-core";
1
+ import { Page, Locator } from "puppeteer-core";
2
2
 
3
3
  /**
4
4
  * Options for the retryNavigation function.
@@ -18,9 +18,53 @@ export interface RetryNavigationOptions {
18
18
  waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
19
19
  }
20
20
 
21
+ /**
22
+ * Options for the retryClick function.
23
+ */
24
+ export interface RetryClickOptions {
25
+ /** The Puppeteer Page object. Required when using selector. */
26
+ page?: Page;
27
+ /** CSS/aria/xpath selector string (old-style). Provide either selector or locator. */
28
+ selector?: string;
29
+ /** Puppeteer Locator (new-style). Provide either selector or locator. */
30
+ locator?: Locator<Node>;
31
+ /** Maximum number of retry attempts. Default: 3 */
32
+ maxRetries?: number;
33
+ /** Timeout for waiting for element visibility/invisibility. Default: 15000 */
34
+ timeout?: number;
35
+ }
36
+
21
37
  /**
22
38
  * Navigates to a URL with retry logic and incremental timeouts.
23
39
  * If navigation fails, it temporarily goes to "about:blank" before retrying.
24
40
  * @returns True if navigation succeeded, throws error otherwise.
25
41
  */
26
42
  export function retryNavigation(options: RetryNavigationOptions): Promise<boolean>;
43
+
44
+ /**
45
+ * Retries clicking an element (by selector or locator).
46
+ * Warning: This function waits for the element to become HIDDEN after clicking.
47
+ * Use this for actions that close modals or navigate away.
48
+ */
49
+ export function retryClick(options: RetryClickOptions): Promise<void>;
50
+
51
+ /**
52
+ * Races multiple conditions (URL match or Locator visibility) to see which happens first.
53
+ *
54
+ * Value types in checksToPerform:
55
+ * - string → URL check (exact match, startsWith, or glob with *)
56
+ * - Locator → Element check via page.locator() (new-style API)
57
+ * - null → skipped
58
+ *
59
+ * NOTE: This function MUTATES the `checksToPerform` object by deleting the matched key.
60
+ *
61
+ * @param page - The Puppeteer Page object.
62
+ * @param checksToPerform - An object mapping keys (names) to URL strings, Locators, or null.
63
+ * @param timeout - Maximum time to wait for a condition in milliseconds.
64
+ * @returns The key name of the matched condition, or null if timed out.
65
+ */
66
+ export function checkPageConditions(
67
+ page: Page,
68
+ checksToPerform: Record<string, string | Locator<Node> | null>,
69
+ timeout: number
70
+ ): Promise<string | null>;
@@ -46,3 +46,182 @@ export const retryNavigation = async ({
46
46
  }
47
47
  return false;
48
48
  };
49
+
50
+ /**
51
+ * Retries clicking an element (by selector or locator).
52
+ * EXPECTATION: The element should disappear (become hidden/detached) after clicking.
53
+ *
54
+ * @param {Object} options
55
+ * @param {import('puppeteer-core').Page} options.page - Puppeteer Page object (required when using selector)
56
+ * @param {string} [options.selector] - CSS/aria/xpath selector string (old-style)
57
+ * @param {import('puppeteer-core').Locator} [options.locator] - Puppeteer Locator (new-style)
58
+ * @param {number} [options.maxRetries=3] - Max retries
59
+ * @param {number} [options.timeout=15000] - Timeout for visibility checks
60
+ */
61
+ export const retryClick = async ({ page, selector, locator, maxRetries = 3, timeout = 15000 }) => {
62
+ if (!selector && !locator) throw new Error("Either selector or locator is required");
63
+ if (selector && !page) throw new Error("Page is required when using selector");
64
+
65
+ for (let retryCount = 0; retryCount < maxRetries; retryCount++) {
66
+ try {
67
+ if (locator) {
68
+ // New-style: use Locator API
69
+ await locator.setTimeout(timeout).setVisibility("visible").click();
70
+ // Wait for element to disappear
71
+ await locator.setTimeout(timeout).setVisibility("hidden").wait().catch(() => {});
72
+ } else {
73
+ // Old-style: use waitForSelector
74
+ const el = await page.waitForSelector(selector, { visible: true, timeout });
75
+ await el.click();
76
+ // Wait for element to disappear
77
+ await page.waitForSelector(selector, { hidden: true, timeout }).catch(() => {});
78
+ }
79
+ return; // Exit if successful
80
+ } catch (error) {
81
+ if (retryCount === maxRetries - 1) {
82
+ throw error;
83
+ }
84
+ }
85
+ }
86
+ };
87
+
88
+ /**
89
+ * Races multiple conditions (URL or Locator) to see which happens first.
90
+ * Modifies the input object by deleting the matched key.
91
+ *
92
+ * Value types in checksToPerform:
93
+ * - string → URL check (exact match, startsWith, or glob with *)
94
+ * - Locator → Element check via page.locator() (new-style API)
95
+ * - null → skipped
96
+ *
97
+ * @param {import('puppeteer-core').Page} page
98
+ * @param {Object.<string, string|import('puppeteer-core').Locator|null>} checksToPerform - Map of names to URL strings or Locators
99
+ * @param {number} timeout - Timeout in ms
100
+ * @returns {Promise<string|null>} The key of the matched condition
101
+ */
102
+ export const checkPageConditions = async (page, checksToPerform, timeout) => {
103
+ // Validate required parameters
104
+ if (!page) throw new Error("Page parameter is required");
105
+ if (!timeout) throw new Error("Timeout parameter is required");
106
+
107
+ let matchedKey = null;
108
+ try {
109
+ const promises = Object.entries(checksToPerform)
110
+ .filter(([_, value]) => value !== null) // Needed when setting value null after match
111
+ .map(async ([key, value]) => {
112
+ if (typeof value === "string") {
113
+ // Handle URL check — poll with waitForFunction
114
+ const urlPattern = value;
115
+ return page
116
+ .waitForFunction(
117
+ (pattern) => {
118
+ const url = window.location.href;
119
+ if (pattern.includes("*")) {
120
+ // Convert glob to regex: ** → .*, * → [^/]*
121
+ const regexStr = pattern.replace(/\*\*/g, ".*").replace(/(?<!\.\*)\*/g, "[^/]*");
122
+ return new RegExp("^" + regexStr + "$").test(url);
123
+ }
124
+ return url === pattern || url.startsWith(pattern);
125
+ },
126
+ { timeout },
127
+ urlPattern
128
+ )
129
+ .then(() => key);
130
+ } else {
131
+ // Handle Locator check (from page.locator())
132
+ return value
133
+ .setTimeout(timeout)
134
+ .setVisibility("visible")
135
+ .wait()
136
+ .then(() => key);
137
+ }
138
+ });
139
+
140
+ matchedKey = await Promise.race(promises);
141
+ if (matchedKey) {
142
+ // Remove the matched key from the original object
143
+ delete checksToPerform[matchedKey];
144
+ console.log(`${matchedKey} matched!`);
145
+ }
146
+ return matchedKey; // Return the matched key if found
147
+ } catch (error) {
148
+ console.log(`No match found - ${error.message}`);
149
+ return null; // Return null if no match is found
150
+ }
151
+ };
152
+
153
+ /**
154
+ * Exports browser session (cookies + localStorage) via CDP.
155
+ * Silent operation — no browser blinking.
156
+ *
157
+ * @param {import('puppeteer-core').Page} page - Any Puppeteer Page
158
+ * @returns {Promise<{cookies: Array, localStorage: Array}>} Session data for JSONB storage
159
+ */
160
+ export const exportSession = async (page) => {
161
+ const client = await page.createCDPSession();
162
+ const { cookies } = await client.send("Network.getAllCookies");
163
+ await client.detach();
164
+
165
+ const localStorage = [];
166
+ const browser = page.browser();
167
+ const pages = await browser.pages();
168
+
169
+ for (const p of pages) {
170
+ const url = p.url();
171
+ if (!url || url === "about:blank" || url.startsWith("chrome")) continue;
172
+
173
+ try {
174
+ const origin = new URL(url).origin;
175
+ const items = await p.evaluate(() => {
176
+ const data = [];
177
+ for (let i = 0; i < window.localStorage.length; i++) {
178
+ const key = window.localStorage.key(i);
179
+ data.push({ name: key, value: window.localStorage.getItem(key) });
180
+ }
181
+ return data;
182
+ });
183
+
184
+ if (items.length > 0 && !localStorage.some((o) => o.origin === origin)) {
185
+ localStorage.push({ origin, items });
186
+ }
187
+ } catch (e) {
188
+ // Skip pages that can't be evaluated
189
+ }
190
+ }
191
+
192
+ return { cookies, localStorage };
193
+ };
194
+
195
+ /**
196
+ * Imports browser session (cookies + localStorage) into a Puppeteer Browser.
197
+ *
198
+ * @param {import('puppeteer-core').Page} page - Any Puppeteer Page
199
+ * @param {Object} data - Session data { cookies, localStorage }
200
+ */
201
+ export const importSession = async (page, data) => {
202
+ if (data.cookies?.length) {
203
+ const client = await page.createCDPSession();
204
+ await client.send("Network.setCookies", { cookies: data.cookies });
205
+ await client.detach();
206
+ }
207
+
208
+ if (data.localStorage?.length) {
209
+ const browser = page.browser();
210
+
211
+ for (const entry of data.localStorage) {
212
+ if (!entry.items?.length) continue;
213
+
214
+ const p = await browser.newPage();
215
+ try {
216
+ await p.goto(entry.origin, { waitUntil: "domcontentloaded", timeout: 15000 });
217
+ await p.evaluate((items) => {
218
+ for (const { name, value } of items) {
219
+ window.localStorage.setItem(name, value);
220
+ }
221
+ }, entry.items);
222
+ } finally {
223
+ await p.close();
224
+ }
225
+ }
226
+ }
227
+ };
@@ -348,6 +348,15 @@ async function braveLauncher({ profilePath, proxy, extraArgs, spoof_fingerprint,
348
348
  if (!prefs.brave) prefs.brave = {};
349
349
  if (!prefs.brave.sidebar) prefs.brave.sidebar = {};
350
350
  prefs.brave.sidebar.sidebar_show_option = 3;
351
+
352
+ // Prevent tab restore (saves proxy bandwidth)
353
+ if (!prefs.profile) prefs.profile = {};
354
+ prefs.profile.exit_type = "Normal";
355
+
356
+ if (!prefs.session) prefs.session = {};
357
+ prefs.session.restore_on_startup = 4;
358
+ prefs.session.startup_urls = ["about:blank"];
359
+
351
360
  fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
352
361
  } catch (e) {
353
362
  console.warn("░░░░░ Could not modify Brave preferences:", e.message);
@@ -380,6 +389,35 @@ async function spawnAndConnect({ binaryPath, profilePath, isPersistent, proxy, e
380
389
  let closing = false;
381
390
  let signalHandler;
382
391
 
392
+ // ======================================================
393
+ // Prevent Tab Restore via Preferences
394
+ // Sets exit_type to "Normal" and startup to open about:blank
395
+ // This prevents old tabs from loading (saves proxy bandwidth)
396
+ // ======================================================
397
+ try {
398
+ const prefsFilePath = path.join(profilePath, "Default", "Preferences");
399
+ const prefsDir = path.dirname(prefsFilePath);
400
+ if (!fs.existsSync(prefsDir)) fs.mkdirSync(prefsDir, { recursive: true });
401
+
402
+ let prefs = {};
403
+ if (fs.existsSync(prefsFilePath)) {
404
+ prefs = JSON.parse(fs.readFileSync(prefsFilePath, "utf-8"));
405
+ }
406
+
407
+ // Prevent "Restore pages?" prompt after crash
408
+ if (!prefs.profile) prefs.profile = {};
409
+ prefs.profile.exit_type = "Normal";
410
+
411
+ // Set startup to open about:blank instead of restoring tabs
412
+ if (!prefs.session) prefs.session = {};
413
+ prefs.session.restore_on_startup = 4;
414
+ prefs.session.startup_urls = ["about:blank"];
415
+
416
+ fs.writeFileSync(prefsFilePath, JSON.stringify(prefs, null, 2), "utf-8");
417
+ } catch (e) {
418
+ // Non-critical — don't break the launch
419
+ }
420
+
383
421
  // Random 5-digit port (10000–65535)
384
422
  const debugPort = Math.floor(Math.random() * 55535) + 10000;
385
423
 
@@ -394,8 +432,10 @@ async function spawnAndConnect({ binaryPath, profilePath, isPersistent, proxy, e
394
432
  // Very Important for Vps
395
433
  "--no-sandbox",
396
434
  // --- Stealth & Anti-Detection ---
435
+ // Suppresses "unsupported command-line flag" warning bar
397
436
  "--test-type",
398
- "--disable-blink-features=AutomationControlled",
437
+ // Not needed for CDP — navigator.webdriver is not set when using --remote-debugging-port
438
+ // "--disable-blink-features=AutomationControlled",
399
439
 
400
440
  // --- PREVENT EXTRA TABS ---
401
441
  "--disable-restore-session-state",