phasmid-profile-gen 0.1.0__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.

Potentially problematic release.


This version of phasmid-profile-gen might be problematic. Click here for more details.

Files changed (41) hide show
  1. phasmid_profile_gen-0.1.0/.gitignore +9 -0
  2. phasmid_profile_gen-0.1.0/.pre-commit-config.yaml +8 -0
  3. phasmid_profile_gen-0.1.0/.python-version +1 -0
  4. phasmid_profile_gen-0.1.0/.ruff.toml +63 -0
  5. phasmid_profile_gen-0.1.0/CHANGELOG.md +16 -0
  6. phasmid_profile_gen-0.1.0/LICENSE +30 -0
  7. phasmid_profile_gen-0.1.0/PKG-INFO +203 -0
  8. phasmid_profile_gen-0.1.0/README.md +176 -0
  9. phasmid_profile_gen-0.1.0/pyproject.toml +67 -0
  10. phasmid_profile_gen-0.1.0/scripts/build_data.py +514 -0
  11. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/__init__.py +141 -0
  12. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/__main__.py +5 -0
  13. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/_data.py +25 -0
  14. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/captures.py +81 -0
  15. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/claims.py +188 -0
  16. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/cli.py +177 -0
  17. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/CLDR-LICENSE +41 -0
  18. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/LICENSES.md +18 -0
  19. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/captures.json.xz +0 -0
  20. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/dbip-city-lite.json +11 -0
  21. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/forge.json.xz +0 -0
  22. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/fpgen.json.xz +0 -0
  23. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/locale.json.xz +0 -0
  24. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/manifest.json +88 -0
  25. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/schema.json.xz +0 -0
  26. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/forge.py +101 -0
  27. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/generate.py +492 -0
  28. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/geoip.py +194 -0
  29. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/locales.py +125 -0
  30. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/location.py +217 -0
  31. phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/surface.py +131 -0
  32. phasmid_profile_gen-0.1.0/tests/conftest.py +46 -0
  33. phasmid_profile_gen-0.1.0/tests/fixtures/geoip-rows.json +30 -0
  34. phasmid_profile_gen-0.1.0/tests/fixtures/phasmid-samples/gen-linux-1707x960-be27e761.json +781 -0
  35. phasmid_profile_gen-0.1.0/tests/fixtures/phasmid-samples/gen-linux-1920x1080-a493e899.json +784 -0
  36. phasmid_profile_gen-0.1.0/tests/fixtures/phasmid-samples/gen-linux-3072x1728-6e34c862.json +780 -0
  37. phasmid_profile_gen-0.1.0/tests/test_build_data.py +124 -0
  38. phasmid_profile_gen-0.1.0/tests/test_data.py +110 -0
  39. phasmid_profile_gen-0.1.0/tests/test_generate.py +336 -0
  40. phasmid_profile_gen-0.1.0/tests/test_location.py +331 -0
  41. phasmid_profile_gen-0.1.0/uv.lock +759 -0
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .claude-scratch/
@@ -0,0 +1,8 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.13.0
4
+ hooks:
5
+ - id: ruff-check
6
+ args: [--fix]
7
+ - id: ruff-format
8
+
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,63 @@
1
+ # Exclude a variety of commonly ignored directories.
2
+ exclude = [
3
+ ".bzr",
4
+ ".direnv",
5
+ ".eggs",
6
+ ".git",
7
+ ".git-rewrite",
8
+ ".hg",
9
+ ".ipynb_checkpoints",
10
+ ".mypy_cache",
11
+ ".nox",
12
+ ".pants.d",
13
+ ".pyenv",
14
+ ".pytest_cache",
15
+ ".pytype",
16
+ ".ruff_cache",
17
+ ".svn",
18
+ ".tox",
19
+ ".venv",
20
+ ".vscode",
21
+ "__pypackages__",
22
+ "_build",
23
+ "buck-out",
24
+ "build",
25
+ "dist",
26
+ "node_modules",
27
+ "site-packages",
28
+ "venv",
29
+ "*/migrations/*",
30
+ ]
31
+
32
+ line-length = 100
33
+ indent-width = 4
34
+
35
+ [lint]
36
+ select = [
37
+ "E", # pycodestyle errors
38
+ "W", # pycodestyle warnings
39
+ "F", # pyflakes
40
+ "I", # isort
41
+ "B", # flake8-bugbear
42
+ "C4", # flake8-comprehensions
43
+ "UP", # pyupgrade
44
+ ]
45
+ ignore = [
46
+ ]
47
+
48
+ # Per-file ignores for common patterns
49
+ [lint.per-file-ignores]
50
+ "**/tests/**" = [
51
+ "F841", # Local variable is assigned to but never used
52
+ ]
53
+
54
+ [format]
55
+ # Like Black, use double quotes for strings.
56
+ quote-style = "double"
57
+
58
+ # Like Black, indent with spaces, rather than tabs.
59
+ indent-style = "space"
60
+
61
+ # Like Black, respect magic trailing commas.
62
+ skip-magic-trailing-comma = false
63
+
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-11-05)
4
+
5
+ First release: Phasmid's `tools/genprofile` as a package, with its data bundled.
6
+
7
+ - Generates the same profiles as `tools/genprofile` from the same captures,
8
+ byte for byte except for the generator's name in `captured.note`.
9
+ - Data built from Phasmid at the commit in `data/manifest.json`, BrowserForge's
10
+ fingerprint network (`apify_fingerprint_datapoints` 0.15.0) and one
11
+ collector-app capture (Windows 10, Intel HD Graphics 4600).
12
+ - A Windows capture backs only its own marketing version (10 or 11), so the
13
+ claimed `platformVersion` agrees with the capture's fonts and colours.
14
+ - `PHASMID_GEOIP_MMDB` / `--geoip-db` use a DB-IP City Lite build other than
15
+ the pinned one once DB-IP stops serving it.
16
+ - CLI subcommands: `generate`, `locate`, `gpus`, `captures`, `geoip`, `validate`.
@@ -0,0 +1,30 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, The Phasmid Authors
4
+ Copyright (c) 2015-2026, The ungoogled-chromium Authors
5
+ All rights reserved.
6
+
7
+ Redistribution and use in source and binary forms, with or without
8
+ modification, are permitted provided that the following conditions are met:
9
+
10
+ 1. Redistributions of source code must retain the above copyright notice, this
11
+ list of conditions and the following disclaimer.
12
+
13
+ 2. Redistributions in binary form must reproduce the above copyright notice,
14
+ this list of conditions and the following disclaimer in the documentation
15
+ and/or other materials provided with the distribution.
16
+
17
+ 3. Neither the name of the copyright holder nor the names of its
18
+ contributors may be used to endorse or promote products derived from
19
+ this software without specific prior written permission.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.5
2
+ Name: phasmid-profile-gen
3
+ Version: 0.1.0
4
+ Summary: Generate Phasmid browser profiles from BrowserForge marginals and captured real devices
5
+ Project-URL: Homepage, https://github.com/tintty1/phasmid-profile-gen
6
+ Project-URL: Repository, https://github.com/tintty1/phasmid-profile-gen
7
+ Project-URL: Issues, https://github.com/tintty1/phasmid-profile-gen/issues
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ License-File: src/phasmid_profile_gen/data/LICENSES.md
11
+ Keywords: browser fingerprint,browser profile,chromium,phasmid
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: jsonschema>=4.23
22
+ Provides-Extra: location
23
+ Requires-Dist: maxminddb==3.2.0; extra == 'location'
24
+ Requires-Dist: timezonefinder-data==4.2026.5; extra == 'location'
25
+ Requires-Dist: timezonefinder==9.2.0; extra == 'location'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # phasmid-profile-gen
29
+
30
+ [![Test](https://github.com/tintty1/phasmid-profile-gen/actions/workflows/test.yml/badge.svg)](https://github.com/tintty1/phasmid-profile-gen/actions/workflows/test.yml)
31
+ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](LICENSE)
32
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
33
+
34
+ Generates device profiles for [Phasmid](https://github.com/tintty1/phasmid), the
35
+ Chromium fork that reports the device described by one JSON profile instead of
36
+ the machine it runs on.
37
+
38
+ This is Phasmid's `tools/genprofile` as a standalone package. The data it needs
39
+ is bundled in compressed form (about 60 KB), so it runs without a Phasmid
40
+ checkout and without BrowserForge installed. From the same captures it produces
41
+ the same profiles as `tools/genprofile`, byte for byte, except for the
42
+ generator's name in `captured.note`.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install phasmid-profile-gen # profiles with a given time zone
48
+ pip install 'phasmid-profile-gen[location]' # also place profiles at a proxy exit IP
49
+ ```
50
+
51
+ Python 3.12 or newer. The `location` extra adds `timezonefinder` (with its
52
+ boundary data, about 33 MB) and `maxminddb`, pinned exactly because they decide
53
+ which zone and position a seed gets.
54
+
55
+ ## Use
56
+
57
+ ```python
58
+ from phasmid_profile_gen import dumps, generate_profile
59
+
60
+ # Placed where the proxy comes out: zone, geolocation, WebRTC address, languages.
61
+ profile = generate_profile("windows", exit_ip=proxy_exit_ip)
62
+
63
+ # Or with an explicit locale, reproducibly.
64
+ profile = generate_profile(
65
+ "linux",
66
+ seed="0a1b2c3d4e5f60718293a4b5c6d7e8f9",
67
+ timezone="Europe/Berlin",
68
+ languages=["de-DE", "de", "en-US", "en"],
69
+ )
70
+ open("profile.json", "w").write(dumps(profile))
71
+ ```
72
+
73
+ ```bash
74
+ phasmid-profile-gen generate --os windows --exit-ip <proxy exit IP> -o profile.json
75
+ phasmid-profile-gen generate --os linux --seed 0a1b2c3d4e5f60718293a4b5c6d7e8f9 \
76
+ --timezone Europe/Berlin --languages de-DE,de,en-US,en -o profile.json
77
+ phasmid-profile-gen gpus # GPUs the bundled captures back (for --gpu)
78
+ phasmid-profile-gen captures # the bundled captures
79
+ phasmid-profile-gen locate <proxy exit IP> --seed <seed>
80
+ phasmid-profile-gen validate profile.json
81
+ ```
82
+
83
+ Give exactly one of an exit IP and a time zone. **Generate Windows profiles
84
+ with an exit IP.** Windows Chrome answers geolocation from the system location
85
+ provider, so a Windows profile with no position is a stronger tell than a Linux
86
+ one. The seed is 32 lowercase hex characters. When omitted, a new one is drawn;
87
+ read it back from `profile["seed"]`. The same seed and inputs always give the
88
+ same bytes (`dumps`).
89
+
90
+ Other functions: `locate(ip, seed)`, `list_gpus()`, `list_captures()`,
91
+ `validate(profile)` (against the bundled Phasmid schema) and `new_seed()`.
92
+ Errors are `GenerationError`, `LocationError` and `geoip.GeoipError`.
93
+
94
+ ## What a profile is made of
95
+
96
+ | Part | From |
97
+ | --- | --- |
98
+ | OS identity, CPU threads, memory class, touch points, screen and window geometry, media device presence, battery | A [BrowserForge](https://github.com/daijro/browserforge) sample for desktop Chrome on the OS, drawn from the seed. Each node is drawn for one sampled user agent, filtered to what the OS and the capture allow. |
99
+ | GPU, WebGL and WebGPU surface, fonts, voices, audio, codecs, system colours, pointer, preferences, RAM, disk, media device labels | One capture of a real device. On Linux the GPU's identity may instead be one that the capture's driver class reports with the exact same WebGL surface (Phasmid decision `gpu-claims-stay-within-a-captured-host-class`). |
100
+ | Time zone, geolocation, WebRTC public IP, languages | The exit IP: DB-IP City Lite, timezonefinder, CLDR, and Chrome's default accept-languages for the country's UI locale. Otherwise the time zone and languages you pass. |
101
+
102
+ No user-agent or version string is written. Phasmid builds those from its own
103
+ version. Nothing is invented: a field only a capture can supply is never
104
+ filled in by guessing, and an OS without a capture fails with
105
+ `MissingCaptures`. `captured.note` records where every part came from.
106
+
107
+ ### Bundled captures
108
+
109
+ Run `phasmid-profile-gen captures` for the current list. It is also in
110
+ [`data/manifest.json`](src/phasmid_profile_gen/data/manifest.json), which lists
111
+ the dumps refused and why.
112
+
113
+ - Phasmid's own: Linux (Arch, RTX 3060) and Windows 11 (Intel UHD 600).
114
+ - From the [collector app](https://phasmid.ecomeezy.com/): Windows 10 (Intel
115
+ HD Graphics 4600). Visitors grant no media permission, so the dump has no
116
+ device labels. Profiles built from it claim no microphone, speaker or
117
+ camera.
118
+
119
+ A Windows capture backs only its own version: a Windows 11 capture gives
120
+ Windows 11 profiles (`platformVersion` 13 and up), a Windows 10 capture
121
+ Windows 10 ones. Their fonts differ (Windows 11 dropped HoloLens MDL2 Assets
122
+ and added Segoe Fluent Icons). This is the one intended difference from
123
+ `tools/genprofile`, which drew either version for its Windows 11 capture.
124
+
125
+ ## The location database
126
+
127
+ The exit IP is looked up in DB-IP IP to City Lite (CC BY 4.0, "IP Geolocation
128
+ by DB-IP"). It is about 127 MB, so it is not bundled. The build pinned in
129
+ `data/dbip-city-lite.json` is downloaded on first use, checked against the
130
+ pin, and kept in `$XDG_CACHE_HOME/phasmid/geoip/`. That is the cache Phasmid's
131
+ tools use, so an existing download is reused.
132
+
133
+ ```bash
134
+ phasmid-profile-gen geoip # download and verify now
135
+ phasmid-profile-gen geoip --archive dbip-city-lite-2026-10.mmdb.gz
136
+ ```
137
+
138
+ DB-IP serves only the last two monthly builds. Once the pinned one is gone,
139
+ either install a saved copy with `--archive`, or use any other DB-IP City Lite
140
+ build with `PHASMID_GEOIP_MMDB=/path/to/dbip-city-lite.mmdb`, `--geoip-db` or
141
+ `geoip_db=`. Another build is not checked against the pin; the profile's note
142
+ names its build date and sha256 instead.
143
+
144
+ ## Rebuilding the data
145
+
146
+ `src/phasmid_profile_gen/data/` is generated, compressed and committed. It is
147
+ rebuilt from a Phasmid checkout and collector-app dumps by
148
+ [`scripts/build_data.py`](scripts/build_data.py):
149
+
150
+ ```bash
151
+ uv sync --group build-data
152
+ # in collector-app: npm run pull -- --out captures
153
+ uv run --group build-data python scripts/build_data.py \
154
+ --phasmid ../phasmid \
155
+ --collector ../collector-app/captures/d1-2026-11-05/dumps
156
+ ```
157
+
158
+ The build:
159
+
160
+ - maps every dump with the checkout's own `tests/consistency/capture.py` (the
161
+ code `consistency capture` runs), and checks Phasmid's captures against their
162
+ records as `tools/genprofile` does;
163
+ - keeps a collector dump only if it is stock desktop Google Chrome on Windows
164
+ or Linux, with a hardware GPU, a consistent UA and UA-CH, and a window inside
165
+ its screen, and only if it maps to a valid profile. Each refusal is listed
166
+ with its reason in `manifest.json`;
167
+ - stores only what generation reads. Raw dumps and visitors' locales are never
168
+ written;
169
+ - restricts BrowserForge's network to desktop Chrome on Windows and Linux and
170
+ the nodes the generator draws. `tests/test_data.py` checks every table
171
+ against browserforge itself.
172
+
173
+ The output is deterministic, so a rebuild from the same sources gives the same
174
+ bytes. A Linux collector dump also needs the operator's OS version (`?manual=1`),
175
+ which a page cannot read.
176
+
177
+ ## Develop
178
+
179
+ ```bash
180
+ uv sync --all-extras --group build-data
181
+ uv run pytest
182
+ uv run pre-commit run --all-files
183
+ PHASMID_GEOIP_NETWORK=1 uv run pytest -m network # reads the real DB-IP build
184
+ ```
185
+
186
+ Releases are published to PyPI by `.github/workflows/publish-pypi.yaml` when a
187
+ GitHub release is published. The version comes from the release tag
188
+ (`uv-dynamic-versioning`, e.g. `v0.1.0`). Publishing uses PyPI trusted
189
+ publishing: register the repository and that workflow as a trusted publisher
190
+ of `phasmid-profile-gen` on PyPI once.
191
+
192
+ ## Licence
193
+
194
+ The code is BSD 3-Clause, as Phasmid is ([`LICENSE`](LICENSE)). The bundled
195
+ data keeps its sources' licences:
196
+
197
+ - BrowserForge's network: Apache 2.0.
198
+ - Unicode CLDR: Unicode License v3.
199
+ - Chromium's locale table: BSD-style.
200
+
201
+ DB-IP (CC BY 4.0) and timezone-boundary-builder (ODbL 1.0) are used at runtime
202
+ but not bundled. See
203
+ [`data/LICENSES.md`](src/phasmid_profile_gen/data/LICENSES.md).
@@ -0,0 +1,176 @@
1
+ # phasmid-profile-gen
2
+
3
+ [![Test](https://github.com/tintty1/phasmid-profile-gen/actions/workflows/test.yml/badge.svg)](https://github.com/tintty1/phasmid-profile-gen/actions/workflows/test.yml)
4
+ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](LICENSE)
5
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
6
+
7
+ Generates device profiles for [Phasmid](https://github.com/tintty1/phasmid), the
8
+ Chromium fork that reports the device described by one JSON profile instead of
9
+ the machine it runs on.
10
+
11
+ This is Phasmid's `tools/genprofile` as a standalone package. The data it needs
12
+ is bundled in compressed form (about 60 KB), so it runs without a Phasmid
13
+ checkout and without BrowserForge installed. From the same captures it produces
14
+ the same profiles as `tools/genprofile`, byte for byte, except for the
15
+ generator's name in `captured.note`.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install phasmid-profile-gen # profiles with a given time zone
21
+ pip install 'phasmid-profile-gen[location]' # also place profiles at a proxy exit IP
22
+ ```
23
+
24
+ Python 3.12 or newer. The `location` extra adds `timezonefinder` (with its
25
+ boundary data, about 33 MB) and `maxminddb`, pinned exactly because they decide
26
+ which zone and position a seed gets.
27
+
28
+ ## Use
29
+
30
+ ```python
31
+ from phasmid_profile_gen import dumps, generate_profile
32
+
33
+ # Placed where the proxy comes out: zone, geolocation, WebRTC address, languages.
34
+ profile = generate_profile("windows", exit_ip=proxy_exit_ip)
35
+
36
+ # Or with an explicit locale, reproducibly.
37
+ profile = generate_profile(
38
+ "linux",
39
+ seed="0a1b2c3d4e5f60718293a4b5c6d7e8f9",
40
+ timezone="Europe/Berlin",
41
+ languages=["de-DE", "de", "en-US", "en"],
42
+ )
43
+ open("profile.json", "w").write(dumps(profile))
44
+ ```
45
+
46
+ ```bash
47
+ phasmid-profile-gen generate --os windows --exit-ip <proxy exit IP> -o profile.json
48
+ phasmid-profile-gen generate --os linux --seed 0a1b2c3d4e5f60718293a4b5c6d7e8f9 \
49
+ --timezone Europe/Berlin --languages de-DE,de,en-US,en -o profile.json
50
+ phasmid-profile-gen gpus # GPUs the bundled captures back (for --gpu)
51
+ phasmid-profile-gen captures # the bundled captures
52
+ phasmid-profile-gen locate <proxy exit IP> --seed <seed>
53
+ phasmid-profile-gen validate profile.json
54
+ ```
55
+
56
+ Give exactly one of an exit IP and a time zone. **Generate Windows profiles
57
+ with an exit IP.** Windows Chrome answers geolocation from the system location
58
+ provider, so a Windows profile with no position is a stronger tell than a Linux
59
+ one. The seed is 32 lowercase hex characters. When omitted, a new one is drawn;
60
+ read it back from `profile["seed"]`. The same seed and inputs always give the
61
+ same bytes (`dumps`).
62
+
63
+ Other functions: `locate(ip, seed)`, `list_gpus()`, `list_captures()`,
64
+ `validate(profile)` (against the bundled Phasmid schema) and `new_seed()`.
65
+ Errors are `GenerationError`, `LocationError` and `geoip.GeoipError`.
66
+
67
+ ## What a profile is made of
68
+
69
+ | Part | From |
70
+ | --- | --- |
71
+ | OS identity, CPU threads, memory class, touch points, screen and window geometry, media device presence, battery | A [BrowserForge](https://github.com/daijro/browserforge) sample for desktop Chrome on the OS, drawn from the seed. Each node is drawn for one sampled user agent, filtered to what the OS and the capture allow. |
72
+ | GPU, WebGL and WebGPU surface, fonts, voices, audio, codecs, system colours, pointer, preferences, RAM, disk, media device labels | One capture of a real device. On Linux the GPU's identity may instead be one that the capture's driver class reports with the exact same WebGL surface (Phasmid decision `gpu-claims-stay-within-a-captured-host-class`). |
73
+ | Time zone, geolocation, WebRTC public IP, languages | The exit IP: DB-IP City Lite, timezonefinder, CLDR, and Chrome's default accept-languages for the country's UI locale. Otherwise the time zone and languages you pass. |
74
+
75
+ No user-agent or version string is written. Phasmid builds those from its own
76
+ version. Nothing is invented: a field only a capture can supply is never
77
+ filled in by guessing, and an OS without a capture fails with
78
+ `MissingCaptures`. `captured.note` records where every part came from.
79
+
80
+ ### Bundled captures
81
+
82
+ Run `phasmid-profile-gen captures` for the current list. It is also in
83
+ [`data/manifest.json`](src/phasmid_profile_gen/data/manifest.json), which lists
84
+ the dumps refused and why.
85
+
86
+ - Phasmid's own: Linux (Arch, RTX 3060) and Windows 11 (Intel UHD 600).
87
+ - From the [collector app](https://phasmid.ecomeezy.com/): Windows 10 (Intel
88
+ HD Graphics 4600). Visitors grant no media permission, so the dump has no
89
+ device labels. Profiles built from it claim no microphone, speaker or
90
+ camera.
91
+
92
+ A Windows capture backs only its own version: a Windows 11 capture gives
93
+ Windows 11 profiles (`platformVersion` 13 and up), a Windows 10 capture
94
+ Windows 10 ones. Their fonts differ (Windows 11 dropped HoloLens MDL2 Assets
95
+ and added Segoe Fluent Icons). This is the one intended difference from
96
+ `tools/genprofile`, which drew either version for its Windows 11 capture.
97
+
98
+ ## The location database
99
+
100
+ The exit IP is looked up in DB-IP IP to City Lite (CC BY 4.0, "IP Geolocation
101
+ by DB-IP"). It is about 127 MB, so it is not bundled. The build pinned in
102
+ `data/dbip-city-lite.json` is downloaded on first use, checked against the
103
+ pin, and kept in `$XDG_CACHE_HOME/phasmid/geoip/`. That is the cache Phasmid's
104
+ tools use, so an existing download is reused.
105
+
106
+ ```bash
107
+ phasmid-profile-gen geoip # download and verify now
108
+ phasmid-profile-gen geoip --archive dbip-city-lite-2026-10.mmdb.gz
109
+ ```
110
+
111
+ DB-IP serves only the last two monthly builds. Once the pinned one is gone,
112
+ either install a saved copy with `--archive`, or use any other DB-IP City Lite
113
+ build with `PHASMID_GEOIP_MMDB=/path/to/dbip-city-lite.mmdb`, `--geoip-db` or
114
+ `geoip_db=`. Another build is not checked against the pin; the profile's note
115
+ names its build date and sha256 instead.
116
+
117
+ ## Rebuilding the data
118
+
119
+ `src/phasmid_profile_gen/data/` is generated, compressed and committed. It is
120
+ rebuilt from a Phasmid checkout and collector-app dumps by
121
+ [`scripts/build_data.py`](scripts/build_data.py):
122
+
123
+ ```bash
124
+ uv sync --group build-data
125
+ # in collector-app: npm run pull -- --out captures
126
+ uv run --group build-data python scripts/build_data.py \
127
+ --phasmid ../phasmid \
128
+ --collector ../collector-app/captures/d1-2026-11-05/dumps
129
+ ```
130
+
131
+ The build:
132
+
133
+ - maps every dump with the checkout's own `tests/consistency/capture.py` (the
134
+ code `consistency capture` runs), and checks Phasmid's captures against their
135
+ records as `tools/genprofile` does;
136
+ - keeps a collector dump only if it is stock desktop Google Chrome on Windows
137
+ or Linux, with a hardware GPU, a consistent UA and UA-CH, and a window inside
138
+ its screen, and only if it maps to a valid profile. Each refusal is listed
139
+ with its reason in `manifest.json`;
140
+ - stores only what generation reads. Raw dumps and visitors' locales are never
141
+ written;
142
+ - restricts BrowserForge's network to desktop Chrome on Windows and Linux and
143
+ the nodes the generator draws. `tests/test_data.py` checks every table
144
+ against browserforge itself.
145
+
146
+ The output is deterministic, so a rebuild from the same sources gives the same
147
+ bytes. A Linux collector dump also needs the operator's OS version (`?manual=1`),
148
+ which a page cannot read.
149
+
150
+ ## Develop
151
+
152
+ ```bash
153
+ uv sync --all-extras --group build-data
154
+ uv run pytest
155
+ uv run pre-commit run --all-files
156
+ PHASMID_GEOIP_NETWORK=1 uv run pytest -m network # reads the real DB-IP build
157
+ ```
158
+
159
+ Releases are published to PyPI by `.github/workflows/publish-pypi.yaml` when a
160
+ GitHub release is published. The version comes from the release tag
161
+ (`uv-dynamic-versioning`, e.g. `v0.1.0`). Publishing uses PyPI trusted
162
+ publishing: register the repository and that workflow as a trusted publisher
163
+ of `phasmid-profile-gen` on PyPI once.
164
+
165
+ ## Licence
166
+
167
+ The code is BSD 3-Clause, as Phasmid is ([`LICENSE`](LICENSE)). The bundled
168
+ data keeps its sources' licences:
169
+
170
+ - BrowserForge's network: Apache 2.0.
171
+ - Unicode CLDR: Unicode License v3.
172
+ - Chromium's locale table: BSD-style.
173
+
174
+ DB-IP (CC BY 4.0) and timezone-boundary-builder (ODbL 1.0) are used at runtime
175
+ but not bundled. See
176
+ [`data/LICENSES.md`](src/phasmid_profile_gen/data/LICENSES.md).
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "phasmid-profile-gen"
3
+ dynamic = ["version"]
4
+ description = "Generate Phasmid browser profiles from BrowserForge marginals and captured real devices"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "BSD-3-Clause"
8
+ license-files = ["LICENSE", "src/phasmid_profile_gen/data/LICENSES.md"]
9
+ keywords = ["phasmid", "chromium", "browser fingerprint", "browser profile"]
10
+ classifiers = [
11
+ "Intended Audience :: Developers",
12
+ "Operating System :: OS Independent",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Programming Language :: Python :: 3.14",
17
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ ]
20
+ dependencies = [
21
+ "jsonschema>=4.23",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ # Placing a profile at a proxy exit IP. Pinned exactly: the zone polygons decide
26
+ # which zone and position a seed gives, so another data release changes profiles.
27
+ location = [
28
+ "maxminddb==3.2.0",
29
+ "timezonefinder==9.2.0",
30
+ "timezonefinder-data==4.2026.5",
31
+ ]
32
+
33
+ [project.scripts]
34
+ phasmid-profile-gen = "phasmid_profile_gen.cli:main"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/tintty1/phasmid-profile-gen"
38
+ Repository = "https://github.com/tintty1/phasmid-profile-gen"
39
+ Issues = "https://github.com/tintty1/phasmid-profile-gen/issues"
40
+
41
+ [build-system]
42
+ requires = ["hatchling", "uv-dynamic-versioning"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [tool.hatch.version]
46
+ source = "uv-dynamic-versioning"
47
+
48
+ [tool.hatch.build.targets.sdist]
49
+ exclude = [".github", ".claude-scratch"]
50
+
51
+ [dependency-groups]
52
+ dev = [
53
+ "pytest>=8",
54
+ "ruff",
55
+ "pre-commit>=4.3.0",
56
+ ]
57
+ # Only for scripts/build_data.py, which rebuilds src/phasmid_profile_gen/data.
58
+ build-data = [
59
+ "browserforge==1.2.4",
60
+ "apify-fingerprint-datapoints==0.15.0",
61
+ ]
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
65
+ markers = [
66
+ "network: downloads the pinned DB-IP database (run with PHASMID_GEOIP_NETWORK=1)",
67
+ ]