arn-browser 0.1.18 → 0.1.20

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/bin/install.js CHANGED
@@ -29,14 +29,14 @@ import { createWriteStream } from "fs";
29
29
 
30
30
  const BRAVE_URLS = {
31
31
  linux: {
32
- x64: "https://github.com/brave/brave-browser/releases/download/v1.87.192/brave-browser-1.87.192-linux-amd64.zip",
33
- arm64: "https://github.com/brave/brave-browser/releases/download/v1.87.192/brave-browser-1.87.192-linux-arm64.zip",
32
+ x64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/brave-browser-1.88.134-linux-amd64.zip",
33
+ arm64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/brave-browser-1.88.134-linux-arm64.zip",
34
34
  },
35
35
  darwin: {
36
- arm64: "https://github.com/brave/brave-browser/releases/download/v1.87.192/Brave-Browser-arm64.dmg",
36
+ arm64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/Brave-Browser-arm64.dmg",
37
37
  },
38
38
  win32: {
39
- x64: "https://github.com/brave/brave-browser/releases/download/v1.87.192/brave-v1.87.192-win32-x64.zip",
39
+ x64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/brave-v1.88.134-win32-x64.zip",
40
40
  }
41
41
  };
42
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arn-browser",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "A lightweight, browser autmation helper.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -12,21 +12,21 @@
12
12
  "test": "test"
13
13
  },
14
14
  "dependencies": {
15
- "@aws-sdk/client-ec2": "^3.953.0",
15
+ "@aws-sdk/client-ec2": "^3.1015.0",
16
16
  "@ghostery/adblocker": "^2.13.0",
17
17
  "arn-knexjs": "^0.0.3",
18
- "camoufox-js": "^0.9.1",
18
+ "camoufox-js": "^0.9.3",
19
19
  "dotenv": "^17.2.3",
20
20
  "fingerprint-generator": "^2.1.78",
21
21
  "fingerprint-injector": "^2.1.78",
22
- "https-proxy-agent": "^7.0.6",
22
+ "https-proxy-agent": "^8.0.0",
23
23
  "node-cache": "^5.1.2",
24
24
  "node-fetch": "^3.3.2",
25
25
  "playwright-core": "1.42.1",
26
26
  "proxy-chain": "^2.6.0",
27
- "puppeteer-core": "^24.38.0",
27
+ "puppeteer-core": "^24.40.0",
28
28
  "randomstring": "^1.3.1",
29
- "socks-proxy-agent": "^8.0.5",
29
+ "socks-proxy-agent": "^9.0.0",
30
30
  "speakeasy": "^2.0.0",
31
31
  "superagent": "^10.2.3"
32
32
  },
@@ -34,7 +34,8 @@
34
34
  "arn-browser": "bin/cli.js"
35
35
  },
36
36
  "scripts": {
37
- "test": "echo \"Error: no test specified\" && exit 1"
37
+ "test": "echo \"Error: no test specified\" && exit 1",
38
+ "postinstall": "npm install playwright-core@1.42.1 --no-save"
38
39
  },
39
40
  "keywords": [
40
41
  "browser",
@@ -110,10 +110,12 @@ export interface MultiloginOptions {
110
110
  audio_masking?: boolean;
111
111
 
112
112
  /**
113
- * Multilogin flag: Custom screen resolution.
114
- * Default: false
113
+ * Multilogin flag: Screen resolution masking.
114
+ * true = randomize screen resolution ("mask")
115
+ * false = use real screen resolution ("natural")
116
+ * Default: true
115
117
  */
116
- custom_screen?: boolean;
118
+ screen_masking?: boolean;
117
119
  }
118
120
 
119
121
  /**
@@ -192,9 +194,12 @@ export interface PwLaunchOptions {
192
194
  * Configuration for browser fingerprint spoofing.
193
195
  *
194
196
  * - `false`: No fingerprint spoofing (default)
195
- * - `true`: Spoof fingerprint with viewport: null (maximized window)
196
- * - `{ minWidth?, maxWidth?, minHeight?, maxHeight? }`: Spoof fingerprint with screen constraints
197
- * and use the generated screen dimensions as viewport.
197
+ * - `true`: Spoof fingerprint with defaults (detected OS, auto device, viewport: null)
198
+ * - `object`: Spoof fingerprint with optional overrides
199
+ *
200
+ * Device type is auto-determined from OS:
201
+ * - `"windows"`, `"macos"`, `"linux"` → desktop
202
+ * - `"android"`, `"ios"` → mobile
198
203
  *
199
204
  * **For persistent Brave profiles:** The value is locked on first launch.
200
205
  * Subsequent launches will use the saved value, ignoring the current parameter.
@@ -204,15 +209,39 @@ export interface PwLaunchOptions {
204
209
  * spoof_fingerprint: true
205
210
  *
206
211
  * @example
207
- * // Spoof with specific screen constraints
208
- * spoof_fingerprint: { minWidth: 1366, maxWidth: 1920, minHeight: 768 }
212
+ * // Spoof as Windows
213
+ * spoof_fingerprint: { os_type: "windows" }
214
+ *
215
+ * @example
216
+ * // Random OS per launch
217
+ * spoof_fingerprint: { os_type: ["windows", "macos"] }
218
+ *
219
+ * @example
220
+ * // Mixed desktop + mobile
221
+ * spoof_fingerprint: { os_type: ["windows", "android"] }
209
222
  *
210
223
  * Default: false
211
224
  */
212
225
  spoof_fingerprint?: boolean | {
226
+ /**
227
+ * Override the OS for fingerprint generation.
228
+ * Can be a single value or array (random pick per launch).
229
+ * Device type is auto-determined: windows/macos/linux → desktop, android/ios → mobile
230
+ * Default: detected OS of the machine
231
+ */
232
+ os_type?: ("windows" | "macos" | "linux" | "android" | "ios") | ("windows" | "macos" | "linux" | "android" | "ios")[];
233
+ /**
234
+ * Minimum browser version for fingerprint generation.
235
+ * Default: 141
236
+ */
237
+ minBrowserVersion?: number;
238
+ /** Minimum screen width constraint */
213
239
  minWidth?: number;
240
+ /** Maximum screen width constraint */
214
241
  maxWidth?: number;
242
+ /** Minimum screen height constraint */
215
243
  minHeight?: number;
244
+ /** Maximum screen height constraint */
216
245
  maxHeight?: number;
217
246
  };
218
247
 
@@ -37,6 +37,10 @@ const osMap = {
37
37
  };
38
38
  const detectedOs = osMap[process.platform] || "windows";
39
39
 
40
+ // Default minimum browser versions for fingerprint generation
41
+ const DEFAULT_MIN_CHROME_VERSION = 141;
42
+ const DEFAULT_MIN_FIREFOX_VERSION = 141;
43
+
40
44
  const MULTILOGIN_LAUNCHER_URL = "https://launcher.mlx.yt:45001";
41
45
  const MULTILOGIN_FOLDER_ID = "bad9e7e1-cfab-4c8d-bd19-91aa82929711";
42
46
 
@@ -227,36 +231,80 @@ function cleanUpProfiles(skipPath) {
227
231
  scanDir(TEMP_DIR);
228
232
  }
229
233
 
234
+ /**
235
+ * Maps OS to its device type.
236
+ */
237
+ const OS_TO_DEVICE = {
238
+ windows: "desktop",
239
+ macos: "desktop",
240
+ linux: "desktop",
241
+ android: "mobile",
242
+ ios: "mobile",
243
+ };
244
+
230
245
  /**
231
246
  * Helper to generate consistent fingerprint options.
232
247
  * @param {string} browserType - 'chromium' | 'firefox' | 'brave'
233
- * @param {object|null} screenOptions - Optional screen constraints { minWidth, maxWidth, minHeight, maxHeight }
248
+ * @param {object|null} spoofOptions - Options from spoof_fingerprint (null if boolean true)
249
+ * Supported properties:
250
+ * - os_type: string | string[] — e.g. "windows" or ["windows", "macos"]. Default: detectedOs
251
+ * - minBrowserVersion: number — minimum browser version for fingerprint. Default: 141
252
+ * - minWidth, maxWidth, minHeight, maxHeight: screen constraints
234
253
  */
235
- function getFingerprintConfig(browserType, screenOptions = null) {
254
+ function getFingerprintConfig(browserType, spoofOptions = null) {
255
+ let selectedOs = detectedOs;
256
+ let selectedDevice = OS_TO_DEVICE[detectedOs] || "desktop";
257
+
258
+ if (spoofOptions && typeof spoofOptions === 'object' && spoofOptions.os_type) {
259
+ const osList = Array.isArray(spoofOptions.os_type) ? spoofOptions.os_type : [spoofOptions.os_type];
260
+
261
+ if (osList.length === 0) {
262
+ throw new Error(`░░░░░ [Fingerprint] os_type array is empty.`);
263
+ }
264
+
265
+ // Validate all values
266
+ for (const os of osList) {
267
+ if (!OS_TO_DEVICE[os]) {
268
+ throw new Error(
269
+ `░░░░░ [Fingerprint] Unknown os_type: "${os}".\n` +
270
+ ` Supported: "windows", "macos", "linux" (desktop), "android", "ios" (mobile)`
271
+ );
272
+ }
273
+ }
274
+
275
+ // Random pick one
276
+ selectedOs = osList[Math.floor(Math.random() * osList.length)];
277
+ selectedDevice = OS_TO_DEVICE[selectedOs];
278
+ }
279
+
280
+ const minVersion = (spoofOptions && typeof spoofOptions === 'object' && spoofOptions.minBrowserVersion) || (browserType === "firefox" ? DEFAULT_MIN_FIREFOX_VERSION : DEFAULT_MIN_CHROME_VERSION);
281
+
236
282
  const config = {
237
- devices: ["desktop"],
238
- operatingSystems: [detectedOs],
283
+ devices: [selectedDevice],
284
+ operatingSystems: [selectedOs],
239
285
  locales: ["en-US"],
240
286
  browsers: [],
241
287
  screen: {},
242
288
  };
243
289
 
244
290
  if (browserType === "chromium") {
245
- config.browsers.push({ name: "chrome", minVersion: 141 });
291
+ config.browsers.push({ name: "chrome", minVersion });
246
292
  } else if (browserType === "firefox") {
247
- config.browsers.push({ name: "firefox", minVersion: 141 });
293
+ config.browsers.push({ name: "firefox", minVersion });
248
294
  } else if (browserType === "brave") {
249
- config.browsers.push({ name: "chrome", minVersion: 141 });
295
+ config.browsers.push({ name: "chrome", minVersion });
250
296
  }
251
297
 
252
298
  // Apply screen filter options if provided
253
- if (screenOptions && typeof screenOptions === 'object') {
254
- if (screenOptions.minWidth) config.screen.minWidth = screenOptions.minWidth;
255
- if (screenOptions.maxWidth) config.screen.maxWidth = screenOptions.maxWidth;
256
- if (screenOptions.minHeight) config.screen.minHeight = screenOptions.minHeight;
257
- if (screenOptions.maxHeight) config.screen.maxHeight = screenOptions.maxHeight;
299
+ if (spoofOptions && typeof spoofOptions === 'object') {
300
+ if (spoofOptions.minWidth) config.screen.minWidth = spoofOptions.minWidth;
301
+ if (spoofOptions.maxWidth) config.screen.maxWidth = spoofOptions.maxWidth;
302
+ if (spoofOptions.minHeight) config.screen.minHeight = spoofOptions.minHeight;
303
+ if (spoofOptions.maxHeight) config.screen.maxHeight = spoofOptions.maxHeight;
258
304
  }
259
305
 
306
+ if (_launchLogs) console.log(`░░░░░ Fingerprint config: OS=${selectedOs}, Device=${selectedDevice}, Browser=${browserType}`);
307
+
260
308
  return config;
261
309
  }
262
310
 
@@ -280,8 +328,11 @@ export async function pwLaunch({
280
328
 
281
329
  // Spoof Fingerprint
282
330
  // - false: No spoofing
283
- // - true: Spoof fingerprint with viewport: null (no screen constraint)
284
- // - { minWidth?, maxWidth?, minHeight?, maxHeight? }: Spoof with screen constraints
331
+ // - true: Spoof fingerprint with defaults (detectedOs, auto device, viewport: null)
332
+ // - object: Spoof with overrides:
333
+ // os_type?: string | string[] (e.g. "windows" or ["windows", "macos"]) — random pick if multiple
334
+ // minBrowserVersion?: number — minimum browser version (default: 141)
335
+ // minWidth?, maxWidth?, minHeight?, maxHeight?: screen constraints
285
336
  spoof_fingerprint = false,
286
337
 
287
338
  // Browser Specific Grouped Options
@@ -441,15 +492,13 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
441
492
  const proxyObj = formatProxy(proxy);
442
493
  const tz = timezoneId || undefined;
443
494
 
444
- // Determine screen options from spoof_fingerprint if it's an object
445
- const screenOptions = (spoof_fingerprint && typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
446
-
447
495
  // ==================================================================
448
496
  // BRANCH A: TEMP PROFILE (launch + newInjectedContext)
449
497
  // ==================================================================
450
498
  if (!isPersistent) {
451
499
  try {
452
- const fpConfig = getFingerprintConfig("chromium", screenOptions);
500
+ const spoofOptions = (spoof_fingerprint && typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
501
+ const fpConfig = getFingerprintConfig("chromium", spoofOptions);
453
502
  const fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
454
503
 
455
504
  // Launch standard browser (not persistent context)
@@ -558,9 +607,6 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
558
607
  const proxyObj = formatProxy(proxy);
559
608
  const tz = timezoneId || undefined;
560
609
 
561
- // Determine screen options from spoof_fingerprint if it's an object
562
- const screenOptions = (spoof_fingerprint && typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
563
-
564
610
  // Firefox specific preferences
565
611
  const firefoxUserPrefs = {
566
612
  "dom.webdriver.enabled": false,
@@ -573,7 +619,8 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
573
619
  // ==================================================================
574
620
  if (!isPersistent) {
575
621
  try {
576
- const fpConfig = getFingerprintConfig("firefox", screenOptions);
622
+ const spoofOptions = (spoof_fingerprint && typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
623
+ const fpConfig = getFingerprintConfig("firefox", spoofOptions);
577
624
  const fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
578
625
 
579
626
  const browser = await firefox.launch({
@@ -673,13 +720,13 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
673
720
  }
674
721
  }
675
722
 
676
- // Determine screen options from spoof_fingerprint if it's an object
677
- const screenOptions = (effectiveSpoofFingerprint && typeof effectiveSpoofFingerprint === 'object') ? effectiveSpoofFingerprint : null;
723
+ // Determine spoof options from effectiveSpoofFingerprint if it's an object
724
+ const spoofOptions = (effectiveSpoofFingerprint && typeof effectiveSpoofFingerprint === 'object') ? effectiveSpoofFingerprint : null;
678
725
 
679
726
  // Determine if we should use screen viewport or null
680
727
  // - spoof_fingerprint = true (boolean) -> spoof with viewport: null
681
728
  // - spoof_fingerprint = { minWidth, maxWidth, ... } (object) -> spoof with generated screen viewport
682
- const useScreenViewport = screenOptions !== null;
729
+ const useScreenViewport = spoofOptions !== null;
683
730
 
684
731
  let fingerprintData;
685
732
  const fingerprintFilePath = path.join(activePath, "fingerprint.json");
@@ -687,8 +734,8 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
687
734
  if (fs.existsSync(fingerprintFilePath)) {
688
735
  fingerprintData = JSON.parse(fs.readFileSync(fingerprintFilePath, "utf-8"));
689
736
  } else {
690
- // Generate fingerprint with screen options (null if spoof_fingerprint is just true)
691
- const fpConfig = getFingerprintConfig("brave", screenOptions);
737
+ // Generate fingerprint with spoof options (null if spoof_fingerprint is just true)
738
+ const fpConfig = getFingerprintConfig("brave", spoofOptions);
692
739
  fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
693
740
 
694
741
  if (isPersistent) {
@@ -937,7 +984,7 @@ async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_opt
937
984
  canvas_noise = true,
938
985
  media_masking = true,
939
986
  audio_masking = true,
940
- custom_screen = false,
987
+ screen_masking = true,
941
988
  } = multilogin_options;
942
989
 
943
990
  if (profileId) {
@@ -949,6 +996,7 @@ async function multiloginLauncher({ proxy, multilogin_options = {}, humanize_opt
949
996
  canvas_noise,
950
997
  media_masking,
951
998
  audio_masking,
999
+ screen_masking,
952
1000
  humanize_options,
953
1001
  });
954
1002
  }
@@ -1030,7 +1078,7 @@ async function launchExistingMultiloginProfile(profileId, humanize_options = nul
1030
1078
  }
1031
1079
  }
1032
1080
 
1033
- async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, humanize_options = null }) {
1081
+ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, screen_masking, humanize_options = null }) {
1034
1082
  const createUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v3/profile/quick`;
1035
1083
  let browser, context, page, profileId;
1036
1084
 
@@ -1043,7 +1091,7 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
1043
1091
  flags: {
1044
1092
  audio_masking: audio_masking ? "mask" : "natural",
1045
1093
  media_devices_masking: media_masking ? "mask" : "natural",
1046
- screen_masking: "natural",
1094
+ screen_masking: screen_masking ? "mask" : "natural",
1047
1095
  canvas_noise: canvas_noise ? "mask" : "natural",
1048
1096
  proxy_masking: proxy ? "custom" : "disabled",
1049
1097
  },
@@ -32,6 +32,12 @@ export interface PwRouteOptions {
32
32
  /** Enable caching for requests */
33
33
  useCache?: boolean;
34
34
 
35
+ /** Strip cookies/auth from outgoing requests and sanitize CORS/CSP/set-cookie from responses (default: true) */
36
+ stripGotHeaders?: boolean;
37
+
38
+ /** Log stripped headers to console for debugging (default: false) */
39
+ stripGotLogger?: boolean;
40
+
35
41
  /**
36
42
  * Proxy for custom fetch requests (only used when useGot is true).
37
43
  * String: "http://host:port", "socks5://user:pass@host:port"
@@ -71,6 +71,48 @@ function createProxyAgent(proxyUrl) {
71
71
  return new HttpsProxyAgent(proxyUrl);
72
72
  }
73
73
 
74
+ /**
75
+ * Strips sensitive headers from outgoing request headers.
76
+ * Removes cookie and authorization to prevent session/IP correlation.
77
+ */
78
+ function sanitizeRequestHeaders(headers, logger, url) {
79
+ const cleaned = { ...headers };
80
+ const stripped = [];
81
+ // Remove known auth headers
82
+ if (cleaned["cookie"]) { stripped.push("cookie"); delete cleaned["cookie"]; }
83
+ if (cleaned["authorization"]) { stripped.push("authorization"); delete cleaned["authorization"]; }
84
+ // Remove any header containing auth/token/csrf keywords
85
+ for (const key of Object.keys(cleaned)) {
86
+ const lower = key.toLowerCase();
87
+ if (lower.includes("token") || lower.includes("csrf") || lower.includes("auth")) {
88
+ stripped.push(key);
89
+ delete cleaned[key];
90
+ }
91
+ }
92
+ if (logger && stripped.length) console.log(`[stripGot] Request stripped: [${stripped.join(", ")}] → ${url}`);
93
+ return cleaned;
94
+ }
95
+
96
+ /**
97
+ * Sanitizes response headers for safe caching across origins.
98
+ * - Replaces access-control-allow-origin with * (if present)
99
+ * - Removes access-control-allow-credentials, set-cookie, CSP, HSTS
100
+ */
101
+ function sanitizeResponseHeaders(headers, logger, url) {
102
+ const cleaned = { ...headers };
103
+ const changes = [];
104
+ if (cleaned["access-control-allow-origin"]) {
105
+ changes.push(`access-control-allow-origin: ${cleaned["access-control-allow-origin"]} → *`);
106
+ cleaned["access-control-allow-origin"] = "*";
107
+ }
108
+ if (cleaned["access-control-allow-credentials"]) { changes.push("access-control-allow-credentials"); delete cleaned["access-control-allow-credentials"]; }
109
+ if (cleaned["set-cookie"]) { changes.push("set-cookie"); delete cleaned["set-cookie"]; }
110
+ if (cleaned["content-security-policy"]) { changes.push("content-security-policy"); delete cleaned["content-security-policy"]; }
111
+ if (cleaned["strict-transport-security"]) { changes.push("strict-transport-security"); delete cleaned["strict-transport-security"]; }
112
+ if (logger && changes.length) console.log(`[stripGot] Response stripped: [${changes.join(", ")}] → ${url}`);
113
+ return cleaned;
114
+ }
115
+
74
116
  /**
75
117
  * Function to fetch resources using Superagent library with optional caching.
76
118
  * This mimics the browser's request but handles it in Node.js to allow caching or header manipulation.
@@ -81,9 +123,11 @@ function createProxyAgent(proxyUrl) {
81
123
  * @param {boolean} useFullUrl - Whether to use the full URL as cache key or just origin+path
82
124
  * @param {string|false} logger - Log level: "info" (success+error), "error" (errors only), false (no logs)
83
125
  * @param {Object|null} proxyAgent - Proxy agent to use for the request
126
+ * @param {boolean} stripHeaders - Whether to sanitize request/response headers
127
+ * @param {boolean} stripLogger - Whether to log stripped headers
84
128
  * @returns {Promise<Object>} - The response object containing status, headers, and body
85
129
  */
86
- async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent) {
130
+ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent, stripHeaders, stripLogger) {
87
131
  // Determine the cache key based on configuration
88
132
  let mainUrl = new URL(url).origin + new URL(url).pathname;
89
133
  if (useFullUrl) {
@@ -102,9 +146,12 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
102
146
  }
103
147
 
104
148
  try {
149
+ // Sanitize outgoing request headers if stripHeaders is enabled
150
+ const finalHeaders = stripHeaders ? sanitizeRequestHeaders(requestHeaders, stripLogger, url) : requestHeaders;
151
+
105
152
  // Fetch the resource using superagent
106
153
  // buffer(true) ensures we get the raw binary data (essential for images/fonts)
107
- let request = superagent(method, url).set(requestHeaders).buffer(true);
154
+ let request = superagent(method, url).set(finalHeaders).buffer(true);
108
155
 
109
156
  // Apply proxy agent if provided
110
157
  if (proxyAgent) {
@@ -116,11 +163,14 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
116
163
  // Determine the correct body type (Buffer for binary, text for others)
117
164
  const responseBody = response.body instanceof Buffer ? response.body : response.text;
118
165
 
166
+ // Sanitize response headers if stripHeaders is enabled
167
+ const finalResponseHeaders = stripHeaders ? sanitizeResponseHeaders(response.headers, stripLogger, url) : response.headers;
168
+
119
169
  // Save to cache only when caching is enabled
120
170
  if (useCache) {
121
171
  globalCache.set(mainUrl, {
122
172
  status: response.status,
123
- headers: response.headers,
173
+ headers: finalResponseHeaders,
124
174
  body: responseBody,
125
175
  });
126
176
  if (logger === "info") console.log(`Success (cached${viaProxy}): ${mainUrl}`);
@@ -130,7 +180,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
130
180
 
131
181
  return {
132
182
  status: response.status,
133
- headers: response.headers,
183
+ headers: finalResponseHeaders,
134
184
  body: responseBody,
135
185
  };
136
186
  } catch (error) {
@@ -165,6 +215,8 @@ export async function pwRoute({
165
215
  useGot = false,
166
216
  useFullUrl = true,
167
217
  useCache = true,
218
+ stripGotHeaders = true,
219
+ stripGotLogger = false,
168
220
  proxy = null,
169
221
  m4w_send_on_post = null,
170
222
  m4w_send_on_message = null,
@@ -357,7 +409,9 @@ export async function pwRoute({
357
409
  requestMethod,
358
410
  useFullUrl,
359
411
  logger,
360
- proxyAgent
412
+ proxyAgent,
413
+ stripGotHeaders,
414
+ stripGotLogger
361
415
  );
362
416
 
363
417
  if (response) {
@@ -60,6 +60,9 @@ export interface ProxyServerOptions {
60
60
  */
61
61
  ip2LocationKey?: string;
62
62
 
63
+ /** Retry delay in milliseconds between IP lookup attempts (default: 1500) */
64
+ retryDelayMs?: number;
65
+
63
66
  /** Enable verbose logging (default: false) */
64
67
  debug?: boolean;
65
68
  /** Track proxy usage statistics (default: true) */
@@ -129,3 +132,11 @@ export function startProxyServer(options: ProxyServerOptions): Promise<ProxyServ
129
132
  * @returns A promise resolving to GeoDetails (including timezoneId) or null if unreachable.
130
133
  */
131
134
  export function fetchPublicIP(proxyUrl: string | null): Promise<GeoDetails | null>;
135
+
136
+ /**
137
+ * Fetches proxy details (IP, country, city, timezone) with retries.
138
+ * @param proxyUrl The proxy URL string. Pass `null` for local fallback.
139
+ * @param apiKey Optional IP2Location API key.
140
+ * @param retryDelayMs Delay between retries in ms (default: 1500).
141
+ */
142
+ export function fetchProxyDetails(proxyUrl: string | null, apiKey?: string | null, retryDelayMs?: number): Promise<GeoDetails | null>;
@@ -2,6 +2,7 @@ import ProxyChain from "proxy-chain";
2
2
  import net from "net";
3
3
  import https from "https";
4
4
  import http from "http";
5
+ import { setTimeout as sleep } from "timers/promises";
5
6
  import { HttpsProxyAgent } from "https-proxy-agent";
6
7
  import { SocksProxyAgent } from "socks-proxy-agent";
7
8
 
@@ -100,7 +101,7 @@ export async function fetchPublicIP(proxyUrl) {
100
101
  * 1. If proxyUrl is provided -> Fetch Real Data (IP + Timezone).
101
102
  * 2. If proxyUrl is NULL/Undefined -> Return "Local" Mock immediately.
102
103
  */
103
- export async function fetchProxyDetails(proxyUrl, apiKey = null) {
104
+ export async function fetchProxyDetails(proxyUrl, apiKey = null, retryDelayMs = 1500) {
104
105
  // --- AUTOMATIC LOCAL FALLBACK ---
105
106
  if (!proxyUrl) {
106
107
  return {
@@ -111,56 +112,65 @@ export async function fetchProxyDetails(proxyUrl, apiKey = null) {
111
112
  };
112
113
  }
113
114
 
114
- // --- EXTERNAL LOOKUP (Only if proxy exists) ---
115
- const attempts = ["IP-API", "IP2Location"];
116
-
117
- for (const provider of attempts) {
118
- try {
119
- const isSocks = proxyUrl.startsWith("socks");
120
- const agent = isSocks ? new SocksProxyAgent(proxyUrl) : new HttpsProxyAgent(proxyUrl);
121
-
122
- let apiUrl;
123
-
124
- if (provider === "IP-API") {
125
- apiUrl = "http://ip-api.com/json/?fields=status,message,country,city,timezone,query";
126
- } else {
127
- const baseUrl = "https://api.ip2location.io/";
128
- const fields = "ip,country_name,city_name,time_zone,time_zone_olson";
129
- apiUrl = apiKey
130
- ? `${baseUrl}?key=${apiKey}&format=json&fields=${fields}`
131
- : `${baseUrl}?format=json&fields=${fields}`;
132
- }
133
-
134
- const jsonStr = await nativeGet(apiUrl, agent, 6000);
135
- const data = JSON.parse(jsonStr);
136
-
137
- if (provider === "IP-API") {
138
- if (data.status === "success") {
139
- return {
140
- ip: data.query,
141
- country: data.country,
142
- city: data.city,
143
- timezoneId: data.timezone || null,
144
- };
115
+ // --- EXTERNAL LOOKUP with retry (10 attempts) ---
116
+ const providers = ["IP-API", "IP2Location"];
117
+ const MAX_RETRIES = 10;
118
+
119
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
120
+ for (const provider of providers) {
121
+ try {
122
+ const isSocks = proxyUrl.startsWith("socks");
123
+ const agent = isSocks ? new SocksProxyAgent(proxyUrl) : new HttpsProxyAgent(proxyUrl);
124
+
125
+ let apiUrl;
126
+
127
+ if (provider === "IP-API") {
128
+ apiUrl = "http://ip-api.com/json/?fields=status,message,country,city,timezone,query";
129
+ } else {
130
+ const baseUrl = "https://api.ip2location.io/";
131
+ const fields = "ip,country_name,city_name,time_zone,time_zone_olson";
132
+ apiUrl = apiKey
133
+ ? `${baseUrl}?key=${apiKey}&format=json&fields=${fields}`
134
+ : `${baseUrl}?format=json&fields=${fields}`;
145
135
  }
146
- } else if (provider === "IP2Location") {
147
- if (data && data.ip) {
148
- const tz = data.time_zone_olson || null;
149
- const isValidId = tz && !tz.startsWith("+") && !tz.startsWith("-");
150
- return {
151
- ip: data.ip,
152
- country: data.country_name,
153
- city: data.city_name,
154
- timezoneId: isValidId ? tz : null,
155
- };
136
+
137
+ const jsonStr = await nativeGet(apiUrl, agent, 6000);
138
+ const data = JSON.parse(jsonStr);
139
+
140
+ if (provider === "IP-API") {
141
+ if (data.status === "success") {
142
+ return {
143
+ ip: data.query,
144
+ country: data.country,
145
+ city: data.city,
146
+ timezoneId: data.timezone || null,
147
+ };
148
+ }
149
+ } else if (provider === "IP2Location") {
150
+ if (data && data.ip) {
151
+ const tz = data.time_zone_olson || null;
152
+ const isValidId = tz && !tz.startsWith("+") && !tz.startsWith("-");
153
+ return {
154
+ ip: data.ip,
155
+ country: data.country_name,
156
+ city: data.city_name,
157
+ timezoneId: isValidId ? tz : null,
158
+ };
159
+ }
156
160
  }
161
+ } catch (err) {
162
+ // Silently fail to next provider
157
163
  }
158
- } catch (err) {
159
- // Silently fail to next provider
164
+ }
165
+
166
+ // Both providers failed, wait 1s before retrying
167
+ if (attempt < MAX_RETRIES) {
168
+ console.warn(`░░ Proxy IP lookup failed (attempt ${attempt}/${MAX_RETRIES}), retrying in ${retryDelayMs}ms...`);
169
+ await sleep(retryDelayMs);
160
170
  }
161
171
  }
162
172
 
163
- // If proxy was provided but failed to connect:
173
+ // All retries exhausted:
164
174
  return null;
165
175
  }
166
176
 
@@ -176,6 +186,7 @@ export async function startProxyServer({
176
186
  PROXY_2_HOSTS = [],
177
187
  NO_PROXY_HOSTS = [],
178
188
  ip2LocationKey = null,
189
+ retryDelayMs = 1500,
179
190
  debug = false,
180
191
  proxy_stats = true,
181
192
  host_stats = true,
@@ -212,9 +223,9 @@ export async function startProxyServer({
212
223
  // 4. Fetch Details (Simplified Logic)
213
224
  // We pass the URL (or null). The function handles the "Local" logic.
214
225
  const [defaultDetails, p1Details, p2Details] = await Promise.all([
215
- fetchProxyDetails(upstreamProxies.default, ip2LocationKey),
216
- fetchProxyDetails(upstreamProxies.p1, ip2LocationKey),
217
- fetchProxyDetails(upstreamProxies.p2, ip2LocationKey),
226
+ fetchProxyDetails(upstreamProxies.default, ip2LocationKey, retryDelayMs),
227
+ fetchProxyDetails(upstreamProxies.p1, ip2LocationKey, retryDelayMs),
228
+ fetchProxyDetails(upstreamProxies.p2, ip2LocationKey, retryDelayMs),
218
229
  ]);
219
230
 
220
231
  // Return null if any configured proxy is unreachable
@@ -224,10 +235,10 @@ export async function startProxyServer({
224
235
 
225
236
  // 5. Stats
226
237
  const stats = {
227
- DEFAULT_PROXY: { request: 0, Tx: 0, Rx: 0 },
228
- NO_PROXY: { request: 0, Tx: 0, Rx: 0 },
229
- PROXY_1: { request: 0, Tx: 0, Rx: 0 },
230
- PROXY_2: { request: 0, Tx: 0, Rx: 0 },
238
+ DEFAULT_PROXY: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
239
+ NO_PROXY: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
240
+ PROXY_1: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
241
+ PROXY_2: { request: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 },
231
242
  };
232
243
  // hostStatsMap is now categorized by proxy type
233
244
  const hostStatsMap = {
@@ -240,9 +251,6 @@ export async function startProxyServer({
240
251
  const connectionMap = {}; // Maps connectionId -> { type: "..." }
241
252
  let serverRunning = false;
242
253
 
243
- // Track all open sockets so we can force-destroy them on close
244
- const activeSockets = new Set();
245
-
246
254
  // 6. Server
247
255
  const server = new ProxyChain.Server({
248
256
  port: selectedPort,
@@ -293,7 +301,7 @@ export async function startProxyServer({
293
301
  // Ensure the type exists in map (it should, but safety first)
294
302
  if (!hostStatsMap[proxyType]) hostStatsMap[proxyType] = {};
295
303
  if (!hostStatsMap[proxyType][hostname]) {
296
- hostStatsMap[proxyType][hostname] = { req: 0, Tx: 0, Rx: 0 };
304
+ hostStatsMap[proxyType][hostname] = { req: 0, srcTx: 0, srcRx: 0, trgTx: 0, trgRx: 0 };
297
305
  }
298
306
  hostStatsMap[proxyType][hostname].req++;
299
307
  }
@@ -312,17 +320,26 @@ export async function startProxyServer({
312
320
 
313
321
  server.on("connectionClosed", ({ connectionId, stats: connStats }) => {
314
322
  const connectionInfo = connectionMap[connectionId];
315
- if (connectionInfo) {
323
+ if (connectionInfo && connStats) {
316
324
  const { type, hostname } = connectionInfo;
325
+ const srcTx = connStats.srcTxBytes || 0;
326
+ const srcRx = connStats.srcRxBytes || 0;
327
+ const trgTx = connStats.trgTxBytes || 0;
328
+ const trgRx = connStats.trgRxBytes || 0;
329
+
317
330
  if (proxy_stats && stats[type]) {
318
331
  stats[type].request++;
319
- stats[type].Tx += connStats.srcTxBytes;
320
- stats[type].Rx += connStats.srcRxBytes;
332
+ stats[type].srcTx += srcTx;
333
+ stats[type].srcRx += srcRx;
334
+ stats[type].trgTx += trgTx;
335
+ stats[type].trgRx += trgRx;
321
336
  }
322
- // Update host stats with Tx/Rx on connection close
337
+ // Update host stats
323
338
  if (host_stats && hostname && hostStatsMap[type] && hostStatsMap[type][hostname]) {
324
- hostStatsMap[type][hostname].Tx += connStats.srcTxBytes;
325
- hostStatsMap[type][hostname].Rx += connStats.srcRxBytes;
339
+ hostStatsMap[type][hostname].srcTx += srcTx;
340
+ hostStatsMap[type][hostname].srcRx += srcRx;
341
+ hostStatsMap[type][hostname].trgTx += trgTx;
342
+ hostStatsMap[type][hostname].trgRx += trgRx;
326
343
  }
327
344
  }
328
345
  delete connectionMap[connectionId];
@@ -331,38 +348,27 @@ export async function startProxyServer({
331
348
  try {
332
349
  await server.listen();
333
350
  serverRunning = true;
334
-
335
- // Track sockets so closeServer() can destroy them all
336
- server.server.on('connection', (socket) => {
337
- activeSockets.add(socket);
338
- socket.once('close', () => activeSockets.delete(socket));
339
-
340
- // Intercept pipe to catch the upstream target sockets created by proxy-chain
341
- const originalPipe = socket.pipe;
342
- socket.pipe = function(destination, options) {
343
- if (destination && typeof destination.destroy === 'function') {
344
- activeSockets.add(destination);
345
- destination.once('close', () => activeSockets.delete(destination));
346
- }
347
- return originalPipe.apply(this, arguments);
348
- };
349
- });
350
-
351
351
  console.log(`░░ Local Proxy Started: http://127.0.0.1:${selectedPort}`);
352
352
  } catch (err) {
353
353
  console.error("░░ Failed to start proxy server:", err);
354
354
  return null;
355
355
  }
356
356
 
357
- const formatBytes = (bytes) => (bytes / 1024 / 1024).toFixed(3);
357
+ const formatBytes = (bytes) => {
358
+ if (bytes < 1024) return bytes + " B";
359
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
360
+ return (bytes / (1024 * 1024)).toFixed(3) + " MB";
361
+ };
358
362
 
359
363
  const getProxyStatsFormatted = () => {
360
364
  const formatted = {};
361
365
  for (const [key, val] of Object.entries(stats)) {
362
366
  formatted[key] = {
363
367
  req: val.request,
364
- Tx: formatBytes(val.Tx) + " MB",
365
- Rx: formatBytes(val.Rx) + " MB",
368
+ srcTx: formatBytes(val.srcTx),
369
+ srcRx: formatBytes(val.srcRx),
370
+ trgTx: formatBytes(val.trgTx),
371
+ trgRx: formatBytes(val.trgRx),
366
372
  };
367
373
  }
368
374
  return formatted;
@@ -370,20 +376,20 @@ export async function startProxyServer({
370
376
 
371
377
  const getHostStatsFormatted = () => {
372
378
  const result = {};
373
- // Iterate over each proxy category
374
379
  for (const [type, hosts] of Object.entries(hostStatsMap)) {
375
380
  const sortedHosts = Object.entries(hosts)
376
- .sort((a, b) => b[1].req - a[1].req) // Sort by request count descending
381
+ .sort((a, b) => b[1].req - a[1].req)
377
382
  .reduce((acc, [host, hostData]) => {
378
383
  acc[host] = {
379
384
  req: hostData.req,
380
- Tx: formatBytes(hostData.Tx) + " MB",
381
- Rx: formatBytes(hostData.Rx) + " MB",
385
+ srcTx: formatBytes(hostData.srcTx),
386
+ srcRx: formatBytes(hostData.srcRx),
387
+ trgTx: formatBytes(hostData.trgTx),
388
+ trgRx: formatBytes(hostData.trgRx),
382
389
  };
383
390
  return acc;
384
391
  }, {});
385
392
 
386
- // Only include categories that have traffic
387
393
  if (Object.keys(sortedHosts).length > 0) {
388
394
  result[type] = sortedHosts;
389
395
  }
@@ -415,16 +421,11 @@ export async function startProxyServer({
415
421
  isServerRunning: () => serverRunning,
416
422
 
417
423
  closeServer: async () => {
424
+ // Close the server — triggers connectionClosed events which accumulate byte stats
418
425
  await server.close(true);
419
426
  serverRunning = false;
420
427
 
421
- // Force-destroy any lingering sockets so Node can exit
422
- for (const socket of activeSockets) {
423
- socket.destroy();
424
- }
425
- activeSockets.clear();
426
-
427
- // Auto console.log stats on close
428
+ // Log stats AFTER close so all connectionClosed events have fired
428
429
  if (proxy_stats) {
429
430
  console.log("░░ Proxy Stats:", getProxyStatsFormatted());
430
431
  }
@@ -33,6 +33,14 @@ export interface PpMultiloginOptions {
33
33
  * Default: true
34
34
  */
35
35
  audio_masking?: boolean;
36
+
37
+ /**
38
+ * Multilogin flag: Screen resolution masking.
39
+ * true = randomize screen resolution ("mask")
40
+ * false = use real screen resolution ("natural")
41
+ * Default: true
42
+ */
43
+ screen_masking?: boolean;
36
44
  }
37
45
 
38
46
  /**
@@ -116,15 +124,47 @@ export interface PpLaunchOptions {
116
124
  * Configuration for browser fingerprint spoofing (per-page via fingerprint-injector).
117
125
  *
118
126
  * - `false`: No fingerprint spoofing (default)
119
- * - `true`: Spoof fingerprint with default screen dimensions
120
- * - `{ minWidth?, maxWidth?, minHeight?, maxHeight? }`: Spoof fingerprint with screen constraints
127
+ * - `true`: Spoof fingerprint with default OS (detected) and auto device
128
+ * - `object`: Spoof fingerprint with optional overrides
129
+ *
130
+ * Device type is auto-determined from OS:
131
+ * - `"windows"`, `"macos"`, `"linux"` → desktop
132
+ * - `"android"`, `"ios"` → mobile
133
+ *
134
+ * @example
135
+ * // Spoof with defaults
136
+ * spoof_fingerprint: true
137
+ *
138
+ * @example
139
+ * // Spoof as Windows
140
+ * spoof_fingerprint: { os_type: "windows" }
141
+ *
142
+ * @example
143
+ * // Random OS per launch (desktop + mobile mixed)
144
+ * spoof_fingerprint: { os_type: ["windows", "android"] }
121
145
  *
122
146
  * Default: false
123
147
  */
124
148
  spoof_fingerprint?: boolean | {
149
+ /**
150
+ * Override the OS for fingerprint generation.
151
+ * Can be a single value or array (random pick per launch).
152
+ * Device type is auto-determined: windows/macos/linux → desktop, android/ios → mobile
153
+ * Default: detected OS of the machine
154
+ */
155
+ os_type?: ("windows" | "macos" | "linux" | "android" | "ios") | ("windows" | "macos" | "linux" | "android" | "ios")[];
156
+ /**
157
+ * Minimum Chrome version for fingerprint generation.
158
+ * Default: 141
159
+ */
160
+ minBrowserVersion?: number;
161
+ /** Minimum screen width constraint */
125
162
  minWidth?: number;
163
+ /** Maximum screen width constraint */
126
164
  maxWidth?: number;
165
+ /** Minimum screen height constraint */
127
166
  minHeight?: number;
167
+ /** Maximum screen height constraint */
128
168
  maxHeight?: number;
129
169
  };
130
170
 
@@ -41,26 +41,73 @@ let _cleanupLogs = false;
41
41
  // Detect OS for fingerprint generation
42
42
  const detectedOs = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
43
43
 
44
+ // Default minimum browser version for fingerprint generation
45
+ const DEFAULT_MIN_CHROME_VERSION = 141;
46
+
47
+ /**
48
+ * Maps OS to its device type.
49
+ */
50
+ const OS_TO_DEVICE = {
51
+ windows: "desktop",
52
+ macos: "desktop",
53
+ linux: "desktop",
54
+ android: "mobile",
55
+ ios: "mobile",
56
+ };
57
+
44
58
  /**
45
59
  * Helper to generate consistent fingerprint options for Puppeteer.
46
- * @param {object|null} screenOptions - Optional screen constraints { minWidth, maxWidth, minHeight, maxHeight }
60
+ * @param {object|null} spoofOptions - Options from spoof_fingerprint (null if boolean true)
61
+ * Supported properties:
62
+ * - os_type: string | string[] — e.g. "windows" or ["windows", "macos"]. Default: detectedOs
63
+ * - minBrowserVersion: number — minimum Chrome version for fingerprint. Default: 141
64
+ * - minWidth, maxWidth, minHeight, maxHeight: screen constraints
47
65
  */
48
- function getFingerprintConfig(screenOptions = null) {
66
+ function getFingerprintConfig(spoofOptions = null) {
67
+ let selectedOs = detectedOs;
68
+ let selectedDevice = OS_TO_DEVICE[detectedOs] || "desktop";
69
+
70
+ if (spoofOptions && typeof spoofOptions === 'object' && spoofOptions.os_type) {
71
+ const osList = Array.isArray(spoofOptions.os_type) ? spoofOptions.os_type : [spoofOptions.os_type];
72
+
73
+ if (osList.length === 0) {
74
+ throw new Error(`░░░░░ [Fingerprint] os_type array is empty.`);
75
+ }
76
+
77
+ // Validate all values
78
+ for (const os of osList) {
79
+ if (!OS_TO_DEVICE[os]) {
80
+ throw new Error(
81
+ `░░░░░ [Fingerprint] Unknown os_type: "${os}".\n` +
82
+ ` Supported: "windows", "macos", "linux" (desktop), "android", "ios" (mobile)`
83
+ );
84
+ }
85
+ }
86
+
87
+ // Random pick one
88
+ selectedOs = osList[Math.floor(Math.random() * osList.length)];
89
+ selectedDevice = OS_TO_DEVICE[selectedOs];
90
+ }
91
+
92
+ const minVersion = (spoofOptions && typeof spoofOptions === 'object' && spoofOptions.minBrowserVersion) || DEFAULT_MIN_CHROME_VERSION;
93
+
49
94
  const config = {
50
- devices: ["desktop"],
51
- operatingSystems: [detectedOs],
95
+ devices: [selectedDevice],
96
+ operatingSystems: [selectedOs],
52
97
  locales: ["en-US"],
53
- browsers: [{ name: "chrome", minVersion: 141 }],
98
+ browsers: [{ name: "chrome", minVersion }],
54
99
  screen: {},
55
100
  };
56
101
 
57
- if (screenOptions && typeof screenOptions === 'object') {
58
- if (screenOptions.minWidth) config.screen.minWidth = screenOptions.minWidth;
59
- if (screenOptions.maxWidth) config.screen.maxWidth = screenOptions.maxWidth;
60
- if (screenOptions.minHeight) config.screen.minHeight = screenOptions.minHeight;
61
- if (screenOptions.maxHeight) config.screen.maxHeight = screenOptions.maxHeight;
102
+ if (spoofOptions && typeof spoofOptions === 'object') {
103
+ if (spoofOptions.minWidth) config.screen.minWidth = spoofOptions.minWidth;
104
+ if (spoofOptions.maxWidth) config.screen.maxWidth = spoofOptions.maxWidth;
105
+ if (spoofOptions.minHeight) config.screen.minHeight = spoofOptions.minHeight;
106
+ if (spoofOptions.maxHeight) config.screen.maxHeight = spoofOptions.maxHeight;
62
107
  }
63
108
 
109
+ if (_launchLogs) console.log(`░░░░░ Fingerprint config: OS=${selectedOs}, Device=${selectedDevice}`);
110
+
64
111
  return config;
65
112
  }
66
113
 
@@ -230,8 +277,11 @@ export async function ppLaunch({
230
277
 
231
278
  // Spoof Fingerprint
232
279
  // - false: No spoofing
233
- // - true: Spoof with default screen
234
- // - { minWidth?, maxWidth?, minHeight?, maxHeight? }: Spoof with screen constraints
280
+ // - true: Spoof with defaults (detectedOs, auto device)
281
+ // - object: Spoof with overrides:
282
+ // os_type?: string | string[] (e.g. "windows" or ["windows", "macos"]) — random pick if multiple
283
+ // minBrowserVersion?: number — minimum Chrome version (default: 141)
284
+ // minWidth?, maxWidth?, minHeight?, maxHeight?: screen constraints
235
285
  spoof_fingerprint = false,
236
286
 
237
287
  // Multilogin
@@ -542,8 +592,8 @@ async function spawnAndConnect({ binaryPath, profilePath, isPersistent, proxy, e
542
592
  if (_launchLogs) console.log(`░░░░░ Loaded saved fingerprint for ${browserLabel}`);
543
593
  } else {
544
594
  // Generate new fingerprint
545
- const screenOptions = (typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
546
- const fpConfig = getFingerprintConfig(screenOptions);
595
+ const spoofOptions = (typeof spoof_fingerprint === 'object') ? spoof_fingerprint : null;
596
+ const fpConfig = getFingerprintConfig(spoofOptions);
547
597
  fingerprintData = new FingerprintGenerator().getFingerprint(fpConfig);
548
598
 
549
599
  // Save for persistent profiles
@@ -593,6 +643,7 @@ async function multiloginLauncher({ proxy, multilogin_options = {} }) {
593
643
  canvas_noise = true,
594
644
  media_masking = true,
595
645
  audio_masking = true,
646
+ screen_masking = true,
596
647
  } = multilogin_options;
597
648
 
598
649
  if (profileId) {
@@ -604,6 +655,7 @@ async function multiloginLauncher({ proxy, multilogin_options = {} }) {
604
655
  canvas_noise,
605
656
  media_masking,
606
657
  audio_masking,
658
+ screen_masking,
607
659
  });
608
660
  }
609
661
  }
@@ -683,7 +735,7 @@ async function launchExistingMultiloginProfile(profileId) {
683
735
  }
684
736
  }
685
737
 
686
- async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking }) {
738
+ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, media_masking, audio_masking, screen_masking }) {
687
739
  const createUrl = `${MULTILOGIN_LAUNCHER_URL}/api/v3/profile/quick`;
688
740
  let browser, profileId;
689
741
 
@@ -696,7 +748,7 @@ async function launchQuickMultiloginProfile({ os_type, proxy, canvas_noise, medi
696
748
  flags: {
697
749
  audio_masking: audio_masking ? "mask" : "natural",
698
750
  media_devices_masking: media_masking ? "mask" : "natural",
699
- screen_masking: "natural",
751
+ screen_masking: screen_masking ? "mask" : "natural",
700
752
  canvas_noise: canvas_noise ? "mask" : "natural",
701
753
  proxy_masking: proxy ? "custom" : "disabled",
702
754
  },
@@ -29,6 +29,12 @@ export interface PpRouteOptions {
29
29
  /** Enable caching for requests */
30
30
  useCache?: boolean;
31
31
 
32
+ /** Strip cookies/auth from outgoing requests and sanitize CORS/CSP/set-cookie from responses (default: true) */
33
+ stripGotHeaders?: boolean;
34
+
35
+ /** Log stripped headers to console for debugging (default: false) */
36
+ stripGotLogger?: boolean;
37
+
32
38
  /**
33
39
  * Proxy for custom fetch requests (only used when useGot is true).
34
40
  * String: "http://host:port", "socks5://user:pass@host:port"
@@ -71,6 +71,48 @@ function createProxyAgent(proxyUrl) {
71
71
  return new HttpsProxyAgent(proxyUrl);
72
72
  }
73
73
 
74
+ /**
75
+ * Strips sensitive headers from outgoing request headers.
76
+ * Removes cookie and authorization to prevent session/IP correlation.
77
+ */
78
+ function sanitizeRequestHeaders(headers, logger, url) {
79
+ const cleaned = { ...headers };
80
+ const stripped = [];
81
+ // Remove known auth headers
82
+ if (cleaned["cookie"]) { stripped.push("cookie"); delete cleaned["cookie"]; }
83
+ if (cleaned["authorization"]) { stripped.push("authorization"); delete cleaned["authorization"]; }
84
+ // Remove any header containing auth/token/csrf keywords
85
+ for (const key of Object.keys(cleaned)) {
86
+ const lower = key.toLowerCase();
87
+ if (lower.includes("token") || lower.includes("csrf") || lower.includes("auth")) {
88
+ stripped.push(key);
89
+ delete cleaned[key];
90
+ }
91
+ }
92
+ if (logger && stripped.length) console.log(`[stripGot] Request stripped: [${stripped.join(", ")}] → ${url}`);
93
+ return cleaned;
94
+ }
95
+
96
+ /**
97
+ * Sanitizes response headers for safe caching across origins.
98
+ * - Replaces access-control-allow-origin with * (if present)
99
+ * - Removes access-control-allow-credentials, set-cookie, CSP, HSTS
100
+ */
101
+ function sanitizeResponseHeaders(headers, logger, url) {
102
+ const cleaned = { ...headers };
103
+ const changes = [];
104
+ if (cleaned["access-control-allow-origin"]) {
105
+ changes.push(`access-control-allow-origin: ${cleaned["access-control-allow-origin"]} → *`);
106
+ cleaned["access-control-allow-origin"] = "*";
107
+ }
108
+ if (cleaned["access-control-allow-credentials"]) { changes.push("access-control-allow-credentials"); delete cleaned["access-control-allow-credentials"]; }
109
+ if (cleaned["set-cookie"]) { changes.push("set-cookie"); delete cleaned["set-cookie"]; }
110
+ if (cleaned["content-security-policy"]) { changes.push("content-security-policy"); delete cleaned["content-security-policy"]; }
111
+ if (cleaned["strict-transport-security"]) { changes.push("strict-transport-security"); delete cleaned["strict-transport-security"]; }
112
+ if (logger && changes.length) console.log(`[stripGot] Response stripped: [${changes.join(", ")}] → ${url}`);
113
+ return cleaned;
114
+ }
115
+
74
116
  /**
75
117
  * Function to fetch resources using Superagent library with optional caching.
76
118
  * This mimics the browser's request but handles it in Node.js to allow caching or header manipulation.
@@ -81,9 +123,11 @@ function createProxyAgent(proxyUrl) {
81
123
  * @param {boolean} useFullUrl - Whether to use the full URL as cache key or just origin+path
82
124
  * @param {string|false} logger - Log level: "info" (success+error), "error" (errors only), false (no logs)
83
125
  * @param {Object|null} proxyAgent - Proxy agent to use for the request
126
+ * @param {boolean} stripHeaders - Whether to sanitize request/response headers
127
+ * @param {boolean} stripLogger - Whether to log stripped headers
84
128
  * @returns {Promise<Object>} - The response object containing status, headers, and body
85
129
  */
86
- async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent) {
130
+ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl, logger, proxyAgent, stripHeaders, stripLogger) {
87
131
  // Determine the cache key based on configuration
88
132
  let mainUrl = new URL(url).origin + new URL(url).pathname;
89
133
  if (useFullUrl) {
@@ -102,9 +146,12 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
102
146
  }
103
147
 
104
148
  try {
149
+ // Sanitize outgoing request headers if stripHeaders is enabled
150
+ const finalHeaders = stripHeaders ? sanitizeRequestHeaders(requestHeaders, stripLogger, url) : requestHeaders;
151
+
105
152
  // Fetch the resource using superagent
106
153
  // buffer(true) ensures we get the raw binary data (essential for images/fonts)
107
- let request = superagent(method, url).set(requestHeaders).buffer(true);
154
+ let request = superagent(method, url).set(finalHeaders).buffer(true);
108
155
 
109
156
  // Apply proxy agent if provided
110
157
  if (proxyAgent) {
@@ -116,11 +163,14 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
116
163
  // Determine the correct body type (Buffer for binary, text for others)
117
164
  const responseBody = response.body instanceof Buffer ? response.body : response.text;
118
165
 
166
+ // Sanitize response headers if stripHeaders is enabled
167
+ const finalResponseHeaders = stripHeaders ? sanitizeResponseHeaders(response.headers, stripLogger, url) : response.headers;
168
+
119
169
  // Save to cache only when caching is enabled
120
170
  if (useCache) {
121
171
  globalCache.set(mainUrl, {
122
172
  status: response.status,
123
- headers: response.headers,
173
+ headers: finalResponseHeaders,
124
174
  body: responseBody,
125
175
  });
126
176
  if (logger === "info") console.log(`Success (cached${viaProxy}): ${mainUrl}`);
@@ -130,7 +180,7 @@ async function fetchWithClient(useCache, url, requestHeaders, method, useFullUrl
130
180
 
131
181
  return {
132
182
  status: response.status,
133
- headers: response.headers,
183
+ headers: finalResponseHeaders,
134
184
  body: responseBody,
135
185
  };
136
186
  } catch (error) {
@@ -163,6 +213,8 @@ export async function ppRoute({
163
213
  useGot = false,
164
214
  useFullUrl = true,
165
215
  useCache = true,
216
+ stripGotHeaders = true,
217
+ stripGotLogger = false,
166
218
  proxy = null,
167
219
  m4w_send_on_post = null,
168
220
  m4w_send_on_message = null,
@@ -360,7 +412,9 @@ export async function ppRoute({
360
412
  requestMethod,
361
413
  useFullUrl,
362
414
  logger,
363
- proxyAgent
415
+ proxyAgent,
416
+ stripGotHeaders,
417
+ stripGotLogger
364
418
  );
365
419
 
366
420
  if (response) {