arn-browser 0.1.12 → 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.12",
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",
@@ -22,7 +22,7 @@
22
22
  "https-proxy-agent": "^7.0.6",
23
23
  "node-cache": "^5.1.2",
24
24
  "node-fetch": "^3.3.2",
25
- "playwright": "^1.42.1",
25
+ "playwright-core": "1.42.1",
26
26
  "proxy-chain": "^2.6.0",
27
27
  "puppeteer-core": "^24.38.0",
28
28
  "randomstring": "^1.3.1",
package/src/index.d.ts CHANGED
@@ -4,7 +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/playwright-helper";
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";
8
9
  import { startProxyServer, fetchPublicIP, fetchProxyDetails } from "./utility/proxy-utility/proxy-chain";
9
10
  import {
10
11
  fetchAwsProxy, getInstanceStatus, getPublicIpAddress,
@@ -20,15 +21,22 @@ export declare const ppBrowser: {
20
21
  launch: typeof ppLaunch;
21
22
  route: typeof ppRoute;
22
23
  cacheLogs: typeof ppCacheLogs;
24
+ Goto: typeof ppRetryNavigation;
25
+ Click: typeof ppRetryClick;
26
+ Conditions: typeof ppCheckPageConditions;
27
+ exportSession: typeof ppExportSession;
28
+ importSession: typeof ppImportSession;
23
29
  };
24
30
 
25
31
  export declare const pwBrowser: {
26
32
  launch: typeof pwLaunch;
27
33
  route: typeof pwRoute;
28
34
  cacheLogs: typeof pwCacheLogs;
29
- pwGoto: typeof retryNavigation;
30
- pwClick: typeof retryClick;
31
- pwConditions: typeof checkPageConditions;
35
+ Goto: typeof retryNavigation;
36
+ Click: typeof retryClick;
37
+ Conditions: typeof checkPageConditions;
38
+ exportSession: typeof exportSession;
39
+ importSession: typeof importSession;
32
40
  };
33
41
 
34
42
  export declare const proxyUtil: {
package/src/index.js CHANGED
@@ -5,25 +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, retryClick as ppRetryClick, checkPageConditions as ppCheckPageConditions, exportSession as ppExportSession, importSession as ppImportSession } from "./utility/puppeteer/ppHelper.js";
8
9
 
9
10
  export const ppBrowser = {
10
11
  launch: ppLaunch,
11
12
  route: ppRoute,
12
13
  cacheLogs: ppCacheLogs,
14
+ Goto: ppRetryNavigation,
15
+ Click: ppRetryClick,
16
+ Conditions: ppCheckPageConditions,
17
+ exportSession: ppExportSession,
18
+ importSession: ppImportSession,
13
19
  };
14
20
 
15
21
  // --- Playwright ---
16
22
  import { pwLaunch } from "./utility/playwright/pwLaunch.js";
17
23
  import { pwRoute, pwCacheLogs } from "./utility/playwright/routes/pwRoute.js";
18
- import { retryNavigation, retryClick, checkPageConditions } from "./utility/playwright/playwright-helper.js";
24
+ import { retryNavigation, retryClick, checkPageConditions, exportSession, importSession } from "./utility/playwright/pwHelper.js";
19
25
 
20
26
  export const pwBrowser = {
21
27
  launch: pwLaunch,
22
28
  route: pwRoute,
23
29
  cacheLogs: pwCacheLogs,
24
- pwGoto: retryNavigation,
25
- pwClick: retryClick,
26
- pwConditions: checkPageConditions,
30
+ Goto: retryNavigation,
31
+ Click: retryClick,
32
+ Conditions: checkPageConditions,
33
+ exportSession,
34
+ importSession,
27
35
  };
28
36
 
29
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);
@@ -273,9 +259,9 @@ function createHumanLocator(cursor, locator) {
273
259
  * Create a HumanCursor that acts as a drop-in replacement for Page
274
260
  * All page methods work, but click/fill/type/check/hover use human cursor
275
261
  *
276
- * @param {import('playwright').Page} page - Playwright Page object
262
+ * @param {import('playwright-core').Page} page - Playwright Page object
277
263
  * @param {Object} options - Configuration options
278
- * @returns {import('playwright').Page & HumanCursorMethods}
264
+ * @returns {import('playwright-core').Page & HumanCursorMethods}
279
265
  */
280
266
  export function createCursor(page, options = {}) {
281
267
  const config = {
@@ -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,
@@ -2,7 +2,7 @@
2
2
  * TypeScript declarations for human-cursor-playwright
3
3
  */
4
4
 
5
- import type { Page, Locator, Response, PageScreenshotOptions, WaitForURLOptions } from 'playwright';
5
+ import type { Page, Locator, Response, PageScreenshotOptions, WaitForURLOptions } from 'playwright-core';
6
6
 
7
7
  // ============= Tweening Functions =============
8
8
 
@@ -1,4 +1,4 @@
1
- import { Page, Locator } from "playwright";
1
+ import { Page, Locator } from "playwright-core";
2
2
 
3
3
  /**
4
4
  * Options for the retryNavigation function.
@@ -3,7 +3,7 @@
3
3
  * If navigation fails, it temporarily goes to "about:blank" before retrying.
4
4
  *
5
5
  * @param {Object} options
6
- * @param {import('playwright').Page} options.page - Playwright Page object
6
+ * @param {import('playwright-core').Page} options.page - Playwright Page object
7
7
  * @param {string} options.url - Target URL
8
8
  * @param {number} [options.maxRetries=5] - Maximum number of attempts
9
9
  * @param {string|null} [options.referer=null] - Referer header
@@ -54,7 +54,7 @@ export const retryNavigation = async ({
54
54
  * EXPECTATION: The element should disappear (become hidden) after clicking.
55
55
  *
56
56
  * @param {Object} options
57
- * @param {import('playwright').Locator} options.locator - The element to click
57
+ * @param {import('playwright-core').Locator} options.locator - The element to click
58
58
  * @param {number} [options.maxRetries=3] - Max retries
59
59
  * @param {number} [options.timeout=15000] - Timeout for visibility checks
60
60
  */
@@ -80,8 +80,8 @@ export const retryClick = async ({ locator, maxRetries = 3, timeout = 15000 }) =
80
80
  * Races multiple conditions (URL or Element) to see which happens first.
81
81
  * Modifies the input object by deleting the matched key.
82
82
  *
83
- * @param {import('playwright').Page} page
84
- * @param {Object.<string, string|import('playwright').Locator|null>} checksToPerform - Map of names to URL strings or Locators
83
+ * @param {import('playwright-core').Page} page
84
+ * @param {Object.<string, string|import('playwright-core').Locator|null>} checksToPerform - Map of names to URL strings or Locators
85
85
  * @param {number} timeout - Timeout in ms
86
86
  * @returns {Promise<string|null>} The key of the matched condition
87
87
  */
@@ -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
+ };
@@ -1,4 +1,4 @@
1
- import { Browser, BrowserContext, Page } from "playwright";
1
+ import { Browser, BrowserContext, Page } from "playwright-core";
2
2
  import type { FingerprintGeneratorOptions } from "fingerprint-generator";
3
3
  import type { HumanPage, CreateCursorOptions } from "./human-cursor/index";
4
4
  type Screen = FingerprintGeneratorOptions["screen"];
@@ -9,7 +9,7 @@ import fs from "fs";
9
9
  import path from "path";
10
10
  import os from "os";
11
11
  import crypto from "node:crypto";
12
- import { chromium, firefox } from "playwright";
12
+ import { chromium, firefox } from "playwright-core";
13
13
  import { setTimeout as sleep } from "timers/promises";
14
14
 
15
15
  // Fingerprint management
@@ -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) {
@@ -966,11 +999,27 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
966
999
  page = createCursor(page, humanize_options);
967
1000
  }
968
1001
 
1002
+ let closing = false;
969
1003
  const closeBrowser = async () => {
1004
+ if (closing) return false;
1005
+ closing = true;
1006
+ if (signalHandler) {
1007
+ process.off("SIGINT", signalHandler);
1008
+ process.off("SIGTERM", signalHandler);
1009
+ process.off("SIGHUP", signalHandler);
1010
+ }
970
1011
  if (browser) await browser.close().catch(() => { });
971
1012
  return await stopMultiloginProfile(profileId);
972
1013
  };
973
1014
 
1015
+ let signalHandler = async () => {
1016
+ await closeBrowser();
1017
+ process.exit(0);
1018
+ };
1019
+ process.once("SIGINT", signalHandler);
1020
+ process.once("SIGTERM", signalHandler);
1021
+ process.once("SIGHUP", signalHandler);
1022
+
974
1023
  return { browser, context, page, isBrowserRunning: () => !!browser, closeBrowser, launchError: null };
975
1024
  } catch (error) {
976
1025
  console.error("Multilogin Launch Error:", error.message);
@@ -1040,11 +1089,27 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
1040
1089
  page = createCursor(page, humanize_options);
1041
1090
  }
1042
1091
 
1092
+ let closing = false;
1043
1093
  const closeBrowser = async () => {
1094
+ if (closing) return false;
1095
+ closing = true;
1096
+ if (signalHandler) {
1097
+ process.off("SIGINT", signalHandler);
1098
+ process.off("SIGTERM", signalHandler);
1099
+ process.off("SIGHUP", signalHandler);
1100
+ }
1044
1101
  if (browser) await browser.close().catch(() => { });
1045
1102
  return await stopMultiloginProfile(profileId);
1046
1103
  };
1047
1104
 
1105
+ let signalHandler = async () => {
1106
+ await closeBrowser();
1107
+ process.exit(0);
1108
+ };
1109
+ process.once("SIGINT", signalHandler);
1110
+ process.once("SIGTERM", signalHandler);
1111
+ process.once("SIGHUP", signalHandler);
1112
+
1048
1113
  return { browser, context, page, isBrowserRunning: () => !!browser, closeBrowser, launchError: null };
1049
1114
  } catch (error) {
1050
1115
  console.error("Quick Profile Error:", error);
@@ -1,4 +1,4 @@
1
- import { Browser, BrowserContext, Page } from "playwright";
1
+ import { Browser, BrowserContext, Page } from "playwright-core";
2
2
 
3
3
  // ============================================================================
4
4
  // ROUTING & CACHE TYPES
@@ -0,0 +1,70 @@
1
+ import { Page, Locator } from "puppeteer-core";
2
+
3
+ /**
4
+ * Options for the retryNavigation function.
5
+ */
6
+ export interface RetryNavigationOptions {
7
+ /** The Puppeteer Page object. */
8
+ page: Page;
9
+ /** The target URL to navigate to. */
10
+ url: string;
11
+ /** Maximum number of retry attempts. Default: 5 */
12
+ maxRetries?: number;
13
+ /** Custom Referer header. Default: null */
14
+ referer?: string | null;
15
+ /** Base timeout in milliseconds. Default: 30000 */
16
+ timeout?: number;
17
+ /** When to consider operation succeeded. Default: "load" */
18
+ waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
19
+ }
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
+
37
+ /**
38
+ * Navigates to a URL with retry logic and incremental timeouts.
39
+ * If navigation fails, it temporarily goes to "about:blank" before retrying.
40
+ * @returns True if navigation succeeded, throws error otherwise.
41
+ */
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>;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Navigates to a URL with retry logic and incremental timeouts.
3
+ * If navigation fails, it temporarily goes to "about:blank" before retrying.
4
+ *
5
+ * @param {Object} options
6
+ * @param {import('puppeteer-core').Page} options.page - Puppeteer Page object
7
+ * @param {string} options.url - Target URL
8
+ * @param {number} [options.maxRetries=5] - Maximum number of attempts
9
+ * @param {string|null} [options.referer=null] - Referer header
10
+ * @param {number} [options.timeout=30000] - Base timeout in ms
11
+ * @param {"load"|"domcontentloaded"|"networkidle0"|"networkidle2"} [options.waitUntil="load"] - Wait condition
12
+ * @returns {Promise<boolean>} True if successful
13
+ */
14
+ export const retryNavigation = async ({
15
+ page,
16
+ url,
17
+ maxRetries = 5,
18
+ referer = null,
19
+ timeout = 30000,
20
+ waitUntil = "load",
21
+ }) => {
22
+ for (let retryCount = 0; retryCount < maxRetries; retryCount++) {
23
+ // Apply incremental timeout only if the initial timeout is <= 30,000
24
+ const currentTimeout = timeout <= 30000 ? timeout + retryCount * 15000 : timeout;
25
+ try {
26
+ const gotoOptions = {
27
+ waitUntil: waitUntil,
28
+ timeout: currentTimeout,
29
+ };
30
+
31
+ if (referer) {
32
+ gotoOptions.referer = referer;
33
+ }
34
+
35
+ await page.goto(url, gotoOptions);
36
+
37
+ return true;
38
+ } catch (error) {
39
+ console.log(`Navigation attempt ${retryCount + 1} failed.`, url);
40
+ await page.goto("about:blank", { waitUntil: "load", timeout: timeout });
41
+ if (retryCount === maxRetries - 1) {
42
+ console.log("All retry attempts failed");
43
+ throw error;
44
+ }
45
+ }
46
+ }
47
+ return false;
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",
@@ -612,13 +652,29 @@ async function launchExistingMultiloginProfile(profileId) {
612
652
  const pages = await browser.pages();
613
653
  const page = pages[0] ?? (await browser.newPage());
614
654
 
655
+ let closing = false;
615
656
  const closeBrowser = async () => {
657
+ if (closing) return false;
658
+ closing = true;
659
+ if (signalHandler) {
660
+ process.off("SIGINT", signalHandler);
661
+ process.off("SIGTERM", signalHandler);
662
+ process.off("SIGHUP", signalHandler);
663
+ }
616
664
  try {
617
665
  if (browser?.connected) await browser.close();
618
666
  } catch { }
619
667
  return await stopMultiloginProfile(profileId);
620
668
  };
621
669
 
670
+ let signalHandler = async () => {
671
+ await closeBrowser();
672
+ process.exit(0);
673
+ };
674
+ process.once("SIGINT", signalHandler);
675
+ process.once("SIGTERM", signalHandler);
676
+ process.once("SIGHUP", signalHandler);
677
+
622
678
  return { browser, context: browser, page, isBrowserRunning: () => !!browser?.connected, closeBrowser, launchError: null };
623
679
  } catch (error) {
624
680
  console.error("Multilogin Launch Error:", error.message);
@@ -685,13 +741,29 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
685
741
  const pages = await browser.pages();
686
742
  const page = pages[0] ?? (await browser.newPage());
687
743
 
744
+ let closing = false;
688
745
  const closeBrowser = async () => {
746
+ if (closing) return false;
747
+ closing = true;
748
+ if (signalHandler) {
749
+ process.off("SIGINT", signalHandler);
750
+ process.off("SIGTERM", signalHandler);
751
+ process.off("SIGHUP", signalHandler);
752
+ }
689
753
  try {
690
754
  if (browser?.connected) await browser.close();
691
755
  } catch { }
692
756
  return await stopMultiloginProfile(profileId);
693
757
  };
694
758
 
759
+ let signalHandler = async () => {
760
+ await closeBrowser();
761
+ process.exit(0);
762
+ };
763
+ process.once("SIGINT", signalHandler);
764
+ process.once("SIGTERM", signalHandler);
765
+ process.once("SIGHUP", signalHandler);
766
+
695
767
  return { browser, context: browser, page, isBrowserRunning: () => !!browser?.connected, closeBrowser, launchError: null };
696
768
  } catch (error) {
697
769
  console.error("Quick Profile Error:", error);