Skip to content

veilbrowser (Python) 1.3.1 — first release

Latest

Choose a tag to compare

@acunningham-ship-it acunningham-ship-it released this 26 Jul 18:07

First release of the Python front end. Same browser, same stealth, Python API.

pip install veilbrowser-1.3.1-py3-none-any.whl        # attached below
# or straight from the repo:
pip install "git+https://github.com/acunningham-ship-it/veilbrowser.git@python-v1.3.1#subdirectory=python"
import asyncio
from veilbrowser import Browser, Fingerprint

async def main():
    async with await Browser.launch(fingerprint=Fingerprint.preset("windows-chrome")) as b:
        page = await b.new_page()
        await page.goto("https://example.com")
        print(await page.title())
        await page.screenshot("shot.png")

asyncio.run(main())

Drive a profile you're already signed into

Chrome locks a user-data-dir, so the profile carrying your real sessions cannot be opened by a second instance. Attach instead of launching:

b = await Browser.connect("127.0.0.1:9222")   # ws:// URL, http:// origin, or host:port
page = (await b.pages())[0]                    # the tab that's already open
await b.close()                                # detaches — never kills a browser it didn't start

That's the difference between "works on sites that allow anonymous access" and "works on the sites that matter": Google, Reddit, Meta and TikTok score the session, not the IP.

The stealth layer has one implementation, not two

The injected script, launch flags, profile identities and keystroke table are generated from the TypeScript source into veilbrowser/_assets/, and tests/python-parity.test.ts fails the build if Python's assembled script differs from the TypeScript one by a single byte.

That's deliberate. Two hand-maintained copies of a stealth patch drift, and a drifted patch doesn't fail loudly — one front end just becomes detectable while all of its own tests stay green.

Verified (21/21, from the installed wheel, outside the source tree)

navigator.webdriver false · UA / platform / screen / timezone / WebGL from the profile · patched getters report [native code] and are inherited from the prototype rather than own-properties · canvas noise stable across reads (a per-call random would itself be the tell) · real keyCodes, not 0 · fragment goto 6 ms vs 110 ms full load · close() on an attached browser leaves it alive.

API

Browser.launch/connect/new_page/pages/close · page.goto/evaluate/click/fill/type/press/select/wait_for_selector/wait_for/wait_for_response/text/attribute/title/content/inner_text/url/screenshot/pdf/get_cookies/set_cookies/scroll/set_viewport/block_private_network · Fingerprint.preset/random

One dependency (websockets); everything else is stdlib.

evaluate() takes a string, not a callable — Python can't serialise a function to JS. An arrow function passed as a string evaluates to a function object, which can't be returned by value, so wrap it: "(() => document.title)()".

Deliberately not ported

The snapshot/ref system (selectors are what Python callers write), auto-Xvfb (set DISPLAY or use xvfb-run; it degrades to headless rather than failing), downloads, FedCM, frame switching. Fingerprint.random(seed) is deterministic but does not produce the same identity as the TypeScript random(seed) — different PRNGs, and nothing needs the two languages to agree on a random identity, only on the stealth script.

Full API: python/README.md