jumpy-lion 0.1.6-beta.7 → 1.0.0-beta.0

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.
Files changed (232) hide show
  1. package/README.md +1 -607
  2. package/browser/fonts/macos-bundle/40-macos-aliases.conf +515 -0
  3. package/browser/fonts/macos-bundle/README.md +24 -0
  4. package/browser/fonts/macos-bundle/install.sh +83 -0
  5. package/dist/browser-controller.d.ts +47 -4
  6. package/dist/browser-controller.d.ts.map +1 -1
  7. package/dist/browser-controller.js +219 -278
  8. package/dist/browser-controller.js.map +1 -1
  9. package/dist/browser-plugin.d.ts +82 -110
  10. package/dist/browser-plugin.d.ts.map +1 -1
  11. package/dist/browser-plugin.js +376 -379
  12. package/dist/browser-plugin.js.map +1 -1
  13. package/dist/browser-process/align-fingerprint-config.d.ts +36 -0
  14. package/dist/browser-process/align-fingerprint-config.d.ts.map +1 -0
  15. package/dist/browser-process/align-fingerprint-config.js +74 -0
  16. package/dist/browser-process/align-fingerprint-config.js.map +1 -0
  17. package/dist/browser-process/anti-detect-browser.d.ts +2 -2
  18. package/dist/browser-process/anti-detect-browser.js +7 -7
  19. package/dist/browser-process/anti-detect-config.d.ts +131 -2
  20. package/dist/browser-process/anti-detect-config.d.ts.map +1 -1
  21. package/dist/browser-process/anti-detect-config.js +337 -127
  22. package/dist/browser-process/anti-detect-config.js.map +1 -1
  23. package/dist/browser-process/browser.d.ts +25 -92
  24. package/dist/browser-process/browser.d.ts.map +1 -1
  25. package/dist/browser-process/browser.js +429 -73
  26. package/dist/browser-process/browser.js.map +1 -1
  27. package/dist/browser-process/fingerprint-config.d.ts +146 -0
  28. package/dist/browser-process/fingerprint-config.d.ts.map +1 -0
  29. package/dist/browser-process/fingerprint-config.js +308 -0
  30. package/dist/browser-process/fingerprint-config.js.map +1 -0
  31. package/dist/browser-process/get-chrome-executable.d.ts +6 -0
  32. package/dist/browser-process/get-chrome-executable.d.ts.map +1 -1
  33. package/dist/browser-process/get-chrome-executable.js +79 -4
  34. package/dist/browser-process/get-chrome-executable.js.map +1 -1
  35. package/dist/browser-process/gpu-family-profiles.d.ts +58 -0
  36. package/dist/browser-process/gpu-family-profiles.d.ts.map +1 -0
  37. package/dist/browser-process/gpu-family-profiles.js +480 -0
  38. package/dist/browser-process/gpu-family-profiles.js.map +1 -0
  39. package/dist/browser-process/launch-options.d.ts +301 -0
  40. package/dist/browser-process/launch-options.d.ts.map +1 -0
  41. package/dist/browser-process/launch-options.js +2 -0
  42. package/dist/browser-process/launch-options.js.map +1 -0
  43. package/dist/browser-process/port-allocator.d.ts +64 -0
  44. package/dist/browser-process/port-allocator.d.ts.map +1 -0
  45. package/dist/browser-process/port-allocator.js +198 -0
  46. package/dist/browser-process/port-allocator.js.map +1 -0
  47. package/dist/browser-process/process.d.ts +85 -0
  48. package/dist/browser-process/process.d.ts.map +1 -1
  49. package/dist/browser-process/process.js +235 -19
  50. package/dist/browser-process/process.js.map +1 -1
  51. package/dist/browser-process/signal-cleanup.d.ts +40 -0
  52. package/dist/browser-process/signal-cleanup.d.ts.map +1 -0
  53. package/dist/browser-process/signal-cleanup.js +105 -0
  54. package/dist/browser-process/signal-cleanup.js.map +1 -0
  55. package/dist/browser-process/widevine-cdm.d.ts +144 -0
  56. package/dist/browser-process/widevine-cdm.d.ts.map +1 -0
  57. package/dist/browser-process/widevine-cdm.js +298 -0
  58. package/dist/browser-process/widevine-cdm.js.map +1 -0
  59. package/dist/browser-profiles/chrome/populate-profile.d.ts.map +1 -1
  60. package/dist/browser-profiles/chrome/populate-profile.js +21 -22
  61. package/dist/browser-profiles/chrome/populate-profile.js.map +1 -1
  62. package/dist/connection/cdp-reconnection.d.ts +50 -0
  63. package/dist/connection/cdp-reconnection.d.ts.map +1 -0
  64. package/dist/connection/cdp-reconnection.js +75 -0
  65. package/dist/connection/cdp-reconnection.js.map +1 -0
  66. package/dist/connection/page-session.d.ts +114 -0
  67. package/dist/connection/page-session.d.ts.map +1 -0
  68. package/dist/connection/page-session.js +237 -0
  69. package/dist/connection/page-session.js.map +1 -0
  70. package/dist/connection/session-client.d.ts +34 -0
  71. package/dist/connection/session-client.d.ts.map +1 -0
  72. package/dist/connection/session-client.js +129 -0
  73. package/dist/connection/session-client.js.map +1 -0
  74. package/dist/crawler.d.ts +9 -88
  75. package/dist/crawler.d.ts.map +1 -1
  76. package/dist/crawler.js +157 -8
  77. package/dist/crawler.js.map +1 -1
  78. package/dist/fingerprinting/fingerprint-injector.d.ts +24 -114
  79. package/dist/fingerprinting/fingerprint-injector.d.ts.map +1 -1
  80. package/dist/fingerprinting/fingerprint-injector.js +305 -611
  81. package/dist/fingerprinting/fingerprint-injector.js.map +1 -1
  82. package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.d.ts +1 -1
  83. package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.d.ts.map +1 -1
  84. package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.js +16 -14
  85. package/dist/fingerprinting/fingerprint-overrides/datadome-bypass.js.map +1 -1
  86. package/dist/fingerprinting/fingerprint-overrides/index.d.ts +6 -2
  87. package/dist/fingerprinting/fingerprint-overrides/index.d.ts.map +1 -1
  88. package/dist/fingerprinting/fingerprint-overrides/index.js +6 -2
  89. package/dist/fingerprinting/fingerprint-overrides/index.js.map +1 -1
  90. package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.d.ts +1 -1
  91. package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.js +4 -4
  92. package/dist/fingerprinting/fingerprint-overrides/keyboard-humanization.js.map +1 -1
  93. package/dist/fingerprinting/fingerprint-overrides/locale-spoofing.js +4 -4
  94. package/dist/fingerprinting/fingerprint-overrides/navigator-override.d.ts +18 -0
  95. package/dist/fingerprinting/fingerprint-overrides/navigator-override.d.ts.map +1 -0
  96. package/dist/fingerprinting/fingerprint-overrides/navigator-override.js +136 -0
  97. package/dist/fingerprinting/fingerprint-overrides/navigator-override.js.map +1 -0
  98. package/dist/fingerprinting/fingerprint-overrides/override.d.ts +151 -0
  99. package/dist/fingerprinting/fingerprint-overrides/override.d.ts.map +1 -0
  100. package/dist/fingerprinting/fingerprint-overrides/override.js +14 -0
  101. package/dist/fingerprinting/fingerprint-overrides/override.js.map +1 -0
  102. package/dist/fingerprinting/fingerprint-overrides/platform-consistency.d.ts.map +1 -1
  103. package/dist/fingerprinting/fingerprint-overrides/platform-consistency.js +46 -15
  104. package/dist/fingerprinting/fingerprint-overrides/platform-consistency.js.map +1 -1
  105. package/dist/fingerprinting/fingerprint-overrides/registry.d.ts +43 -0
  106. package/dist/fingerprinting/fingerprint-overrides/registry.d.ts.map +1 -0
  107. package/dist/fingerprinting/fingerprint-overrides/registry.js +342 -0
  108. package/dist/fingerprinting/fingerprint-overrides/registry.js.map +1 -0
  109. package/dist/fingerprinting/fingerprint-overrides/screen-override.d.ts +16 -0
  110. package/dist/fingerprinting/fingerprint-overrides/screen-override.d.ts.map +1 -0
  111. package/dist/fingerprinting/fingerprint-overrides/screen-override.js +175 -0
  112. package/dist/fingerprinting/fingerprint-overrides/screen-override.js.map +1 -0
  113. package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.d.ts +1 -1
  114. package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.js +4 -4
  115. package/dist/fingerprinting/fingerprint-overrides/scroll-humanization.js.map +1 -1
  116. package/dist/fingerprinting/fingerprint-overrides/stealth-script.d.ts.map +1 -1
  117. package/dist/fingerprinting/fingerprint-overrides/stealth-script.js +75 -9
  118. package/dist/fingerprinting/fingerprint-overrides/stealth-script.js.map +1 -1
  119. package/dist/fingerprinting/fingerprint-overrides/ua-ch.d.ts +1 -1
  120. package/dist/fingerprinting/fingerprint-overrides/ua-ch.d.ts.map +1 -1
  121. package/dist/fingerprinting/fingerprint-overrides/ua-ch.js +34 -20
  122. package/dist/fingerprinting/fingerprint-overrides/ua-ch.js.map +1 -1
  123. package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.d.ts.map +1 -1
  124. package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.js +25 -6
  125. package/dist/fingerprinting/fingerprint-overrides/webgl-spoofing.js.map +1 -1
  126. package/dist/fingerprinting/fingerprint-overrides/webrtc-spoofing.js +1 -1
  127. package/dist/fingerprinting/hardware-realism.d.ts +83 -0
  128. package/dist/fingerprinting/hardware-realism.d.ts.map +1 -0
  129. package/dist/fingerprinting/hardware-realism.js +156 -0
  130. package/dist/fingerprinting/hardware-realism.js.map +1 -0
  131. package/dist/fingerprinting/injection-planner.d.ts +133 -0
  132. package/dist/fingerprinting/injection-planner.d.ts.map +1 -0
  133. package/dist/fingerprinting/injection-planner.js +497 -0
  134. package/dist/fingerprinting/injection-planner.js.map +1 -0
  135. package/dist/fingerprinting/locale-resolver.d.ts +8 -1
  136. package/dist/fingerprinting/locale-resolver.d.ts.map +1 -1
  137. package/dist/fingerprinting/locale-resolver.js +26 -3
  138. package/dist/fingerprinting/locale-resolver.js.map +1 -1
  139. package/dist/fingerprinting/non-apify-fingerprint-generator.d.ts +4 -4
  140. package/dist/fingerprinting/non-apify-fingerprint-generator.d.ts.map +1 -1
  141. package/dist/fingerprinting/non-apify-fingerprint-generator.js +24 -58
  142. package/dist/fingerprinting/non-apify-fingerprint-generator.js.map +1 -1
  143. package/dist/fingerprinting/non-apify-profiles.json +10515 -8494
  144. package/dist/fingerprinting/os-consistency.d.ts +28 -0
  145. package/dist/fingerprinting/os-consistency.d.ts.map +1 -0
  146. package/dist/fingerprinting/os-consistency.js +45 -0
  147. package/dist/fingerprinting/os-consistency.js.map +1 -0
  148. package/dist/fingerprinting/profile-quality.d.ts +25 -0
  149. package/dist/fingerprinting/profile-quality.d.ts.map +1 -0
  150. package/dist/fingerprinting/profile-quality.js +310 -0
  151. package/dist/fingerprinting/profile-quality.js.map +1 -0
  152. package/dist/fingerprinting/profile-selector.d.ts +102 -0
  153. package/dist/fingerprinting/profile-selector.d.ts.map +1 -0
  154. package/dist/fingerprinting/profile-selector.js +168 -0
  155. package/dist/fingerprinting/profile-selector.js.map +1 -0
  156. package/dist/fingerprinting/ua-alignment.d.ts +83 -0
  157. package/dist/fingerprinting/ua-alignment.d.ts.map +1 -0
  158. package/dist/fingerprinting/ua-alignment.js +197 -0
  159. package/dist/fingerprinting/ua-alignment.js.map +1 -0
  160. package/dist/fingerprinting/ua-ch-brands.d.ts +88 -0
  161. package/dist/fingerprinting/ua-ch-brands.d.ts.map +1 -0
  162. package/dist/fingerprinting/ua-ch-brands.js +174 -0
  163. package/dist/fingerprinting/ua-ch-brands.js.map +1 -0
  164. package/dist/index.d.ts +5 -0
  165. package/dist/index.d.ts.map +1 -1
  166. package/dist/index.js +6 -0
  167. package/dist/index.js.map +1 -1
  168. package/dist/input/dropdown-selector.d.ts +90 -0
  169. package/dist/input/dropdown-selector.d.ts.map +1 -0
  170. package/dist/input/dropdown-selector.js +394 -0
  171. package/dist/input/dropdown-selector.js.map +1 -0
  172. package/dist/input/element-target.d.ts +152 -0
  173. package/dist/input/element-target.d.ts.map +1 -0
  174. package/dist/input/element-target.js +461 -0
  175. package/dist/input/element-target.js.map +1 -0
  176. package/dist/input/input-emulator.d.ts +85 -0
  177. package/dist/input/input-emulator.d.ts.map +1 -0
  178. package/dist/input/input-emulator.js +325 -0
  179. package/dist/input/input-emulator.js.map +1 -0
  180. package/dist/input/input-transport.d.ts +60 -0
  181. package/dist/input/input-transport.d.ts.map +1 -0
  182. package/dist/input/input-transport.js +28 -0
  183. package/dist/input/input-transport.js.map +1 -0
  184. package/dist/input/recording-transport.d.ts +32 -0
  185. package/dist/input/recording-transport.d.ts.map +1 -0
  186. package/dist/input/recording-transport.js +43 -0
  187. package/dist/input/recording-transport.js.map +1 -0
  188. package/dist/keyboard-layout.d.ts +2 -2
  189. package/dist/keyboard-layout.js +2 -2
  190. package/dist/navigation/page-navigation.d.ts +67 -0
  191. package/dist/navigation/page-navigation.d.ts.map +1 -0
  192. package/dist/navigation/page-navigation.js +122 -0
  193. package/dist/navigation/page-navigation.js.map +1 -0
  194. package/dist/network/network-watch.d.ts +110 -0
  195. package/dist/network/network-watch.d.ts.map +1 -0
  196. package/dist/network/network-watch.js +223 -0
  197. package/dist/network/network-watch.js.map +1 -0
  198. package/dist/page.d.ts +118 -104
  199. package/dist/page.d.ts.map +1 -1
  200. package/dist/page.js +311 -1130
  201. package/dist/page.js.map +1 -1
  202. package/dist/session/inject-hook.d.ts +24 -0
  203. package/dist/session/inject-hook.d.ts.map +1 -0
  204. package/dist/session/inject-hook.js +80 -0
  205. package/dist/session/inject-hook.js.map +1 -0
  206. package/dist/session/save.d.ts +21 -0
  207. package/dist/session/save.d.ts.map +1 -0
  208. package/dist/session/save.js +163 -0
  209. package/dist/session/save.js.map +1 -0
  210. package/dist/session/tar-data-dir.d.ts +18 -0
  211. package/dist/session/tar-data-dir.d.ts.map +1 -0
  212. package/dist/session/tar-data-dir.js +105 -0
  213. package/dist/session/tar-data-dir.js.map +1 -0
  214. package/dist/session/types.d.ts +80 -0
  215. package/dist/session/types.d.ts.map +1 -0
  216. package/dist/session/types.js +29 -0
  217. package/dist/session/types.js.map +1 -0
  218. package/dist/session-profile.d.ts +102 -0
  219. package/dist/session-profile.d.ts.map +1 -0
  220. package/dist/session-profile.js +163 -0
  221. package/dist/session-profile.js.map +1 -0
  222. package/dist/tsconfig.build.tsbuildinfo +1 -1
  223. package/package.json +17 -6
  224. package/scripts/postinstall.cjs +58 -0
  225. package/dist/fingerprinting/custom-fingerprint-injector.d.ts +0 -87
  226. package/dist/fingerprinting/custom-fingerprint-injector.d.ts.map +0 -1
  227. package/dist/fingerprinting/custom-fingerprint-injector.js +0 -342
  228. package/dist/fingerprinting/custom-fingerprint-injector.js.map +0 -1
  229. package/dist/launcher-wrap.d.ts +0 -10
  230. package/dist/launcher-wrap.d.ts.map +0 -1
  231. package/dist/launcher-wrap.js +0 -11
  232. package/dist/launcher-wrap.js.map +0 -1
@@ -3,12 +3,13 @@ import fs from 'fs/promises';
3
3
  import os from 'os';
4
4
  import path from 'path';
5
5
  import { log } from '@crawlee/core';
6
- import { getChromeBinaryVersion, getChromeExecutablePath, getAntiDetectBrowserPath } from './get-chrome-executable.js';
6
+ import { isAntiDetectBrowserSync } from './anti-detect-browser.js';
7
+ import { writeTempFingerprintConfig } from './anti-detect-config.js';
8
+ import { getChromeExecutablePath, getAntiDetectBrowserPath } from './get-chrome-executable.js';
7
9
  import { BrowserProcess } from './process.js';
10
+ import { seedWidevineCdm } from './widevine-cdm.js';
8
11
  import { populateChromeProfile } from '../browser-profiles/chrome/populate-profile.js';
9
12
  import { createChromeProfile } from '../browser-profiles/index.js';
10
- import { isAntiDetectBrowserSync } from './anti-detect-browser.js';
11
- import { writeTempFingerprintConfig } from './anti-detect-config.js';
12
13
  const EXTENSION_PATH = path.resolve(import.meta.dirname, '../fingerprinting/all-fingerprint-defender');
13
14
  const EXTENSION_PATH2 = path.resolve(import.meta.dirname, '../fingerprinting/anti-webgpu');
14
15
  // --lang is intentionally NOT hardcoded below. It is appended per-session from the
@@ -18,25 +19,35 @@ const EXTENSION_PATH2 = path.resolve(import.meta.dirname, '../fingerprinting/ant
18
19
  // Flag policy (see ChromeDriver default-switch audit in git history for full evidence):
19
20
  //
20
21
  // Dropped as actively harmful:
21
- // --enable-features=NetworkService,NetworkServiceInProcess — desktop default is DISABLED
22
+ // --enable-features=NetworkService,NetworkServiceInProcess - desktop default is DISABLED
22
23
  // since M89; forcing it creates a runtime state no real user has.
23
- // --disable-ipc-flooding-protection — JS-probeable via 201+ history.pushState()/10s.
24
+ // --disable-ipc-flooding-protection - JS-probeable via 201+ history.pushState()/10s.
24
25
  //
25
26
  // Dropped because they have a concrete page-JS detection probe:
26
- // --disable-popup-blocking — window.open() null-vs-non-null from a timer.
27
- // --disable-renderer-backgrounding — document.visibilityState / rAF throttling.
28
- // --disable-backgrounding-occluded-windows — same surface; internally named
29
- // kDisableBackgroundingOccludedWindowsForTesting.
30
- // --disable-component-extensions-with-background-pages — speechSynthesis.getVoices()
27
+ // --disable-popup-blocking - window.open() null-vs-non-null from a timer.
28
+ // --disable-component-extensions-with-background-pages - speechSynthesis.getVoices()
31
29
  // leaks the missing TTS component extension.
32
30
  //
31
+ // Conditionally added based on which binary we launch:
32
+ // --disable-renderer-backgrounding + --disable-backgrounding-occluded-windows
33
+ // auto-enabled when launching the anti-detect binary; the binary's
34
+ // direct source-tree edit to document.visibilityState / document.hidden
35
+ // hides the rAF / setTimeout throttling tell, so these flags are safe.
36
+ // On vanilla Chrome they remain opt-in via CHROMIUM_KEEP_BACKGROUNDING=1.
37
+ // macOS's own compositor-pause-on-occlusion path is in
38
+ // WebContentsViewCocoa and has no command-line switch - the anti-detect
39
+ // binary drops the kOccluded transition directly in
40
+ // content/app_shim_remote_cocoa/web_contents_view_cocoa.mm.
41
+ //
33
42
  // Dropped because they are provably dead or placebo:
34
- // --disable-translate — string absent from Chromium switches files; noop since ~2018.
35
- // --disable-infobars — infobar_manager.cc:22-31 gate requires --headless OR a
43
+ // --disable-translate - string absent from Chromium switches files; noop since ~2018.
44
+ // --disable-infobars - infobar_manager.cc:22-31 gate requires --headless OR a
36
45
  // Chrome-for-Testing build; never active in our setup.
37
- // --disable-blink-features=AutomationControlled — only blocks the disabled-by-default
46
+ // --disable-blink-features=AutomationControlled - only blocks the disabled-by-default
38
47
  // RuntimeEnabledFeatures path; the actual webdriver-setter is
39
- // probe::ApplyAutomationOverride via CDP, which Cromite patch 0001 already no-ops.
48
+ // probe::ApplyAutomationOverride via CDP, which the anti-detect binary's
49
+ // direct edit to inspector_emulation_agent.cc / navigator.cc already no-ops
50
+ // (Navigator::webdriver() hardcoded to false).
40
51
  //
41
52
  // Dropped because they are network-signature flags absent from modern stealth tooling
42
53
  // (nodriver / Patchright) with no page-JS benefit on our setup:
@@ -48,6 +59,21 @@ const EXTENSION_PATH2 = path.resolve(import.meta.dirname, '../fingerprinting/ant
48
59
  * (a) no known page-JS observable probe, AND
49
60
  * (b) an operational or consistency justification (avoid startup dialogs, reduce
50
61
  * automation-irrelevant background work, match realistic fresh-profile defaults).
62
+ *
63
+ * Two operationally-detectable flags moved to opt-in env vars:
64
+ * - --disable-renderer-backgrounding / --disable-backgrounding-occluded-windows
65
+ * (rAF throttle probe): opt in via CHROMIUM_KEEP_BACKGROUNDING=1 for local
66
+ * headfull dev where window focus loss would otherwise stall the crawl. The
67
+ * document.hidden() C++ override (cromite patch D26) handles the JS-property
68
+ * side of the visibility spoof regardless.
69
+ * - --use-angle=swiftshader (WebGL perf probe + tell on macOS): opt in via
70
+ * CHROMIUM_USE_SWIFTSHADER=1 for canvas/WebGL pixel parity between local-mac
71
+ * (Metal-capable) and Apify-linux. Off by default lets ANGLE Metal/D3D/GL
72
+ * run natively. The anti-detect binary's direct source-tree edit to
73
+ * WebGLRenderingContextBase::getParameter (in
74
+ * third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc)
75
+ * still spoofs UNMASKED_VENDOR_WEBGL / UNMASKED_RENDERER_WEBGL regardless
76
+ * of which backend ANGLE picks.
51
77
  */
52
78
  const UNIVERSAL_STEALTH_ARGS = [
53
79
  // CDP transport: Chrome 111+ requires an origin allow-list for WebSocket connections
@@ -61,28 +87,118 @@ const UNIVERSAL_STEALTH_ARGS = [
61
87
  '--disable-session-crashed-bubble',
62
88
  // Suppress the EU search-engine choice modal, which would block a fresh profile.
63
89
  '--disable-search-engine-choice-screen',
64
- // Predictable startup URL — blank page instead of NTP fetch.
90
+ // Predictable startup URL - blank page instead of NTP fetch.
65
91
  '--homepage=about:blank',
66
92
  // Operational: do not show the "Page Unresponsive" dialog mid-crawl. No JS probe.
67
93
  '--disable-hang-monitor',
68
94
  // Operational: suppress the "Confirm form resubmission" dialog on POST reload. No JS probe.
69
95
  '--disable-prompt-on-repost',
70
96
  ];
97
+ /**
98
+ * Optional flags gated on env vars. Adding either of these to the launch argv
99
+ * narrows operational pain (headfull focus-loss stalls; canvas parity drift)
100
+ * but adds a fingerprint surface that real Chrome users do not have.
101
+ *
102
+ * Default-off so production crawls ship the cleanest argv we can manage. Local
103
+ * dev opts in via env. The anti-detect binary path also auto-enables the
104
+ * backgrounding flags - see `getAntiDetectBackgroundingArgs`.
105
+ */
106
+ function getOptionalStealthArgs() {
107
+ const out = [];
108
+ if (process.env.CHROMIUM_KEEP_BACKGROUNDING === '1'
109
+ || process.env.CHROMIUM_KEEP_BACKGROUNDING === 'true') {
110
+ // Keep the renderer running at full speed when the OS window loses focus
111
+ // or is occluded. Without these, headfull dev stalls when the user
112
+ // alt-tabs away - Chrome drops renderer priority and clamps setTimeout
113
+ // to ≥1s. (render_process_host_impl.cc; web_contents_impl.cc.)
114
+ //
115
+ // Trade-off: rAF/setTimeout drift while document.hidden===true is JS-probeable.
116
+ out.push('--disable-renderer-backgrounding');
117
+ out.push('--disable-backgrounding-occluded-windows');
118
+ }
119
+ if (process.env.CHROMIUM_USE_SWIFTSHADER === '1'
120
+ || process.env.CHROMIUM_USE_SWIFTSHADER === 'true') {
121
+ // Force ANGLE → SwiftShader so canvas/WebGL pixel output is identical
122
+ // between local-mac (Metal-capable) and Apify-linux (no GPU). Cromite's
123
+ // seeded pixel noise is a delta on top of a baseline; the baseline must
124
+ // match to make the final hash reproducible across hosts.
125
+ //
126
+ // Trade-off: WebGL perf benchmarks (timing a heavy scene) can detect
127
+ // SwiftShader on macOS hardware. Mainstream anti-bot vendors hash
128
+ // statically, but the flag is also visible in /proc/<pid>/cmdline on
129
+ // Linux when --no-sandbox is active.
130
+ out.push('--use-angle=swiftshader');
131
+ }
132
+ return out;
133
+ }
134
+ /**
135
+ * Backgrounding/occlusion suppression. Auto-enabled when launching the
136
+ * cromite anti-detect binary because cromite patch D26 pins
137
+ * `document.visibilityState` / `document.hidden` to "visible" at the
138
+ * Blink layer, so the rAF/setTimeout drift these flags would expose on
139
+ * vanilla Chrome is invisible to page JS under the patched binary.
140
+ *
141
+ * On macOS the equivalent of `CalculateNativeWinOcclusion` (the AURA
142
+ * Linux/Windows path) is `WebContentsViewCocoa.updateWebContentsVisibility`
143
+ * which has NO command-line kill switch - the kOccluded transition is
144
+ * hardcoded in content/app_shim_remote_cocoa/web_contents_view_cocoa.mm.
145
+ * That path is patched out in the cromite binary directly (see the
146
+ * "Anti-Detect: drop the kOccluded branch" patch), so no darwin-only
147
+ * launch flag is needed (or available) for the same effect.
148
+ *
149
+ * What these flags still do on macOS:
150
+ * --disable-renderer-backgrounding stops the renderer-process priority
151
+ * drop when our window is not the key window (CDP-driven sessions
152
+ * often never become key).
153
+ * --disable-backgrounding-occluded-windows stops the priority drop
154
+ * specifically on occlusion (separate from the visibility transition
155
+ * fixed by the cromite Cocoa patch).
156
+ */
157
+ function getAntiDetectBackgroundingArgs(_platform) {
158
+ return [
159
+ '--disable-renderer-backgrounding',
160
+ '--disable-backgrounding-occluded-windows',
161
+ ];
162
+ }
71
163
  /**
72
164
  * Linux-specific flags. Necessary for server-side operation; no page-JS probe on any:
73
165
  * --no-sandbox: running as root or in a rootless container where seccomp-bpf is
74
166
  * unavailable. Not observable from page JS (/proc/self/status is not reachable
75
- * from the renderer).
76
- * --disable-dev-shm-usage: Docker's default /dev/shm is 64 MB — renderers crash when
167
+ * from the renderer). Can be opted out with $CHROMIUM_KEEP_SANDBOX=1 when the
168
+ * container is rootless AND the host kernel allows user namespaces - e.g.
169
+ * modern Apify runners that drop privileges to UID 1000. The kernel-level
170
+ * sandbox is a small extra defense and removing the --no-sandbox flag
171
+ * reduces /proc/<pid>/cmdline noise.
172
+ * --disable-dev-shm-usage: Docker's default /dev/shm is 64 MB - renderers crash when
77
173
  * it fills. Backing switches to /tmp. Not observable from page JS.
78
174
  * --password-store=basic: avoid blocking on gnome-keyring/kwallet prompts. Not
79
175
  * observable from page JS.
80
176
  */
81
- const LINUX_STEALTH_ARGS = [
82
- '--no-sandbox',
177
+ const LINUX_STEALTH_ARGS_BASE = [
83
178
  '--disable-dev-shm-usage',
84
179
  '--password-store=basic',
85
180
  ];
181
+ /**
182
+ * Build the Linux flag list at runtime so the $CHROMIUM_KEEP_SANDBOX opt-out is
183
+ * picked up per-launch (process env can change between calls in long-running
184
+ * processes, e.g. when an Apify actor sets the var at startup).
185
+ *
186
+ * `--disable-setuid-sandbox` ships alongside `--no-sandbox`: even with
187
+ * `--no-sandbox`, Chrome may try to call the setuid sandbox helper
188
+ * (chrome_sandbox / chrome-sandbox) at startup; on restricted containers
189
+ * (Apify LIMITED_PERMISSIONS, Docker rootless) the helper isn't installed
190
+ * or can't bind, and Chrome hangs waiting on it instead of crashing -
191
+ * presents to the caller as a silent CDP-connect timeout. When
192
+ * CHROMIUM_KEEP_SANDBOX=1, both flags drop so the user-namespace sandbox
193
+ * can engage normally.
194
+ */
195
+ function getLinuxStealthArgs() {
196
+ const keepSandbox = process.env.CHROMIUM_KEEP_SANDBOX === '1'
197
+ || process.env.CHROMIUM_KEEP_SANDBOX === 'true';
198
+ return keepSandbox
199
+ ? LINUX_STEALTH_ARGS_BASE
200
+ : ['--no-sandbox', '--disable-setuid-sandbox', ...LINUX_STEALTH_ARGS_BASE];
201
+ }
86
202
  /**
87
203
  * macOS-specific flags. `--use-mock-keychain` substitutes the real Keychain with an
88
204
  * in-memory mock to avoid blocking-permission dialogs. Not observable from page JS.
@@ -94,28 +210,63 @@ const DARWIN_STEALTH_ARGS = [
94
210
  * Build the stealth argv list. Exported for unit tests so we can assert exact flag
95
211
  * sets without booting a browser.
96
212
  *
97
- * The `usingAntiDetectBrowser` flag is retained as a hook for future native-stealth-only
98
- * divergence, but is not currently needed — every flag in this set is either covered
99
- * identically by Cromite's C++ patches (no conflict) or orthogonal to them.
213
+ * When `usingAntiDetectBrowser` is true, the renderer-backgrounding and
214
+ * (on macOS) native-window-occlusion features are disabled regardless of
215
+ * env vars, because cromite patch D26 hides the only page-JS tell those
216
+ * flags would otherwise expose (document.hidden / rAF throttling). This
217
+ * fixes the "page goes blank until I refocus the window" symptom on
218
+ * headful macOS launches driven by CDP.
100
219
  */
101
220
  export function buildStealthArgs(env = {}) {
102
221
  const platform = env.platform ?? process.platform;
103
- const args = [...UNIVERSAL_STEALTH_ARGS];
222
+ const args = [...UNIVERSAL_STEALTH_ARGS, ...getOptionalStealthArgs()];
223
+ if (env.usingAntiDetectBrowser) {
224
+ for (const flag of getAntiDetectBackgroundingArgs(platform)) {
225
+ if (!args.includes(flag))
226
+ args.push(flag);
227
+ }
228
+ }
104
229
  if (platform === 'linux')
105
- args.push(...LINUX_STEALTH_ARGS);
230
+ args.push(...getLinuxStealthArgs());
106
231
  if (platform === 'darwin')
107
232
  args.push(...DARWIN_STEALTH_ARGS);
108
233
  return args;
109
234
  }
235
+ /**
236
+ * Process-layer launch options: everything in BaseLaunchBrowserOptions
237
+ * (the single shared declaration in launch-options.ts) plus the spawn-time
238
+ * fields only this layer knows about.
239
+ */
240
+ /**
241
+ * Resolve the platform-overrides config for an anti-detect browser launch, or
242
+ * THROW. The anti-detect binary exists to spoof; launching it without a config
243
+ * silently ships the host identity (navigator.platform / userAgent / WebGL all
244
+ * real) - the "30% of sessions are detectable and nobody notices" failure mode.
245
+ *
246
+ * Resolution order: the per-session config (stamped by browser-plugin.ts) first,
247
+ * then the static browser/config/default_fingerprint.json. If both are absent
248
+ * (per-session missing AND the default loader fails or yields nothing), this
249
+ * throws so the launch aborts instead of leaking. `loadStaticDefault` is injected
250
+ * so the dynamic-import path can be scripted in tests.
251
+ */
252
+ export async function resolveAntiDetectFingerprintConfig(perSessionConfig, loadStaticDefault) {
253
+ const resolved = perSessionConfig ?? await loadStaticDefault().catch(() => null);
254
+ if (resolved == null) {
255
+ throw new Error('[AntiDetect] No fingerprint config resolved for the anti-detect browser '
256
+ + '(per-session config absent and browser/config/default_fingerprint.json unavailable). '
257
+ + 'Refusing to launch unspoofed - the session would leak the host platform, user agent, and WebGL identity.');
258
+ }
259
+ return resolved;
260
+ }
110
261
  export const launchBrowserWithNewProfile = async (executablePath, options = {}) => {
111
262
  const { anonymizedProxyUrl, debuggingPort, windowWidth = 1920, windowHeight = 1080, ignoreHttpsErrors, headless, useExperimentalFingerprints = false, maximizeWindow = true, enableStealthMode = true, populateProfile: shouldPopulateProfile = true, userDataDir: providedUserDataDir, keepUserDataDir, dnsOverHttpsServer, secureDnsMode, webrtcIpHandlingPolicy, useNativeStealth = false, } = options;
112
263
  // Detect if we're running with the anti-detect browser binary
113
264
  const usingAntiDetectBrowser = useNativeStealth && isAntiDetectBrowserSync(executablePath);
114
265
  if (useNativeStealth && !usingAntiDetectBrowser) {
115
- log.debug('[AntiDetect] useNativeStealth requested but anti-detect browser not detected — using standard Chrome');
266
+ log.debug('[AntiDetect] useNativeStealth requested but anti-detect browser not detected - using standard Chrome');
116
267
  }
117
268
  else if (usingAntiDetectBrowser) {
118
- log.debug('[AntiDetect] Anti-detect browser detected — native C++ stealth active');
269
+ log.debug('[AntiDetect] Anti-detect browser detected - native C++ stealth active');
119
270
  }
120
271
  // Track temp config file for cleanup
121
272
  let fingerprintConfigPath = null;
@@ -127,8 +278,12 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
127
278
  if (providedUserDataDir) {
128
279
  await fs.mkdir(userDataDir, { recursive: true });
129
280
  }
130
- await createChromeProfile(userDataDir);
131
- // Populate profile with realistic user data for 2024-2025 anti-bot evasion
281
+ // Populate profile with realistic user data for 2024-2025 anti-bot evasion.
282
+ // populateChromeProfile is the sole Preferences writer here: it builds the
283
+ // merged Preferences object in memory and writes it once (no separate
284
+ // createChromeProfile write to immediately overwrite). createChromeProfile
285
+ // is only used when population is disabled, or as a fallback if population
286
+ // fails partway so the launch still has a valid base profile.
132
287
  if (shouldPopulateProfile) {
133
288
  try {
134
289
  await populateChromeProfile(userDataDir, {
@@ -140,6 +295,22 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
140
295
  }
141
296
  catch (popError) {
142
297
  log.debug(`Could not populate profile: ${popError}`);
298
+ await createChromeProfile(userDataDir).catch(() => { });
299
+ }
300
+ }
301
+ else {
302
+ await createChromeProfile(userDataDir);
303
+ }
304
+ // Seed the Widevine CDM from the per-host cache before Chromium starts, so
305
+ // startup registration picks it up and `requestMediaKeySystemAccess` resolves
306
+ // on the first navigation, exactly as it does in a real Chrome. On Linux this
307
+ // is the ONLY thing that works: the CDM is bound at startup there, so a copy
308
+ // fetched later in the session cannot be used until the next launch (see
309
+ // widevine-cdm.ts). A cold cache is a no-op; ensureWidevineCdm fills it.
310
+ if (usingAntiDetectBrowser) {
311
+ const seeded = await seedWidevineCdm(userDataDir);
312
+ if (seeded) {
313
+ log.debug(`[AntiDetect] Widevine CDM ${seeded.version} seeded into the profile before launch`);
143
314
  }
144
315
  }
145
316
  const browserArgs = [
@@ -154,7 +325,7 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
154
325
  '--no-default-browser-check',
155
326
  ];
156
327
  // Add stealth arguments for anti-bot evasion. buildStealthArgs produces a
157
- // platform-scoped set aligned with nodriver — see the rationale block above
328
+ // platform-scoped set aligned with nodriver - see the rationale block above
158
329
  // the UNIVERSAL_STEALTH_ARGS constant for per-flag evidence.
159
330
  if (enableStealthMode) {
160
331
  browserArgs.push(...buildStealthArgs({ usingAntiDetectBrowser }));
@@ -165,6 +336,38 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
165
336
  if (webrtcIpHandlingPolicy) {
166
337
  browserArgs.push(`--force-webrtc-ip-handling-policy=${webrtcIpHandlingPolicy}`);
167
338
  }
339
+ if (options.fingerprintOptions?.forceEmulateWebgl === true) {
340
+ // Two switch names, because two binaries are in play and each reads
341
+ // only its own. Verified with `strings` on the shipped artifacts:
342
+ //
343
+ // dist/linux-x64/chrome (151) force-gpu-emulation x2
344
+ // dist/linux-x64.cromite-147.bak/… (147) force-webgl-emulation x2
345
+ //
346
+ // and neither name appears in the other binary. The 147 artifact is
347
+ // the one currently published to the KV store, so pushing only the
348
+ // 151 name means `forceEmulateWebgl: true` is silently dropped for
349
+ // every consumer on the released browser - exactly the way stock
350
+ // Chrome drops an unknown switch. Pushing only the 147 name has the
351
+ // mirror-image problem once #148 flips.
352
+ //
353
+ // Same dual-pass shape as --blink-settings-file /
354
+ // --blink-platform-overrides-file above. Delete the 147 line at the
355
+ // #148 flip, not before.
356
+ //
357
+ // The canonical declaration is switches::kForceGpuEmulation in
358
+ // chrome/common/chrome_switches.{h,cc}, propagated to
359
+ // renderer/GPU children via the kCommonSwitchNames list in
360
+ // chrome/browser/chrome_content_browser_client.cc. The renderer
361
+ // WebGL factory hook in third_party/blink/renderer/modules/webgl/
362
+ // webgl_rendering_context_base.cc reads the switch and, when
363
+ // FingerprintConfig::IsLoaded() also holds, returns the in-process
364
+ // EmulatedWebGraphicsContext3DProvider directly (skipping the GPU
365
+ // command-buffer round-trip entirely). On stock Chrome the switch
366
+ // is unknown and silently dropped.
367
+ browserArgs.push('--force-gpu-emulation');
368
+ browserArgs.push('--force-webgl-emulation');
369
+ log.debug('[AntiDetect] forceEmulateWebgl=true → pushed --force-gpu-emulation + --force-webgl-emulation');
370
+ }
168
371
  // Set window size - either specific dimensions or maximize
169
372
  // Don't maximize if specific dimensions are provided to avoid conflicts
170
373
  if (windowWidth && windowHeight) {
@@ -184,41 +387,87 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
184
387
  browserArgs.push(`--disable-extensions-except=${extList}`, // optional, keeps Chrome from loading the rest
185
388
  `--load-extension=${extList}`);
186
389
  }
187
- // Anti-detect browser: write --fingerprint-config JSON and pass it to the browser.
188
- // The C++ FingerprintConfig singleton reads this file at startup (lazy, first access).
390
+ // Anti-detect browser: write platform-overrides JSON and pass its path to the
391
+ // browser via the config switch (see the two-name note below - the shipped
392
+ // 147 binary calls it --blink-platform-overrides-file, the vanilla-151 one
393
+ // --blink-settings-file). The C++ singleton reads this file at startup
394
+ // (lazy, first access); the browser process re-emits the content inline as
395
+ // the matching -data switch so sandboxed children (renderer/GPU/utility)
396
+ // can parse it without file IO.
189
397
  if (usingAntiDetectBrowser) {
190
398
  try {
191
399
  // Prefer a pre-generated per-session config (passed by browser-plugin.ts) so that
192
400
  // each browser instance gets unique C++-level hardware fingerprints (GPU, screen,
193
401
  // CPU). Fall back to the static default_fingerprint.json if none was provided.
194
- const configToWrite = options.fingerprintConfig ?? await (async () => {
402
+ // Throws if neither source yields a config - see resolveAntiDetectFingerprintConfig.
403
+ const configToWrite = await resolveAntiDetectFingerprintConfig(options.fingerprintConfig, async () => {
195
404
  const { default: def } = await import(
196
- /* @vite-ignore */ new URL('../../browser/config/default_fingerprint.json', import.meta.url).href).catch(() => ({ default: null }));
405
+ /* @vite-ignore */ new URL('../../browser/config/default_fingerprint.json', import.meta.url).href);
197
406
  return def;
198
- })();
199
- if (configToWrite) {
407
+ });
408
+ {
200
409
  fingerprintConfigPath = await writeTempFingerprintConfig(configToWrite);
201
- browserArgs.push(`--fingerprint-config=${fingerprintConfigPath}`);
202
- // --fingerprint-accept-language is our custom C++ hook in
203
- // net/url_request/url_request_http_job.cc. Only the anti-detect binary
204
- // understands this flag. The hook splits on commas and feeds each entry
205
- // verbatim into `navigator.languages`, so we MUST strip q-values here;
206
- // otherwise the JS array leaks `"ko;q=0.9"`-style entries and becomes a
207
- // detectable anomaly (real Chrome's navigator.languages never carries
208
- // q-values — those are HTTP Accept-Language semantics only).
209
- const fpLangs = configToWrite.navigator?.languages;
210
- if (fpLangs) {
211
- const cleanLangs = fpLangs.split(',')
212
- .map((l) => l.split(';')[0].trim())
213
- .filter(Boolean)
214
- .join(',');
215
- browserArgs.push(`--fingerprint-accept-language=${cleanLangs}`);
410
+ // Opaque switch name (renamed from --fingerprint-config). Mimics
411
+ // existing Blink runtime-flag shape so /proc/<pid>/cmdline does
412
+ // not advertise an anti-detect-specific flag.
413
+ // #145 lockstep: the vanilla-151 binary renamed this switch to
414
+ // --blink-settings-file (switches::kBlinkSettingsFile). It knows
415
+ // no other name, and because it hard-fails when neither
416
+ // --blink-settings-file nor --blink-settings-data is present
417
+ // (#78), sending only the old name does not degrade gracefully -
418
+ // the browser SIGTRAPs before the CDP endpoint is ready and the
419
+ // crawler cannot launch it at all. The SHIPPED Cromite/147
420
+ // artifact has the mirror-image problem: it only knows the old
421
+ // name and hard-fails the same way if it is not passed.
422
+ //
423
+ // TEMPORARY COMPATIBILITY SHIM, not design. Both names are
424
+ // pushed, pointing at the SAME file, so one crawler build drives
425
+ // both binaries. Each binary reads the name it knows and ignores
426
+ // the other: base::CommandLine::AppendSwitchesAndArguments
427
+ // (base/command_line.cc) stores every `--k=v` token in a plain
428
+ // map with no allowlist, so an unrecognised switch is inert -
429
+ // nothing in the tree validates the switch set, and the only
430
+ // switch-name-driven warning path (kBadFlags in
431
+ // chrome/browser/ui/startup/bad_flags_prompt.cc) is a fixed list
432
+ // that contains neither name. Verified against the artifacts
433
+ // themselves: `strings` on the 147 build exports only
434
+ // "blink-platform-overrides-file", on the 151 build only
435
+ // "blink-settings-file".
436
+ //
437
+ // DELETE the legacy line below - and this comment - once the
438
+ // shipped artifact is the vanilla line, i.e. after the browser
439
+ // submodule pointer is flipped to the 151 build (#148). At that
440
+ // point no binary we ship or download reads the old name, and
441
+ // keeping it only adds a needless token to /proc/<pid>/cmdline.
442
+ browserArgs.push(`--blink-settings-file=${fingerprintConfigPath}`);
443
+ // LEGACY (Cromite/147 only) - remove with #148.
444
+ browserArgs.push(`--blink-platform-overrides-file=${fingerprintConfigPath}`);
445
+ // Anti-Detect 2026-05-18: also pass the spoofed User-Agent via
446
+ // Chromium's standard `--user-agent` switch so the HTTP wire
447
+ // layer sees the same UA as JS-visible `navigator.userAgent`.
448
+ // The previous mechanism - reading FingerprintConfig directly
449
+ // from `net/url_request/url_request_http_job.cc` - was a
450
+ // net/→blink/ layering violation that broke component builds
451
+ // (FAST_BUILD=1). `--user-agent` is honored by
452
+ // embedder_support::GetUserAgent() which HttpUserAgentSettings
453
+ // caches per-NetworkContext, so every outgoing HTTP request
454
+ // carries the spoofed UA without any cross-layer dep.
455
+ // Accept-Language is already covered by the `--lang=` switch
456
+ // pushed below (line ~520) plus the `setenv("LANG"/"LC_ALL")`
457
+ // pin in chrome_main_delegate.cc::BasicStartupComplete.
458
+ const spoofedUA = configToWrite?.navigator?.userAgent;
459
+ if (spoofedUA) {
460
+ browserArgs.push(`--user-agent=${spoofedUA}`);
216
461
  }
217
- log.debug(`[AntiDetect] Wrote fingerprint config: ${fingerprintConfigPath}${options.fingerprintConfig ? ' (per-session)' : ' (static default)'}`);
462
+ log.debug(`[Blink] Wrote platform-overrides config: ${fingerprintConfigPath}${options.fingerprintConfig ? ' (per-session)' : ' (static default)'}`);
218
463
  }
219
464
  }
220
465
  catch (configErr) {
221
- log.debug(`[AntiDetect] Could not write fingerprint config: ${configErr}`);
466
+ // Never swallow a config failure for the anti-detect binary: a
467
+ // browser launched without the platform-overrides config runs
468
+ // unspoofed and silently leaks the host identity. Surface it so
469
+ // the launch aborts rather than shipping a detectable session.
470
+ throw new Error(`[AntiDetect] Failed to prepare the fingerprint config for the anti-detect browser - refusing to launch unspoofed: ${configErr instanceof Error ? configErr.message : String(configErr)}`);
222
471
  }
223
472
  }
224
473
  // --lang is understood by every Chrome binary (stock + anti-detect) and configures
@@ -235,14 +484,24 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
235
484
  browserArgs.push(`--lang=${langArg || options.resolvedLocale.locale}`);
236
485
  }
237
486
  else {
238
- // No locale resolved — preserve historical default so we don't change behavior
487
+ // No locale resolved - preserve historical default so we don't change behavior
239
488
  // for callers that never touched fingerprintOptions.
240
489
  browserArgs.push('--lang=en-US,en');
241
490
  }
242
491
  if (anonymizedProxyUrl) {
243
492
  browserArgs.push(`--proxy-server=${anonymizedProxyUrl}`);
244
493
  }
245
- if (ignoreHttpsErrors) {
494
+ // Anti-Detect 2026-05-18: `--ignore-certificate-errors` is JS-detectable.
495
+ // A page can issue a `fetch()` to a sub-resource hosted under a
496
+ // self-signed certificate; real Chrome blocks the load (cert error),
497
+ // we silently succeed. That single asymmetry is enough to flag us.
498
+ //
499
+ // When running the native anti-detect binary we therefore force real
500
+ // certificate validation, regardless of the caller's `ignoreHttpsErrors`
501
+ // setting - the stealth invariant (mimic real Chrome wire behaviour)
502
+ // overrides the convenience flag. Stock-Chromium / non-stealth callers
503
+ // keep the historical default.
504
+ if (ignoreHttpsErrors && !usingAntiDetectBrowser) {
246
505
  browserArgs.push('--ignore-certificate-errors');
247
506
  // disabled because of cloudflare blocking
248
507
  // browserArgs.push('--disable-web-security');
@@ -251,31 +510,117 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
251
510
  browserArgs.push('--headless=new');
252
511
  }
253
512
  log.debug(`Launching browser with user data dir: ${userDataDir}`);
254
- const chromeVersion = getChromeBinaryVersion(executablePath);
255
- if (chromeVersion) {
256
- log.debug(`Using Chrome binary version: ${chromeVersion}`);
257
- }
258
513
  log.debug('browserArgs', browserArgs);
259
514
  log.debug(`Stealth mode: ${enableStealthMode}, Profile populated: ${shouldPopulateProfile}, cleanup profile: ${shouldCleanupUserDataDir}`);
260
- // Chrome bundles its own ICU so LANG/LC_ALL don't drive Intl — but a handful of
515
+ // Chrome bundles its own ICU so LANG/LC_ALL don't drive Intl - but a handful of
261
516
  // system APIs (file dialogs, native notifications, extension readers) consult them.
262
517
  // Setting these matches what a real user's shell would do and costs nothing.
518
+ //
519
+ // We also override TZ and LANGUAGE: a hardcoded host TZ (e.g. Europe/Berlin from
520
+ // an Apify Dockerfile) would otherwise leak through to anything that reads
521
+ // /etc/localtime or honors $TZ for fallback paths. Chrome's main Intl/Date path
522
+ // is covered by Emulation.setTimezoneOverride (called from fingerprint-injector)
523
+ // but workers and out-of-process subsystems can still consult $TZ.
263
524
  const spawnEnv = { ...process.env };
264
525
  if (options.resolvedLocale?.posix) {
265
526
  spawnEnv.LANG = options.resolvedLocale.posix;
266
527
  spawnEnv.LC_ALL = options.resolvedLocale.posix;
267
528
  }
529
+ if (options.resolvedLocale?.languages) {
530
+ // GTK/glib and some apps read $LANGUAGE for fallback chains; align with the
531
+ // primary language (strip q-values).
532
+ const primaryLang = options.resolvedLocale.languages
533
+ .split(',')[0]
534
+ ?.split(';')[0]
535
+ ?.trim();
536
+ if (primaryLang)
537
+ spawnEnv.LANGUAGE = primaryLang;
538
+ }
539
+ if (options.resolvedLocale?.timezone) {
540
+ spawnEnv.TZ = options.resolvedLocale.timezone;
541
+ }
542
+ // Pipe stderr instead of swallowing it so a Chromium crash on Linux
543
+ // (missing shared lib, sandbox failure, GLIBC mismatch, …) surfaces
544
+ // in the logs instead of presenting as a generic "Failed to connect to
545
+ // browser on port: 9200" 30s timeout. Last ~50 lines are buffered and
546
+ // dumped at WARN level if the process exits before the CDP listener
547
+ // comes up.
268
548
  const browserProcess = spawn(executablePath, browserArgs, {
269
- stdio: 'ignore',
549
+ stdio: ['ignore', 'ignore', 'pipe'],
270
550
  env: spawnEnv,
551
+ // Put the browser in its own process group (pgid === pid) so the
552
+ // whole Chromium process tree - parent + renderer/GPU/utility
553
+ // helpers - can be reaped at once via `process.kill(-pid, sig)`.
554
+ // Without this, signals hit only the parent and helpers orphan to
555
+ // launchd/init, leaking RAM and exhausting macOS IOSurface clients.
556
+ // Ctrl+C is still handled explicitly by signal-cleanup.ts, so losing
557
+ // the TTY's automatic SIGINT forwarding to the group is fine.
558
+ detached: true,
271
559
  });
272
560
  const proc = new BrowserProcess(browserProcess, userDataDir, shouldCleanupUserDataDir);
273
- // Clean up temp fingerprint config file when the browser process exits
561
+ const stderrBuffer = [];
562
+ const STDERR_BUFFER_LIMIT = 50;
563
+ let stderrLeftover = '';
564
+ // Chromium writes a large volume of benign stderr on headless container
565
+ // launches (GPU-less init, ALSA "no sound card", dbus, WebRTC P2P
566
+ // resolve failures, the DevTools banner, zygote teardown races, …).
567
+ // None of it is actionable at runtime, so EVERY stderr line is logged at
568
+ // DEBUG only - it surfaces solely when the Apify log level is DEBUG.
569
+ // Real failures are not lost: the `exit` handler below dumps the buffered
570
+ // stderr tail at WARNING whenever Chromium exits with a non-zero code,
571
+ // which is the signature of an actual crash.
572
+ browserProcess.stderr?.on('data', (chunk) => {
573
+ stderrLeftover += chunk.toString('utf8');
574
+ const lines = stderrLeftover.split('\n');
575
+ stderrLeftover = lines.pop() ?? '';
576
+ for (const line of lines) {
577
+ if (!line)
578
+ continue;
579
+ stderrBuffer.push(line);
580
+ if (stderrBuffer.length > STDERR_BUFFER_LIMIT)
581
+ stderrBuffer.shift();
582
+ log.debug(`[Chromium stderr] ${line}`);
583
+ // Puppeteer-style readiness: Chromium prints the browser-level CDP
584
+ // WebSocket endpoint here once its remote-debugging server is
585
+ // accepting connections. Parsing it is the exact moment the browser
586
+ // is connectable - no TCP-port poll or fixed pre-connect delay
587
+ // needed.
588
+ const wsMatch = line.match(/DevTools listening on (ws:\S+)/);
589
+ if (wsMatch) {
590
+ proc.resolveWsEndpoint(wsMatch[1]);
591
+ }
592
+ }
593
+ });
594
+ // Surface the buffered stderr if Chrome exits before something else
595
+ // gets a chance to inspect it (typical crash-on-startup signature), and
596
+ // fail the readiness promise fast with that same tail so a dead binary
597
+ // never presents as a generic connect timeout.
598
+ browserProcess.once('exit', (code, signal) => {
599
+ if (stderrLeftover) {
600
+ stderrBuffer.push(stderrLeftover);
601
+ stderrLeftover = '';
602
+ }
603
+ const tail = stderrBuffer.slice(-20).join('\n');
604
+ if (code !== 0 && code !== null) {
605
+ log.warning(`Chromium exited with code=${code} signal=${signal}. `
606
+ + `Last stderr lines:\n${tail || '(empty - Chrome wrote nothing to stderr)'}`);
607
+ }
608
+ // No-op once the ws line was already seen (settled-once guard).
609
+ proc.rejectWsEndpoint(new Error(`Chromium exited (code=${code} signal=${signal}) before the CDP WebSocket endpoint was ready. `
610
+ + `Last stderr lines:\n${tail || '(empty - Chrome wrote nothing to stderr)'}`));
611
+ });
612
+ // A spawn-level failure (missing binary, EACCES, …) fires `error` and may
613
+ // never fire `exit`, so reject readiness here too. Also settled-once.
614
+ browserProcess.once('error', (err) => {
615
+ const tail = stderrBuffer.slice(-20).join('\n');
616
+ proc.rejectWsEndpoint(new Error(`Browser process failed to start before the CDP WebSocket endpoint was ready: ${err.message}${tail ? `\nLast stderr lines:\n${tail}` : ''}`));
617
+ });
618
+ // Track the temp fingerprint config file on the BrowserProcess so it is
619
+ // removed both on normal exit (via cleanup()) and on a SIGINT/SIGTERM
620
+ // signal (via the signal-cleanup registry), not just the exit event -
621
+ // which never fires when the process is interrupted before teardown.
274
622
  if (fingerprintConfigPath) {
275
- const configPath = fingerprintConfigPath;
276
- browserProcess.once('exit', () => {
277
- fs.unlink(configPath).catch(() => { });
278
- });
623
+ proc.trackFingerprintConfigPath(fingerprintConfigPath);
279
624
  }
280
625
  return proc;
281
626
  }
@@ -284,7 +629,18 @@ export const launchBrowserWithNewProfile = async (executablePath, options = {})
284
629
  throw new Error(`Error launching the browser: ${error}`);
285
630
  }
286
631
  };
287
- export const createTempUserDataDir = async () => {
632
+ // Cache the resolved writable base directory for the process lifetime. The
633
+ // mkdir + write-test probe below only depends on the filesystem layout and the
634
+ // process's cwd/home, none of which change underneath us mid-process, so the
635
+ // probe is run once and its result reused. Only a SUCCESSFUL probe is cached -
636
+ // a transient failure (e.g. a full disk during the first launch) must be
637
+ // retried on the next launch rather than pinned for the whole process. Each
638
+ // launch still gets its own fresh user-data-dir via fs.mkdtemp under the base.
639
+ let cachedBaseTempDir = null;
640
+ const resolveWritableBaseTempDir = async () => {
641
+ if (cachedBaseTempDir) {
642
+ return cachedBaseTempDir;
643
+ }
288
644
  // Try to create temp directory in order of preference:
289
645
  // 1. Project's temp directory (if we're in a project)
290
646
  // 2. User's home directory temp folder
@@ -297,7 +653,6 @@ export const createTempUserDataDir = async () => {
297
653
  // System temp directory (fallback)
298
654
  path.join(os.tmpdir(), 'cdp-crawler-browsers'),
299
655
  ];
300
- let baseTempDir = null;
301
656
  for (const candidate of tempDirCandidates) {
302
657
  try {
303
658
  await fs.mkdir(candidate, { recursive: true });
@@ -305,17 +660,18 @@ export const createTempUserDataDir = async () => {
305
660
  const testFile = path.join(candidate, '.write-test');
306
661
  await fs.writeFile(testFile, 'test');
307
662
  await fs.unlink(testFile);
308
- baseTempDir = candidate;
309
- break;
663
+ cachedBaseTempDir = candidate;
664
+ return candidate;
310
665
  }
311
666
  catch (error) {
312
667
  log.debug(`Cannot use temp directory ${candidate}: ${error}`);
313
668
  continue;
314
669
  }
315
670
  }
316
- if (!baseTempDir) {
317
- throw new Error('Could not find a writable directory for browser profiles');
318
- }
671
+ throw new Error('Could not find a writable directory for browser profiles');
672
+ };
673
+ export const createTempUserDataDir = async () => {
674
+ const baseTempDir = await resolveWritableBaseTempDir();
319
675
  const tempDirPrefix = path.join(baseTempDir, 'browser_');
320
676
  const userDataDir = await fs.mkdtemp(tempDirPrefix);
321
677
  log.debug(`Created browser profile directory: ${userDataDir}`);
@@ -326,7 +682,7 @@ export const launchChromeBrowserWithNewProfile = async (options = {}) => {
326
682
  // CDPBrowserPlugin._launch() (resolveAntiDetectBrowserPath throws there if the
327
683
  // binary is missing). Direct callers of this function without native stealth
328
684
  // still fall through to the system Chrome lookup.
329
- let executablePath = options.executablePath;
685
+ let { executablePath } = options;
330
686
  if (!executablePath) {
331
687
  const wantsNativeStealth = options.useNativeStealth || options.fingerprintOptions?.useNativeStealth;
332
688
  if (wantsNativeStealth) {