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 +1 -1
- package/src/index.d.ts +56 -5
- package/src/index.js +12 -6
- package/src/utility/playwright/human-cursor/HumanCursor.js +26 -19
- package/src/utility/playwright/pwHelper.d.ts +62 -0
- package/src/utility/playwright/pwHelper.js +110 -1
- package/src/utility/playwright/pwLaunch.js +34 -1
- package/src/utility/playwright/routes/pwRoute.d.ts +2 -5
- package/src/utility/playwright/routes/pwRoute.js +9 -13
- package/src/utility/puppeteer/ppHelper.d.ts +107 -1
- package/src/utility/puppeteer/ppHelper.js +212 -0
- package/src/utility/puppeteer/ppLaunch.js +41 -1
- package/src/utility/puppeteer/routes/ppRoute.d.ts +2 -5
- package/src/utility/puppeteer/routes/ppRoute.js +9 -13
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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
|
|
18
|
-
|
|
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 {
|
|
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,
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
15
|
-
|
|
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 {
|
|
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,
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
309
|
+
if (logger) console.log("Continuing with normal request (fetchWithClient returned null):", url);
|
|
314
310
|
await request.continue();
|
|
315
311
|
return;
|
|
316
312
|
}
|