arn-browser 0.1.13 → 0.1.15

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.15",
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
@@ -5,7 +5,7 @@ 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
7
  import { retryNavigation, retryClick, checkPageConditions } from "./utility/playwright/pwHelper";
8
- import { retryNavigation as ppRetryNavigation } from "./utility/puppeteer/ppHelper";
8
+ import { retryNavigation as ppRetryNavigation, retryClick as ppRetryClick, checkPageConditions as ppCheckPageConditions } from "./utility/puppeteer/ppHelper";
9
9
  import { startProxyServer, fetchPublicIP, fetchProxyDetails } from "./utility/proxy-utility/proxy-chain";
10
10
  import {
11
11
  fetchAwsProxy, getInstanceStatus, getPublicIpAddress,
@@ -17,20 +17,71 @@ import {
17
17
  import { get_multilogin_proxy, get_packetstream_proxy, get_x_proxy, fetchNextProxy } from "./utility/proxy-utility/proxy-helper";
18
18
  import { generateOTP } from "./others/totp-generator";
19
19
 
20
+ /** A single cookie object (CDP format) */
21
+ interface SessionCookie {
22
+ name: string;
23
+ value: string;
24
+ domain?: string;
25
+ path?: string;
26
+ expires?: number;
27
+ httpOnly?: boolean;
28
+ secure?: boolean;
29
+ sameSite?: string;
30
+ [key: string]: any;
31
+ }
32
+
33
+ /** A single localStorage entry for one origin */
34
+ interface LocalStorageEntry {
35
+ origin: string;
36
+ items: { name: string; value: string }[];
37
+ }
38
+
39
+ /** Session data result from export */
40
+ interface SessionData {
41
+ cookies?: SessionCookie[];
42
+ localStorage?: LocalStorageEntry[];
43
+ }
44
+
20
45
  export declare const ppBrowser: {
21
46
  launch: typeof ppLaunch;
22
47
  route: typeof ppRoute;
23
48
  cacheLogs: typeof ppCacheLogs;
24
- ppGoto: typeof ppRetryNavigation;
49
+ Goto: typeof ppRetryNavigation;
50
+ Click: typeof ppRetryClick;
51
+ Conditions: typeof ppCheckPageConditions;
52
+ exportSession: (options: {
53
+ page: import('puppeteer-core').Page;
54
+ cookies?: boolean;
55
+ localStorage?: boolean;
56
+ logger?: boolean;
57
+ }) => Promise<SessionData>;
58
+ importSession: (options: {
59
+ page: import('puppeteer-core').Page;
60
+ cookies?: SessionCookie[];
61
+ localStorage?: LocalStorageEntry[];
62
+ logger?: boolean;
63
+ }) => Promise<void>;
25
64
  };
26
65
 
27
66
  export declare const pwBrowser: {
28
67
  launch: typeof pwLaunch;
29
68
  route: typeof pwRoute;
30
69
  cacheLogs: typeof pwCacheLogs;
31
- pwGoto: typeof retryNavigation;
32
- pwClick: typeof retryClick;
33
- pwConditions: typeof checkPageConditions;
70
+ Goto: typeof retryNavigation;
71
+ Click: typeof retryClick;
72
+ Conditions: typeof checkPageConditions;
73
+ exportSession: (options: {
74
+ page: import('playwright-core').Page;
75
+ cookies?: boolean;
76
+ localStorage?: boolean;
77
+ logger?: boolean;
78
+ }) => Promise<SessionData>;
79
+ importSession: (options: {
80
+ page: import('playwright-core').Page;
81
+ cookies?: SessionCookie[];
82
+ localStorage?: LocalStorageEntry[];
83
+ logger?: boolean;
84
+ }) => Promise<void>;
34
85
  };
35
86
 
36
87
  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,
@@ -30,6 +30,55 @@ export interface RetryClickOptions {
30
30
  timeout?: number;
31
31
  }
32
32
 
33
+ /** A single cookie object (CDP format) */
34
+ export interface SessionCookie {
35
+ name: string;
36
+ value: string;
37
+ domain?: string;
38
+ path?: string;
39
+ expires?: number;
40
+ httpOnly?: boolean;
41
+ secure?: boolean;
42
+ sameSite?: string;
43
+ [key: string]: any;
44
+ }
45
+
46
+ /** A single localStorage entry for one origin */
47
+ export interface LocalStorageEntry {
48
+ origin: string;
49
+ items: { name: string; value: string }[];
50
+ }
51
+
52
+ /** Options for exportSession */
53
+ export interface ExportSessionOptions {
54
+ /** Playwright Page */
55
+ page: Page;
56
+ /** Export cookies. Default: true */
57
+ cookies?: boolean;
58
+ /** Export localStorage. Default: false */
59
+ localStorage?: boolean;
60
+ /** Log export progress. Default: true */
61
+ logger?: boolean;
62
+ }
63
+
64
+ /** Options for importSession */
65
+ export interface ImportSessionOptions {
66
+ /** Playwright Page */
67
+ page: Page;
68
+ /** Cookies to import */
69
+ cookies?: SessionCookie[];
70
+ /** localStorage entries to import */
71
+ localStorage?: LocalStorageEntry[];
72
+ /** Log import progress. Default: true */
73
+ logger?: boolean;
74
+ }
75
+
76
+ /** Session data result */
77
+ export interface SessionData {
78
+ cookies?: SessionCookie[];
79
+ localStorage?: LocalStorageEntry[];
80
+ }
81
+
33
82
  /**
34
83
  * Navigates to a URL with retry logic and incremental timeouts.
35
84
  * If navigation fails, it temporarily goes to "about:blank" before retrying.
@@ -59,3 +108,16 @@ export function checkPageConditions(
59
108
  checksToPerform: Record<string, string | Locator | null>,
60
109
  timeout: number
61
110
  ): Promise<string | null>;
111
+
112
+ /**
113
+ * Exports browser session (cookies and/or localStorage) via CDP.
114
+ * Silent operation — no browser blinking.
115
+ * cookies default: true, localStorage default: false
116
+ */
117
+ export function exportSession(options: ExportSessionOptions): Promise<SessionData>;
118
+
119
+ /**
120
+ * Imports browser session (cookies and/or localStorage) into a Playwright Browser.
121
+ * Imports whatever is provided. Order: localStorage first, then cookies.
122
+ */
123
+ export function importSession(options: ImportSessionOptions): Promise<void>;
@@ -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,112 @@ 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 and/or localStorage) via CDP.
133
+ * Silent operation — no browser blinking.
134
+ *
135
+ * @param {Object} options
136
+ * @param {import('playwright-core').Page} options.page - Playwright Page
137
+ * @param {boolean} [options.cookies=true] - Export cookies
138
+ * @param {boolean} [options.localStorage=false] - Export localStorage
139
+ * @param {boolean} [options.logger=true] - Log export progress
140
+ * @returns {Promise<{cookies?: Array, localStorage?: Array}>} Session data for JSONB storage
141
+ */
142
+ export const exportSession = async ({ page, cookies = true, localStorage = false, logger = true }) => {
143
+ const context = page.context();
144
+ const result = {};
145
+
146
+ if (cookies) {
147
+ const client = await context.newCDPSession(page);
148
+ const { cookies: allCookies } = await client.send("Network.getAllCookies");
149
+ result.cookies = allCookies;
150
+ await client.detach();
151
+ if (logger) console.log(`░░░░░ exportSession: ${allCookies.length} cookies exported`);
152
+ }
153
+
154
+ if (localStorage) {
155
+ result.localStorage = [];
156
+ const pages = context.pages();
157
+ for (const p of pages) {
158
+ const url = p.url();
159
+ if (!url || url === "about:blank" || url.startsWith("chrome")) continue;
160
+
161
+ try {
162
+ const origin = new URL(url).origin;
163
+ const items = await p.evaluate(() => {
164
+ const data = [];
165
+ for (let i = 0; i < window.localStorage.length; i++) {
166
+ const key = window.localStorage.key(i);
167
+ data.push({ name: key, value: window.localStorage.getItem(key) });
168
+ }
169
+ return data;
170
+ });
171
+
172
+ if (items.length > 0 && !result.localStorage.some((o) => o.origin === origin)) {
173
+ result.localStorage.push({ origin, items });
174
+ }
175
+ } catch (e) {
176
+ // Skip pages that can't be evaluated
177
+ }
178
+ }
179
+ if (logger) console.log(`░░░░░ exportSession: ${result.localStorage.length} localStorage origins exported`);
180
+ }
181
+
182
+ return result;
183
+ };
184
+
185
+ /**
186
+ * Imports browser session (cookies and/or localStorage) into a Playwright Browser.
187
+ * Imports whatever is provided. Order: localStorage first, then cookies.
188
+ *
189
+ * @param {Object} options
190
+ * @param {import('playwright-core').Page} options.page - Playwright Page
191
+ * @param {Array} [options.cookies] - Cookies to import
192
+ * @param {Array} [options.localStorage] - localStorage entries to import
193
+ * @param {boolean} [options.logger=true] - Log import progress
194
+ */
195
+ export const importSession = async ({ page, cookies, localStorage, logger = true }) => {
196
+ const context = page.context();
197
+
198
+ // localStorage first — navigate to each origin and set items
199
+ if (localStorage?.length) {
200
+ for (const entry of localStorage) {
201
+ if (!entry.items?.length) continue;
202
+
203
+ const p = await context.newPage();
204
+ try {
205
+ await p.goto(entry.origin, { waitUntil: "load", timeout: 15000 });
206
+ await p.waitForLoadState("domcontentloaded");
207
+ await p.evaluate((items) => {
208
+ for (const { name, value } of items) {
209
+ window.localStorage.setItem(name, value);
210
+ }
211
+ }, entry.items);
212
+ } catch (e) {
213
+ // Origin may redirect — try to set localStorage on the final page
214
+ try {
215
+ await p.waitForLoadState("load", { timeout: 10000 });
216
+ await p.evaluate((items) => {
217
+ for (const { name, value } of items) {
218
+ window.localStorage.setItem(name, value);
219
+ }
220
+ }, entry.items);
221
+ } catch (_) {
222
+ // Skip this origin if it can't be reached
223
+ }
224
+ } finally {
225
+ await p.close();
226
+ }
227
+ }
228
+ if (logger) console.log(`░░░░░ importSession: ${localStorage.length} localStorage origins imported`);
229
+ }
230
+
231
+ // Cookies via CDP — silent, no navigation needed
232
+ if (cookies?.length) {
233
+ const client = await context.newCDPSession(page);
234
+ await client.send("Network.setCookies", { cookies });
235
+ await client.detach();
236
+ if (logger) console.log(`░░░░░ importSession: ${cookies.length} cookies imported`);
237
+ }
238
+ };
@@ -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) {
@@ -14,11 +14,8 @@ export interface PwRouteOptions {
14
14
  /** Playwright Page (provide either context or page) */
15
15
  page?: Page | null;
16
16
 
17
- /** Log successful requests to console */
18
- successLogs?: boolean;
19
-
20
- /** Log failed requests to console */
21
- errorLogs?: boolean;
17
+ /** Log level: "info" (success+error), "error" (errors only), false (no logs). Default: "error" */
18
+ logger?: "info" | "error" | false;
22
19
 
23
20
  /** Block Ad requests (Ghostery engine) */
24
21
  blockAds?: boolean;
@@ -45,11 +45,10 @@ export function pwCacheLogs(log_cache = globalCache, interval = 10) {
45
45
  * @param {Object} requestHeaders - Request headers from the original request
46
46
  * @param {string} method - HTTP method (GET, POST, etc.)
47
47
  * @param {boolean} useFullUrl - Whether to use the full URL as cache key or just origin+path
48
- * @param {boolean} successLogs - Whether to log successful requests
49
- * @param {boolean} errorLogs - Whether to log error requests
48
+ * @param {string|false} logger - Log level: "info" (success+error), "error" (errors only), false (no logs)
50
49
  * @returns {Promise<Object>} - The response object containing status, headers, and body
51
50
  */
52
- async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, successLogs, errorLogs) {
51
+ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger) {
53
52
  // Determine the cache key based on configuration
54
53
  let mainUrl = new URL(url).origin + new URL(url).pathname;
55
54
  if (useFullUrl) {
@@ -60,7 +59,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
60
59
  if (useCache) {
61
60
  const cachedResponse = globalCache.get(mainUrl);
62
61
  if (cachedResponse) {
63
- if (successLogs) console.log(`Serving from globalCache: ${mainUrl}`);
62
+ if (logger === "info") console.log(`Serving from globalCache: ${mainUrl}`);
64
63
  return cachedResponse;
65
64
  }
66
65
  }
@@ -79,7 +78,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
79
78
  headers: response.headers,
80
79
  body: responseBody,
81
80
  });
82
- if (successLogs) console.log(`Success (cached): ${mainUrl}`);
81
+ if (logger === "info") console.log(`Success (cached): ${mainUrl}`);
83
82
 
84
83
  return {
85
84
  status: response.status,
@@ -87,7 +86,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
87
86
  body: responseBody,
88
87
  };
89
88
  } catch (error) {
90
- if (errorLogs) console.error(`Failed to fetch: ${url}`, error);
89
+ if (logger) console.error(`Failed to fetch: ${url}`, error);
91
90
  // We return undefined on error, which signals the route handler to fall back to normal request
92
91
  }
93
92
  }
@@ -97,10 +96,9 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
97
96
  * @param {Object} options - Configuration options
98
97
  * @param {Object} options.context - Playwright context (optional, one is required)
99
98
  * @param {Object} options.page - Playwright page (optional, one is required)
100
- * @param {boolean} options.successLogs - Enable logging for successful fetches
101
- * @param {boolean} options.errorLogs - Enable logging for failed fetches
102
99
  * @param {boolean} options.blockImage - Enable global image blocking
103
100
  * @param {boolean} options.blockAds - Enable Ghostery ad blocking
101
+ * @param {string|false} [options.logger="error"] - Log level: "info" (success+error), "error" (errors only), false (no logs)
104
102
  * @param {boolean} options.useGot - Enable custom fetching via Superagent (bypassing browser network stack for intercepted types)
105
103
  * @param {boolean} options.useFullUrl - Use full URL for cache keys
106
104
  * @param {boolean} options.useCache - Enable caching
@@ -112,8 +110,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
112
110
  export async function pwRoute({
113
111
  context = null,
114
112
  page = null,
115
- successLogs = false,
116
- errorLogs = false,
113
+ logger = false,
117
114
  blockAds = true,
118
115
  blockImage = true,
119
116
  useGot = true,
@@ -294,8 +291,7 @@ export async function pwRoute({
294
291
  requestHeaders,
295
292
  requestMethod,
296
293
  useFullUrl,
297
- successLogs,
298
- errorLogs
294
+ logger
299
295
  );
300
296
 
301
297
  if (response) {
@@ -306,7 +302,7 @@ export async function pwRoute({
306
302
  });
307
303
  return;
308
304
  } else {
309
- if (errorLogs) console.log("Continuing with normal request (fetchWithClient returned null):", url);
305
+ if (logger) console.log("Continuing with normal request (fetchWithClient returned null):", url);
310
306
  await route.continue();
311
307
  return;
312
308
  }
@@ -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,115 @@ 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
+
37
+ /** A single cookie object (CDP format) */
38
+ export interface SessionCookie {
39
+ name: string;
40
+ value: string;
41
+ domain?: string;
42
+ path?: string;
43
+ expires?: number;
44
+ httpOnly?: boolean;
45
+ secure?: boolean;
46
+ sameSite?: string;
47
+ [key: string]: any;
48
+ }
49
+
50
+ /** A single localStorage entry for one origin */
51
+ export interface LocalStorageEntry {
52
+ origin: string;
53
+ items: { name: string; value: string }[];
54
+ }
55
+
56
+ /** Options for exportSession */
57
+ export interface ExportSessionOptions {
58
+ /** Any Puppeteer Page */
59
+ page: Page;
60
+ /** Export cookies. Default: true */
61
+ cookies?: boolean;
62
+ /** Export localStorage. Default: false */
63
+ localStorage?: boolean;
64
+ /** Log export progress. Default: true */
65
+ logger?: boolean;
66
+ }
67
+
68
+ /** Options for importSession */
69
+ export interface ImportSessionOptions {
70
+ /** Any Puppeteer Page */
71
+ page: Page;
72
+ /** Cookies to import */
73
+ cookies?: SessionCookie[];
74
+ /** localStorage entries to import */
75
+ localStorage?: LocalStorageEntry[];
76
+ /** Log import progress. Default: true */
77
+ logger?: boolean;
78
+ }
79
+
80
+ /** Session data result */
81
+ export interface SessionData {
82
+ cookies?: SessionCookie[];
83
+ localStorage?: LocalStorageEntry[];
84
+ }
85
+
21
86
  /**
22
87
  * Navigates to a URL with retry logic and incremental timeouts.
23
88
  * If navigation fails, it temporarily goes to "about:blank" before retrying.
24
89
  * @returns True if navigation succeeded, throws error otherwise.
25
90
  */
26
91
  export function retryNavigation(options: RetryNavigationOptions): Promise<boolean>;
92
+
93
+ /**
94
+ * Retries clicking an element (by selector or locator).
95
+ * Warning: This function waits for the element to become HIDDEN after clicking.
96
+ * Use this for actions that close modals or navigate away.
97
+ */
98
+ export function retryClick(options: RetryClickOptions): Promise<void>;
99
+
100
+ /**
101
+ * Races multiple conditions (URL match or Locator visibility) to see which happens first.
102
+ *
103
+ * Value types in checksToPerform:
104
+ * - string → URL check (exact match, startsWith, or glob with *)
105
+ * - Locator → Element check via page.locator() (new-style API)
106
+ * - null → skipped
107
+ *
108
+ * NOTE: This function MUTATES the `checksToPerform` object by deleting the matched key.
109
+ *
110
+ * @param page - The Puppeteer Page object.
111
+ * @param checksToPerform - An object mapping keys (names) to URL strings, Locators, or null.
112
+ * @param timeout - Maximum time to wait for a condition in milliseconds.
113
+ * @returns The key name of the matched condition, or null if timed out.
114
+ */
115
+ export function checkPageConditions(
116
+ page: Page,
117
+ checksToPerform: Record<string, string | Locator<Node> | null>,
118
+ timeout: number
119
+ ): Promise<string | null>;
120
+
121
+ /**
122
+ * Exports browser session (cookies and/or localStorage) via CDP.
123
+ * Silent operation — no browser blinking.
124
+ * cookies default: true, localStorage default: false
125
+ */
126
+ export function exportSession(options: ExportSessionOptions): Promise<SessionData>;
127
+
128
+ /**
129
+ * Imports browser session (cookies and/or localStorage) into a Puppeteer Browser.
130
+ * Imports whatever is provided. Order: localStorage first, then cookies.
131
+ */
132
+ export function importSession(options: ImportSessionOptions): Promise<void>;
@@ -46,3 +46,215 @@ 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 and/or localStorage) via CDP.
155
+ * Silent operation — no browser blinking.
156
+ *
157
+ * @param {Object} options
158
+ * @param {import('puppeteer-core').Page} options.page - Any Puppeteer Page
159
+ * @param {boolean} [options.cookies=true] - Export cookies
160
+ * @param {boolean} [options.localStorage=false] - Export localStorage
161
+ * @param {boolean} [options.logger=true] - Log export progress
162
+ * @returns {Promise<{cookies?: Array, localStorage?: Array}>} Session data for JSONB storage
163
+ */
164
+ export const exportSession = async ({ page, cookies = true, localStorage = false, logger = true }) => {
165
+ const result = {};
166
+
167
+ if (cookies) {
168
+ const client = await page.createCDPSession();
169
+ const { cookies: allCookies } = await client.send("Network.getAllCookies");
170
+ result.cookies = allCookies;
171
+ await client.detach();
172
+ if (logger) console.log(`░░░░░ exportSession: ${allCookies.length} cookies exported`);
173
+ }
174
+
175
+ if (localStorage) {
176
+ result.localStorage = [];
177
+ const browser = page.browser();
178
+ const pages = await browser.pages();
179
+
180
+ for (const p of pages) {
181
+ const url = p.url();
182
+ if (!url || url === "about:blank" || url.startsWith("chrome")) continue;
183
+
184
+ try {
185
+ const origin = new URL(url).origin;
186
+ const items = await p.evaluate(() => {
187
+ const data = [];
188
+ for (let i = 0; i < window.localStorage.length; i++) {
189
+ const key = window.localStorage.key(i);
190
+ data.push({ name: key, value: window.localStorage.getItem(key) });
191
+ }
192
+ return data;
193
+ });
194
+
195
+ if (items.length > 0 && !result.localStorage.some((o) => o.origin === origin)) {
196
+ result.localStorage.push({ origin, items });
197
+ }
198
+ } catch (e) {
199
+ // Skip pages that can't be evaluated
200
+ }
201
+ }
202
+ if (logger) console.log(`░░░░░ exportSession: ${result.localStorage.length} localStorage origins exported`);
203
+ }
204
+
205
+ return result;
206
+ };
207
+
208
+ /**
209
+ * Imports browser session (cookies and/or localStorage) into a Puppeteer Browser.
210
+ * Imports whatever is provided. Order: localStorage first, then cookies.
211
+ *
212
+ * @param {Object} options
213
+ * @param {import('puppeteer-core').Page} options.page - Any Puppeteer Page
214
+ * @param {Array} [options.cookies] - Cookies to import
215
+ * @param {Array} [options.localStorage] - localStorage entries to import
216
+ * @param {boolean} [options.logger=true] - Log import progress
217
+ */
218
+ export const importSession = async ({ page, cookies, localStorage, logger = true }) => {
219
+ // localStorage first — navigate to each origin and set items
220
+ if (localStorage?.length) {
221
+ const browser = page.browser();
222
+
223
+ for (const entry of localStorage) {
224
+ if (!entry.items?.length) continue;
225
+
226
+ const p = await browser.newPage();
227
+ try {
228
+ await p.goto(entry.origin, { waitUntil: "load", timeout: 15000 });
229
+ await p.evaluate((items) => {
230
+ for (const { name, value } of items) {
231
+ window.localStorage.setItem(name, value);
232
+ }
233
+ }, entry.items);
234
+ } catch (e) {
235
+ // Origin may redirect — retry after load
236
+ try {
237
+ await p.waitForNavigation({ waitUntil: "load", timeout: 10000 }).catch(() => {});
238
+ await p.evaluate((items) => {
239
+ for (const { name, value } of items) {
240
+ window.localStorage.setItem(name, value);
241
+ }
242
+ }, entry.items);
243
+ } catch (_) {
244
+ // Skip this origin
245
+ }
246
+ } finally {
247
+ await p.close();
248
+ }
249
+ }
250
+ if (logger) console.log(`░░░░░ importSession: ${localStorage.length} localStorage origins imported`);
251
+ }
252
+
253
+ // Cookies via CDP — silent, no navigation needed
254
+ if (cookies?.length) {
255
+ const client = await page.createCDPSession();
256
+ await client.send("Network.setCookies", { cookies });
257
+ await client.detach();
258
+ if (logger) console.log(`░░░░░ importSession: ${cookies.length} cookies imported`);
259
+ }
260
+ };
@@ -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",
@@ -11,11 +11,8 @@ export interface PpRouteOptions {
11
11
  /** Puppeteer Page */
12
12
  page?: Page | null;
13
13
 
14
- /** Log successful requests to console */
15
- successLogs?: boolean;
16
-
17
- /** Log failed requests to console */
18
- errorLogs?: boolean;
14
+ /** Log level: "info" (success+error), "error" (errors only), false (no logs). Default: "error" */
15
+ logger?: "info" | "error" | false;
19
16
 
20
17
  /** Block Ad requests (Ghostery engine) */
21
18
  blockAds?: boolean;
@@ -45,11 +45,10 @@ export function ppCacheLogs(log_cache = globalCache, interval = 10) {
45
45
  * @param {Object} requestHeaders - Request headers from the original request
46
46
  * @param {string} method - HTTP method (GET, POST, etc.)
47
47
  * @param {boolean} useFullUrl - Whether to use the full URL as cache key or just origin+path
48
- * @param {boolean} successLogs - Whether to log successful requests
49
- * @param {boolean} errorLogs - Whether to log error requests
48
+ * @param {string|false} logger - Log level: "info" (success+error), "error" (errors only), false (no logs)
50
49
  * @returns {Promise<Object>} - The response object containing status, headers, and body
51
50
  */
52
- async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, successLogs, errorLogs) {
51
+ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger) {
53
52
  // Determine the cache key based on configuration
54
53
  let mainUrl = new URL(url).origin + new URL(url).pathname;
55
54
  if (useFullUrl) {
@@ -60,7 +59,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
60
59
  if (useCache) {
61
60
  const cachedResponse = globalCache.get(mainUrl);
62
61
  if (cachedResponse) {
63
- if (successLogs) console.log(`Serving from globalCache: ${mainUrl}`);
62
+ if (logger === "info") console.log(`Serving from globalCache: ${mainUrl}`);
64
63
  return cachedResponse;
65
64
  }
66
65
  }
@@ -79,7 +78,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
79
78
  headers: response.headers,
80
79
  body: responseBody,
81
80
  });
82
- if (successLogs) console.log(`Success (cached): ${mainUrl}`);
81
+ if (logger === "info") console.log(`Success (cached): ${mainUrl}`);
83
82
 
84
83
  return {
85
84
  status: response.status,
@@ -87,7 +86,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
87
86
  body: responseBody,
88
87
  };
89
88
  } catch (error) {
90
- if (errorLogs) console.error(`Failed to fetch: ${url}`, error);
89
+ if (logger) console.error(`Failed to fetch: ${url}`, error);
91
90
  // We return undefined on error, which signals the route handler to fall back to normal request
92
91
  }
93
92
  }
@@ -97,10 +96,9 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
97
96
  * @param {Object} options - Configuration options
98
97
  * @param {Object} options.context - Playwright context (optional, one is required)
99
98
  * @param {Object} options.page - Playwright page (optional, one is required)
100
- * @param {boolean} options.successLogs - Enable logging for successful fetches
101
- * @param {boolean} options.errorLogs - Enable logging for failed fetches
102
99
  * @param {boolean} options.blockImage - Enable global image blocking
103
100
  * @param {boolean} options.blockAds - Enable Ghostery ad blocking
101
+ * @param {string|false} [options.logger="error"] - Log level: "info" (success+error), "error" (errors only), false (no logs)
104
102
  * @param {boolean} options.useGot - Enable custom fetching via Superagent (bypassing browser network stack for intercepted types)
105
103
  * @param {boolean} options.useFullUrl - Use full URL for cache keys
106
104
  * @param {boolean} options.useCache - Enable caching
@@ -111,8 +109,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
111
109
  */
112
110
  export async function ppRoute({
113
111
  page = null,
114
- successLogs = false,
115
- errorLogs = false,
112
+ logger = false,
116
113
  blockAds = true,
117
114
  blockImage = true,
118
115
  useGot = true,
@@ -298,8 +295,7 @@ export async function ppRoute({
298
295
  requestHeaders,
299
296
  requestMethod,
300
297
  useFullUrl,
301
- successLogs,
302
- errorLogs
298
+ logger
303
299
  );
304
300
 
305
301
  if (response) {
@@ -310,7 +306,7 @@ export async function ppRoute({
310
306
  });
311
307
  return;
312
308
  } else {
313
- if (errorLogs) console.log("Continuing with normal request (fetchWithClient returned null):", url);
309
+ if (logger) console.log("Continuing with normal request (fetchWithClient returned null):", url);
314
310
  await request.continue();
315
311
  return;
316
312
  }