invisible-playwright 0.3.5__tar.gz

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 (71) hide show
  1. invisible_playwright-0.3.5/.gitignore +16 -0
  2. invisible_playwright-0.3.5/LICENSE +21 -0
  3. invisible_playwright-0.3.5/PKG-INFO +234 -0
  4. invisible_playwright-0.3.5/README.md +198 -0
  5. invisible_playwright-0.3.5/pyproject.toml +106 -0
  6. invisible_playwright-0.3.5/src/invisible_playwright/__init__.py +68 -0
  7. invisible_playwright-0.3.5/src/invisible_playwright/__main__.py +4 -0
  8. invisible_playwright-0.3.5/src/invisible_playwright/_engine.py +90 -0
  9. invisible_playwright-0.3.5/src/invisible_playwright/_fpforge/__init__.py +17 -0
  10. invisible_playwright-0.3.5/src/invisible_playwright/_geo.py +4 -0
  11. invisible_playwright-0.3.5/src/invisible_playwright/_headless.py +4 -0
  12. invisible_playwright-0.3.5/src/invisible_playwright/_pin.py +143 -0
  13. invisible_playwright-0.3.5/src/invisible_playwright/_proxy.py +4 -0
  14. invisible_playwright-0.3.5/src/invisible_playwright/_recaptcha_seed.py +340 -0
  15. invisible_playwright-0.3.5/src/invisible_playwright/_webgl_personas.py +4 -0
  16. invisible_playwright-0.3.5/src/invisible_playwright/async_api.py +271 -0
  17. invisible_playwright-0.3.5/src/invisible_playwright/cli.py +127 -0
  18. invisible_playwright-0.3.5/src/invisible_playwright/config.py +4 -0
  19. invisible_playwright-0.3.5/src/invisible_playwright/constants.py +4 -0
  20. invisible_playwright-0.3.5/src/invisible_playwright/download.py +4 -0
  21. invisible_playwright-0.3.5/src/invisible_playwright/launcher.py +436 -0
  22. invisible_playwright-0.3.5/src/invisible_playwright/prefs.py +4 -0
  23. invisible_playwright-0.3.5/src/invisible_playwright/py.typed +0 -0
  24. invisible_playwright-0.3.5/src/invisible_playwright/sync_api.py +4 -0
  25. invisible_playwright-0.3.5/tests/conftest.py +54 -0
  26. invisible_playwright-0.3.5/tests/test_async_api.py +83 -0
  27. invisible_playwright-0.3.5/tests/test_backcompat.py +84 -0
  28. invisible_playwright-0.3.5/tests/test_build.py +42 -0
  29. invisible_playwright-0.3.5/tests/test_canvas_render_stealth.py +67 -0
  30. invisible_playwright-0.3.5/tests/test_cli.py +174 -0
  31. invisible_playwright-0.3.5/tests/test_cloak.py +115 -0
  32. invisible_playwright-0.3.5/tests/test_constants.py +217 -0
  33. invisible_playwright-0.3.5/tests/test_core_pin.py +417 -0
  34. invisible_playwright-0.3.5/tests/test_cross_origin_iframe.py +278 -0
  35. invisible_playwright-0.3.5/tests/test_detectors_e2e.py +294 -0
  36. invisible_playwright-0.3.5/tests/test_download.py +590 -0
  37. invisible_playwright-0.3.5/tests/test_e2e.py +219 -0
  38. invisible_playwright-0.3.5/tests/test_fingerprint_consistency.py +510 -0
  39. invisible_playwright-0.3.5/tests/test_fingerprint_surface.py +311 -0
  40. invisible_playwright-0.3.5/tests/test_fpforge.py +77 -0
  41. invisible_playwright-0.3.5/tests/test_geo.py +325 -0
  42. invisible_playwright-0.3.5/tests/test_geoip_update.py +142 -0
  43. invisible_playwright-0.3.5/tests/test_headless.py +124 -0
  44. invisible_playwright-0.3.5/tests/test_imports.py +56 -0
  45. invisible_playwright-0.3.5/tests/test_integration.py +358 -0
  46. invisible_playwright-0.3.5/tests/test_launcher_config.py +271 -0
  47. invisible_playwright-0.3.5/tests/test_launcher_helpers.py +229 -0
  48. invisible_playwright-0.3.5/tests/test_mouse.py +255 -0
  49. invisible_playwright-0.3.5/tests/test_network.py +260 -0
  50. invisible_playwright-0.3.5/tests/test_pin.py +72 -0
  51. invisible_playwright-0.3.5/tests/test_prefs.py +486 -0
  52. invisible_playwright-0.3.5/tests/test_profile.py +296 -0
  53. invisible_playwright-0.3.5/tests/test_proxy.py +266 -0
  54. invisible_playwright-0.3.5/tests/test_proxy_socks_auth_e2e.py +197 -0
  55. invisible_playwright-0.3.5/tests/test_recaptcha_seed.py +349 -0
  56. invisible_playwright-0.3.5/tests/test_release_e2e.py +269 -0
  57. invisible_playwright-0.3.5/tests/test_sampler.py +316 -0
  58. invisible_playwright-0.3.5/tests/test_seal_binary_path.py +177 -0
  59. invisible_playwright-0.3.5/tests/test_seal_floor.py +263 -0
  60. invisible_playwright-0.3.5/tests/test_seal_playwright_range.py +79 -0
  61. invisible_playwright-0.3.5/tests/test_seal_wire_version.py +72 -0
  62. invisible_playwright-0.3.5/tests/test_service_worker.py +225 -0
  63. invisible_playwright-0.3.5/tests/test_version.py +114 -0
  64. invisible_playwright-0.3.5/tests/test_webgl_noise_active.py +63 -0
  65. invisible_playwright-0.3.5/tests/test_webrtc_realness.py +490 -0
  66. invisible_playwright-0.3.5/tests/unit/test_config_public.py +125 -0
  67. invisible_playwright-0.3.5/tests/vendor/README.md +24 -0
  68. invisible_playwright-0.3.5/tests/vendor/botd-2.0.0.esm.js +811 -0
  69. invisible_playwright-0.3.5/tests/vendor/creepjs-10aa672.js +9710 -0
  70. invisible_playwright-0.3.5/tests/vendor/fingerprintjs-5.2.0.umd.min.js +27 -0
  71. invisible_playwright-0.3.5/tests/vendor/fpscanner-1.0.6.es.js +1253 -0
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .venv/
8
+ firefox-source/
9
+
10
+ # Third-party source trees kept on disk for on-demand compatibility audits.
11
+ # They are somebody else's code, with their own licence and their own .git, and
12
+ # they must never be committed into this repository: `git add -A` would either
13
+ # stage 3.9 MB of Apache-2.0 sources into an MIT repo or, worse, record a bare
14
+ # gitlink that nobody can clone. The sdist/wheel excludes only protect the
15
+ # published artifacts, not the repo, so the ignore has to live here too.
16
+ tests/playwright-upstream/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 stealthfox contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,234 @@
1
+ Metadata-Version: 2.4
2
+ Name: invisible-playwright
3
+ Version: 0.3.5
4
+ Summary: Playwright wrapper for a patched Firefox with deterministic stealth profile.
5
+ Project-URL: Homepage, https://github.com/feder-cr/invisible_playwright
6
+ Project-URL: Repository, https://github.com/feder-cr/invisible_playwright
7
+ Project-URL: Issues, https://github.com/feder-cr/invisible_playwright/issues
8
+ Project-URL: Changelog, https://github.com/feder-cr/invisible_playwright/blob/main/CHANGELOG.md
9
+ Project-URL: Documentation, https://github.com/feder-cr/invisible_playwright#readme
10
+ Author-email: feder-cr <85809106+feder-cr@users.noreply.github.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: anti-detect,automation,firefox,playwright,stealth
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.11
26
+ Requires-Dist: invisible-core==18.0.0
27
+ Requires-Dist: playwright<=1.61.0,>=1.55
28
+ Provides-Extra: dev
29
+ Requires-Dist: build>=1; extra == 'dev'
30
+ Requires-Dist: playwright<=1.61.0,>=1.55; extra == 'dev'
31
+ Requires-Dist: pytest-mock>=3; extra == 'dev'
32
+ Requires-Dist: pytest-rerunfailures>=14; extra == 'dev'
33
+ Requires-Dist: pytest>=7; extra == 'dev'
34
+ Requires-Dist: responses>=0.24; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ <p>
38
+ <a href="https://github.com/feder-cr/invisible_playwright/actions/workflows/tests.yml"><img src="https://github.com/feder-cr/invisible_playwright/actions/workflows/tests.yml/badge.svg" alt="tests"></a>
39
+ <a href="https://github.com/feder-cr/invisible_playwright/blob/main/LICENSE"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/license.svg" alt="License: MIT"></a>
40
+ <a href="https://www.python.org/downloads/"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/python.svg" alt="Python 3.11+"></a>
41
+ <a href="https://github.com/feder-cr/firefox_antidetect_patch/releases"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/firefox.svg" alt="Firefox 151.0"></a>
42
+ <a href="https://github.com/feder-cr/invisible_playwright/stargazers"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/stars.svg" alt="GitHub stars"></a>
43
+ <a href="https://github.com/feder-cr/firefox_antidetect_patch/releases/tag/usage-counter"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/launches.svg" alt="browser launches"></a>
44
+ </p>
45
+
46
+ <div align="center">
47
+ <picture>
48
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/feder-cr/invisible_playwright/7a8693c6b4386e9a84dd93bedc479ca8654482e1/docs/banner-dark.png">
49
+ <img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/7a8693c6b4386e9a84dd93bedc479ca8654482e1/docs/banner-light.png" alt="invisible_playwright" width="620">
50
+ </picture>
51
+ </div>
52
+
53
+ <h3 align="center">A modified Firefox that passes every bot detection test.</h3>
54
+
55
+ ![invisible_playwright - 5/5 detection suites passed](https://raw.githubusercontent.com/feder-cr/invisible_playwright/7a8693c6b4386e9a84dd93bedc479ca8654482e1/docs/screenshots/hero.gif)
56
+
57
+ ## How it works
58
+
59
+ Anti-bots ask two questions. invisible_playwright answers yes to both.
60
+
61
+ **1. Is this a real browser?** Yes. It is Firefox, patched at the C++ source level.
62
+
63
+ - Fingerprint set inside the engine, not injected into the page: Navigator, screen, GPU/WebGL, Canvas, fonts, audio, WebRTC, timezone, network.
64
+ - No JS shim, no override, no seam to read.
65
+
66
+ **2. Is a real person using it?** Yes. The actions are humanized in the driver.
67
+
68
+ - Every click, hover and drag follows a natural mouse path with human timing, no teleporting cursor.
69
+ - Each input is byte-identical to a real mouse: real input source, pressure, trusted events.
70
+
71
+ Driven by the standard Playwright API. Full breakdown: [feder-cr/firefox_antidetect_patch](https://github.com/feder-cr/firefox_antidetect_patch).
72
+
73
+ ---
74
+
75
+ ## Still seeing captchas or anti-bot? It's the proxy.
76
+ Once the browser is handled it stops being the variable. If you are still getting challenged, the tell is no longer the browser, it is the IP you come from. Around 90% of proxies are public: anyone can rent the same address, so it is already known and sits on the blocked-IP lists sites check. A perfect browser on a known IP still loses.
77
+
78
+ > The fix is the clean 10%, residential IPs that aren't already known. For those we recommend [sx.org](https://sx.org/?c=invisible_playwright), who filter for and serve only IPs that aren't already on those lists.
79
+
80
+ ---
81
+
82
+ ## Install
83
+
84
+ ```bash
85
+ pip install invisible-playwright
86
+ python -m invisible_playwright fetch # one-time ~100 MB download, SHA256-verified
87
+ ```
88
+
89
+ Supported platforms: **Windows x86_64**, **Linux x86_64 / arm64**, **macOS arm64 / x86_64**. On macOS the app is ad-hoc signed (not notarized): if Gatekeeper complains, clear the quarantine flag once with `xattr -dr com.apple.quarantine` on the cached `Firefox.app`.
90
+
91
+ ---
92
+
93
+ ## Usage
94
+ ### Random fingerprint per session
95
+ **100% Playwright-compatible** - sync and async, all methods, zero API changes. If you already use Playwright, switching is two lines:
96
+
97
+ ```diff
98
+ - from playwright.sync_api import sync_playwright
99
+ - with sync_playwright() as p:
100
+ - browser = p.firefox.launch()
101
+ + from invisible_playwright import InvisiblePlaywright
102
+ + with InvisiblePlaywright() as browser:
103
+ ```
104
+
105
+ Every session gets a distinct fingerprint (GPU, audio, fonts, screen, ~400 fields) and Bezier-curve mouse motion.
106
+
107
+ **Sync**
108
+ ```python
109
+ from invisible_playwright import InvisiblePlaywright
110
+
111
+ with InvisiblePlaywright(proxy={"server": "socks5://...", "username": "u", "password": "p"}) as browser:
112
+ page = browser.new_page()
113
+ page.goto("https://example.com")
114
+ page.click("#submit") # mouse arcs to the button on a Bezier curve
115
+ ```
116
+
117
+ **Async**
118
+ ```python
119
+ from invisible_playwright.async_api import InvisiblePlaywright
120
+
121
+ async with InvisiblePlaywright(proxy={"server": "socks5://...", "username": "u", "password": "p"}) as browser:
122
+ page = await browser.new_page()
123
+ await page.goto("https://example.com")
124
+ await page.click("#submit")
125
+ ```
126
+
127
+ The `browser` object is a `playwright.sync_api.Browser` / `playwright.async_api.Browser` - every Playwright method works as-is.
128
+
129
+ Log the seed to replay a run:
130
+
131
+ ```python
132
+ sf = InvisiblePlaywright()
133
+ with sf as browser:
134
+ print("seed =", sf.seed)
135
+ # ...
136
+ ```
137
+
138
+ ### Reproducible fingerprint
139
+
140
+ ```python
141
+ with InvisiblePlaywright(seed=42) as browser:
142
+ ... # same GPU, same canvas hash, same audio context, every run
143
+ ```
144
+
145
+ ### Proxies
146
+
147
+ ```python
148
+ proxy = {
149
+ "server": "socks5://gate.example.com:1080",
150
+ "username": "user",
151
+ "password": "pass",
152
+ }
153
+ with InvisiblePlaywright(proxy=proxy) as browser:
154
+ ...
155
+ ```
156
+
157
+ Schemes supported: `socks5`, `socks4`, `http`, `https`. DNS is routed through the proxy by default, no local leak.
158
+
159
+ Around 90% of proxies are public, so their IPs are already known and blocked. For the clean 10%, residential IPs that aren't already known, we recommend [sx.org](https://sx.org/?c=invisible_playwright), who filter for and serve only IPs that aren't already on those lists.
160
+
161
+ ### Timezone
162
+
163
+ The browser timezone follows `timezone=`:
164
+
165
+ ```python
166
+ # default: timezone is auto-derived from the egress IP (proxy egress if a
167
+ # proxy is set, otherwise the host's own public IP)
168
+ with InvisiblePlaywright(proxy=proxy) as browser:
169
+ ...
170
+
171
+ # explicit IANA zone always wins, the only way to force a specific zone
172
+ with InvisiblePlaywright(proxy=proxy, timezone="America/New_York") as browser:
173
+ ...
174
+ ```
175
+
176
+ ### Pinning specific fingerprint fields
177
+
178
+ By default everything comes from `seed`. To force specific values while the rest stays seed-derived:
179
+
180
+ ```python
181
+ with InvisiblePlaywright(
182
+ seed=42,
183
+ pin={
184
+ "gpu.renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 Direct3D11)",
185
+ "gpu.vendor": "Google Inc. (NVIDIA)",
186
+ "screen.width": 2560,
187
+ "screen.height": 1440,
188
+ "hardware.concurrency": 16,
189
+ },
190
+ ) as browser:
191
+ ...
192
+ ```
193
+
194
+ Full list of pinnable keys, how pinning interacts with the Bayesian sampler, and common patterns are in **[docs/pinning.md](https://github.com/feder-cr/invisible_playwright/blob/main/docs/pinning.md)**.
195
+
196
+ ---
197
+
198
+ ## CLI
199
+
200
+ ```bash
201
+ invisible_playwright fetch # download the binary if missing
202
+ invisible_playwright fetch --force # re-download even if cached
203
+ invisible_playwright path # print the absolute path to the cached binary
204
+ invisible_playwright version # wrapper and binary versions
205
+ invisible_playwright clear-cache # remove all cached binaries
206
+ ```
207
+
208
+ ## Related projects
209
+
210
+ Related projects that cover similar ground:
211
+
212
+ - **[arkenfox/user.js](https://github.com/arkenfox/user.js)** - Firefox privacy hardening via prefs. invisible_playwright patches C++ where prefs are insufficient.
213
+ - **[LibreWolf](https://librewolf.net)** - Firefox fork with privacy defaults. LibreWolf ships a configured binary; invisible_playwright ships source patches + automation wrapper.
214
+ - **[Camoufox](https://github.com/daijro/camoufox)** - open-source anti-detect Firefox. Patches a wider surface and ships its own fingerprint database; invisible_playwright uses a Bayesian sampler.
215
+
216
+ ---
217
+
218
+ ## License
219
+
220
+ MIT - see [LICENSE](https://github.com/feder-cr/invisible_playwright/blob/main/LICENSE). The patched Firefox binary is distributed under the MPL-2.0 (Firefox upstream license). The C++ patches against mozilla-central that produce that binary are at [feder-cr/firefox_antidetect_patch](https://github.com/feder-cr/firefox_antidetect_patch).
221
+
222
+ ---
223
+
224
+ ## Disclaimer
225
+
226
+ This project is for educational purposes only. It is provided as-is, with no warranties. I take no responsibility for how it is used. Use it at your own risk and in compliance with the laws of your jurisdiction.
227
+
228
+
229
+ ---
230
+
231
+ <p align="center">
232
+ Built by <a href="https://it.linkedin.com/in/federico-elia-5199951b6">Federico Elia</a>
233
+ &nbsp;<a href="https://it.linkedin.com/in/federico-elia-5199951b6"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/linkedin.svg" alt="LinkedIn"></a>
234
+ </p>
@@ -0,0 +1,198 @@
1
+ <p>
2
+ <a href="https://github.com/feder-cr/invisible_playwright/actions/workflows/tests.yml"><img src="https://github.com/feder-cr/invisible_playwright/actions/workflows/tests.yml/badge.svg" alt="tests"></a>
3
+ <a href="https://github.com/feder-cr/invisible_playwright/blob/main/LICENSE"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/license.svg" alt="License: MIT"></a>
4
+ <a href="https://www.python.org/downloads/"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/python.svg" alt="Python 3.11+"></a>
5
+ <a href="https://github.com/feder-cr/firefox_antidetect_patch/releases"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/firefox.svg" alt="Firefox 151.0"></a>
6
+ <a href="https://github.com/feder-cr/invisible_playwright/stargazers"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/stars.svg" alt="GitHub stars"></a>
7
+ <a href="https://github.com/feder-cr/firefox_antidetect_patch/releases/tag/usage-counter"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/launches.svg" alt="browser launches"></a>
8
+ </p>
9
+
10
+ <div align="center">
11
+ <picture>
12
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/feder-cr/invisible_playwright/7a8693c6b4386e9a84dd93bedc479ca8654482e1/docs/banner-dark.png">
13
+ <img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/7a8693c6b4386e9a84dd93bedc479ca8654482e1/docs/banner-light.png" alt="invisible_playwright" width="620">
14
+ </picture>
15
+ </div>
16
+
17
+ <h3 align="center">A modified Firefox that passes every bot detection test.</h3>
18
+
19
+ ![invisible_playwright - 5/5 detection suites passed](https://raw.githubusercontent.com/feder-cr/invisible_playwright/7a8693c6b4386e9a84dd93bedc479ca8654482e1/docs/screenshots/hero.gif)
20
+
21
+ ## How it works
22
+
23
+ Anti-bots ask two questions. invisible_playwright answers yes to both.
24
+
25
+ **1. Is this a real browser?** Yes. It is Firefox, patched at the C++ source level.
26
+
27
+ - Fingerprint set inside the engine, not injected into the page: Navigator, screen, GPU/WebGL, Canvas, fonts, audio, WebRTC, timezone, network.
28
+ - No JS shim, no override, no seam to read.
29
+
30
+ **2. Is a real person using it?** Yes. The actions are humanized in the driver.
31
+
32
+ - Every click, hover and drag follows a natural mouse path with human timing, no teleporting cursor.
33
+ - Each input is byte-identical to a real mouse: real input source, pressure, trusted events.
34
+
35
+ Driven by the standard Playwright API. Full breakdown: [feder-cr/firefox_antidetect_patch](https://github.com/feder-cr/firefox_antidetect_patch).
36
+
37
+ ---
38
+
39
+ ## Still seeing captchas or anti-bot? It's the proxy.
40
+ Once the browser is handled it stops being the variable. If you are still getting challenged, the tell is no longer the browser, it is the IP you come from. Around 90% of proxies are public: anyone can rent the same address, so it is already known and sits on the blocked-IP lists sites check. A perfect browser on a known IP still loses.
41
+
42
+ > The fix is the clean 10%, residential IPs that aren't already known. For those we recommend [sx.org](https://sx.org/?c=invisible_playwright), who filter for and serve only IPs that aren't already on those lists.
43
+
44
+ ---
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install invisible-playwright
50
+ python -m invisible_playwright fetch # one-time ~100 MB download, SHA256-verified
51
+ ```
52
+
53
+ Supported platforms: **Windows x86_64**, **Linux x86_64 / arm64**, **macOS arm64 / x86_64**. On macOS the app is ad-hoc signed (not notarized): if Gatekeeper complains, clear the quarantine flag once with `xattr -dr com.apple.quarantine` on the cached `Firefox.app`.
54
+
55
+ ---
56
+
57
+ ## Usage
58
+ ### Random fingerprint per session
59
+ **100% Playwright-compatible** - sync and async, all methods, zero API changes. If you already use Playwright, switching is two lines:
60
+
61
+ ```diff
62
+ - from playwright.sync_api import sync_playwright
63
+ - with sync_playwright() as p:
64
+ - browser = p.firefox.launch()
65
+ + from invisible_playwright import InvisiblePlaywright
66
+ + with InvisiblePlaywright() as browser:
67
+ ```
68
+
69
+ Every session gets a distinct fingerprint (GPU, audio, fonts, screen, ~400 fields) and Bezier-curve mouse motion.
70
+
71
+ **Sync**
72
+ ```python
73
+ from invisible_playwright import InvisiblePlaywright
74
+
75
+ with InvisiblePlaywright(proxy={"server": "socks5://...", "username": "u", "password": "p"}) as browser:
76
+ page = browser.new_page()
77
+ page.goto("https://example.com")
78
+ page.click("#submit") # mouse arcs to the button on a Bezier curve
79
+ ```
80
+
81
+ **Async**
82
+ ```python
83
+ from invisible_playwright.async_api import InvisiblePlaywright
84
+
85
+ async with InvisiblePlaywright(proxy={"server": "socks5://...", "username": "u", "password": "p"}) as browser:
86
+ page = await browser.new_page()
87
+ await page.goto("https://example.com")
88
+ await page.click("#submit")
89
+ ```
90
+
91
+ The `browser` object is a `playwright.sync_api.Browser` / `playwright.async_api.Browser` - every Playwright method works as-is.
92
+
93
+ Log the seed to replay a run:
94
+
95
+ ```python
96
+ sf = InvisiblePlaywright()
97
+ with sf as browser:
98
+ print("seed =", sf.seed)
99
+ # ...
100
+ ```
101
+
102
+ ### Reproducible fingerprint
103
+
104
+ ```python
105
+ with InvisiblePlaywright(seed=42) as browser:
106
+ ... # same GPU, same canvas hash, same audio context, every run
107
+ ```
108
+
109
+ ### Proxies
110
+
111
+ ```python
112
+ proxy = {
113
+ "server": "socks5://gate.example.com:1080",
114
+ "username": "user",
115
+ "password": "pass",
116
+ }
117
+ with InvisiblePlaywright(proxy=proxy) as browser:
118
+ ...
119
+ ```
120
+
121
+ Schemes supported: `socks5`, `socks4`, `http`, `https`. DNS is routed through the proxy by default, no local leak.
122
+
123
+ Around 90% of proxies are public, so their IPs are already known and blocked. For the clean 10%, residential IPs that aren't already known, we recommend [sx.org](https://sx.org/?c=invisible_playwright), who filter for and serve only IPs that aren't already on those lists.
124
+
125
+ ### Timezone
126
+
127
+ The browser timezone follows `timezone=`:
128
+
129
+ ```python
130
+ # default: timezone is auto-derived from the egress IP (proxy egress if a
131
+ # proxy is set, otherwise the host's own public IP)
132
+ with InvisiblePlaywright(proxy=proxy) as browser:
133
+ ...
134
+
135
+ # explicit IANA zone always wins, the only way to force a specific zone
136
+ with InvisiblePlaywright(proxy=proxy, timezone="America/New_York") as browser:
137
+ ...
138
+ ```
139
+
140
+ ### Pinning specific fingerprint fields
141
+
142
+ By default everything comes from `seed`. To force specific values while the rest stays seed-derived:
143
+
144
+ ```python
145
+ with InvisiblePlaywright(
146
+ seed=42,
147
+ pin={
148
+ "gpu.renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 Direct3D11)",
149
+ "gpu.vendor": "Google Inc. (NVIDIA)",
150
+ "screen.width": 2560,
151
+ "screen.height": 1440,
152
+ "hardware.concurrency": 16,
153
+ },
154
+ ) as browser:
155
+ ...
156
+ ```
157
+
158
+ Full list of pinnable keys, how pinning interacts with the Bayesian sampler, and common patterns are in **[docs/pinning.md](https://github.com/feder-cr/invisible_playwright/blob/main/docs/pinning.md)**.
159
+
160
+ ---
161
+
162
+ ## CLI
163
+
164
+ ```bash
165
+ invisible_playwright fetch # download the binary if missing
166
+ invisible_playwright fetch --force # re-download even if cached
167
+ invisible_playwright path # print the absolute path to the cached binary
168
+ invisible_playwright version # wrapper and binary versions
169
+ invisible_playwright clear-cache # remove all cached binaries
170
+ ```
171
+
172
+ ## Related projects
173
+
174
+ Related projects that cover similar ground:
175
+
176
+ - **[arkenfox/user.js](https://github.com/arkenfox/user.js)** - Firefox privacy hardening via prefs. invisible_playwright patches C++ where prefs are insufficient.
177
+ - **[LibreWolf](https://librewolf.net)** - Firefox fork with privacy defaults. LibreWolf ships a configured binary; invisible_playwright ships source patches + automation wrapper.
178
+ - **[Camoufox](https://github.com/daijro/camoufox)** - open-source anti-detect Firefox. Patches a wider surface and ships its own fingerprint database; invisible_playwright uses a Bayesian sampler.
179
+
180
+ ---
181
+
182
+ ## License
183
+
184
+ MIT - see [LICENSE](https://github.com/feder-cr/invisible_playwright/blob/main/LICENSE). The patched Firefox binary is distributed under the MPL-2.0 (Firefox upstream license). The C++ patches against mozilla-central that produce that binary are at [feder-cr/firefox_antidetect_patch](https://github.com/feder-cr/firefox_antidetect_patch).
185
+
186
+ ---
187
+
188
+ ## Disclaimer
189
+
190
+ This project is for educational purposes only. It is provided as-is, with no warranties. I take no responsibility for how it is used. Use it at your own risk and in compliance with the laws of your jurisdiction.
191
+
192
+
193
+ ---
194
+
195
+ <p align="center">
196
+ Built by <a href="https://it.linkedin.com/in/federico-elia-5199951b6">Federico Elia</a>
197
+ &nbsp;<a href="https://it.linkedin.com/in/federico-elia-5199951b6"><img src="https://raw.githubusercontent.com/feder-cr/invisible_playwright/main/docs/badges/linkedin.svg" alt="LinkedIn"></a>
198
+ </p>
@@ -0,0 +1,106 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "invisible-playwright"
7
+ version = "0.3.5"
8
+ description = "Playwright wrapper for a patched Firefox with deterministic stealth profile."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{ name = "feder-cr", email = "85809106+feder-cr@users.noreply.github.com" }]
13
+ keywords = ["playwright", "firefox", "stealth", "anti-detect", "automation"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: Microsoft :: Windows",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Testing",
24
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ # Pure config (seed -> fingerprint -> prefs, binary download, proxy, geo) lives
29
+ # in invisible-core since the 2026-07-03 split; it pulls platformdirs/requests/
30
+ # maxminddb/tzdata transitively. The wrapper only adds Playwright.
31
+ #
32
+ # An EXACT specifier, not the bare `invisible-core @ git+...` direct reference
33
+ # this line used to carry. Measured, not assumed: pip is structurally blind to
34
+ # a violated direct reference. A direct reference carries no version specifier,
35
+ # so there is nothing for `pip check` to compare, and it reports "No broken
36
+ # requirements found" (exit 0) on an environment where the wrong core is
37
+ # installed. The same mismatch written as a real version specifier is reported,
38
+ # exit 1. pip also discards the metadata it just built for a moved git tag and
39
+ # leaves direct_url.json recording the OLD commit, so the file meant to be
40
+ # ground truth lies exactly when it matters.
41
+ #
42
+ # Cost, accepted knowingly: the pin literal now moves on every engine release,
43
+ # so a firefox-N bump is a three-repo release instead of a core-only push.
44
+ # invisible-core must be live on the index BEFORE this package is published,
45
+ # or a cold install fails to resolve.
46
+ #
47
+ # This literal is the ONLY place the expected core version is written. The
48
+ # import-time assertion in _pin.py reads it back out of this package's own
49
+ # metadata; nothing in src/ hardcodes a second copy (tests/test_core_pin.py
50
+ # fails if one appears). What stays invisible to install-time machinery is the
51
+ # --no-deps / lockfile residue, and _pin.py is the only layer that sees it.
52
+ "invisible-core==18.0.0",
53
+ # Install-time approximation of the Juggler client range we test against.
54
+ # 1.61 added an isMobile field to Browser.setDefaultViewport that an older
55
+ # Juggler rejects, which kills the session (issue #48). The authoritative
56
+ # range now travels in the seal and is enforced at import by
57
+ # _engine.assert_playwright_range(), so widening it no longer needs a
58
+ # wrapper release; this bound only keeps a fresh install in the ballpark.
59
+ "playwright>=1.55,<=1.61.0",
60
+ ]
61
+
62
+ [project.optional-dependencies]
63
+ dev = ["pytest>=7", "pytest-mock>=3", "responses>=0.24", "build>=1", "pytest-rerunfailures>=14", "playwright>=1.55,<=1.61.0"]
64
+
65
+ [tool.pytest.ini_options]
66
+ markers = [
67
+ "unit: pure-logic tests, no I/O or external deps",
68
+ "integration: multi-module tests, no browser",
69
+ "e2e: requires patched Firefox binary and display",
70
+ "slow: tests that build the wheel - opt-in only",
71
+ "linux_only: tests that require Linux platform",
72
+ ]
73
+ addopts = "-m 'not slow and not e2e'"
74
+ # tests/playwright-upstream/ is a vendored Microsoft Playwright test suite
75
+ # used for compatibility verification on demand. It has its own deps
76
+ # (pixelmatch with API not matching our version) and a conftest that fails
77
+ # collection in our env. Run it explicitly with --override-ini for compat
78
+ # audits, not on every push.
79
+ norecursedirs = ["playwright-upstream"]
80
+
81
+ [project.scripts]
82
+ invisible-playwright = "invisible_playwright.cli:main"
83
+
84
+ [project.urls]
85
+ Homepage = "https://github.com/feder-cr/invisible_playwright"
86
+ Repository = "https://github.com/feder-cr/invisible_playwright"
87
+ Issues = "https://github.com/feder-cr/invisible_playwright/issues"
88
+ Changelog = "https://github.com/feder-cr/invisible_playwright/blob/main/CHANGELOG.md"
89
+ Documentation = "https://github.com/feder-cr/invisible_playwright#readme"
90
+
91
+ # No [tool.hatch.metadata] allow-direct-references opt-in: there is no direct
92
+ # reference left in [project].dependencies, and leaving the switch on is what
93
+ # would let one creep back in silently. Without it hatchling refuses to build a
94
+ # direct reference at all, which turns the decision above into a build-time gate.
95
+
96
+ [tool.hatch.build.targets.wheel]
97
+ packages = ["src/invisible_playwright"]
98
+ exclude = ["*.bak", "*-bak"]
99
+
100
+ [tool.hatch.build.targets.sdist]
101
+ include = ["src/invisible_playwright", "tests", "README.md", "LICENSE", "pyproject.toml"]
102
+ # tests/playwright-upstream/ is a vendored copy of microsoft/playwright-python,
103
+ # Apache-2.0, ~322 files. Redistributing it inside an sdist whose only declared
104
+ # licence is MIT misstates the licence of what is being distributed, and a PyPI
105
+ # filename can never be re-uploaded once published.
106
+ exclude = ["tests/playwright-upstream"]
@@ -0,0 +1,68 @@
1
+ """invisible_playwright - Playwright wrapper for a patched Firefox with stealth profile.
2
+
3
+ Quickstart:
4
+
5
+ from invisible_playwright import InvisiblePlaywright
6
+
7
+ with InvisiblePlaywright() as browser: # random seed
8
+ page = browser.new_page()
9
+ page.goto("https://example.com")
10
+
11
+ with InvisiblePlaywright(seed=42) as browser: # deterministic
12
+ ...
13
+
14
+ with InvisiblePlaywright(humanize=True) as browser: # human-like cursor motion
15
+ page = browser.new_page()
16
+ page.click("#submit") # expanded into a Bezier trajectory
17
+ """
18
+ # ── Import-time core assertion, and repair ───────────────────────────────────
19
+ # Runs BEFORE every other import. Two questions, one check: is the installed
20
+ # invisible-core new enough to carry a release seal at all, and is it the exact
21
+ # version this distribution declares in pyproject? The expected version is read
22
+ # back out of our own metadata, so it is written in exactly one place.
23
+ #
24
+ # A mismatch is REPAIRED, not just reported: the declared core is installed and
25
+ # picked up in this process, so the script that just imported us keeps going.
26
+ # That is only sound here, on the first line, while invisible_core has not been
27
+ # imported by anyone yet; _pin.py captures exactly that fact before it imports
28
+ # the core, and the repair refuses itself if the snapshot says otherwise. Every
29
+ # failure of the repair falls back to the message it replaced.
30
+ #
31
+ # The comparison and the repair both live in invisible_core._pin, shared with the
32
+ # profile manager so both products diagnose and fix an environment the same way.
33
+ # Our _pin.py is the floor: it owns the three states a module inside the core
34
+ # cannot report on (core absent, core present but unimportable, core present but
35
+ # too damaged to derive its own version) and delegates everything else.
36
+ from ._pin import enforce_core_pin as _enforce_core_pin
37
+ _enforce_core_pin()
38
+
39
+ from ._engine import assert_playwright_range as _assert_pw_range
40
+ _assert_pw_range()
41
+
42
+ from .config import get_default_args, get_default_stealth_prefs
43
+ from .constants import BINARY_VERSION, FIREFOX_UPSTREAM_VERSION
44
+ from ._geo import GeoTimezoneError, resolve_session_timezone
45
+ from .download import ensure_binary, ensure_geoip_mmdb
46
+ from .launcher import InvisiblePlaywright
47
+
48
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
49
+
50
+ try:
51
+ __version__ = _pkg_version("invisible-playwright")
52
+ except PackageNotFoundError:
53
+ # Editable / source checkout without an install record: fall back to a
54
+ # marker rather than risk shipping a stale hardcoded string.
55
+ __version__ = "0.0.0+unknown"
56
+
57
+ __all__ = [
58
+ "InvisiblePlaywright",
59
+ "ensure_binary",
60
+ "ensure_geoip_mmdb",
61
+ "get_default_stealth_prefs",
62
+ "get_default_args",
63
+ "resolve_session_timezone",
64
+ "GeoTimezoneError",
65
+ "BINARY_VERSION",
66
+ "FIREFOX_UPSTREAM_VERSION",
67
+ "__version__",
68
+ ]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+ import sys
3
+
4
+ sys.exit(main())