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 +1 -1
- package/src/index.d.ts +12 -6
- package/src/index.js +12 -6
- package/src/utility/playwright/human-cursor/HumanCursor.js +26 -19
- package/src/utility/playwright/pwHelper.js +81 -1
- package/src/utility/playwright/pwLaunch.js +34 -1
- package/src/utility/puppeteer/ppHelper.d.ts +45 -1
- package/src/utility/puppeteer/ppHelper.js +179 -0
- package/src/utility/puppeteer/ppLaunch.js +41 -1
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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",
|