camoufox-playwright-cli 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shun
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.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # camoufox-playwright-cli
2
+
3
+ Official [`@playwright/cli`](https://github.com/microsoft/playwright-cli) (Microsoft's Playwright CLI — open, click, fill, snapshot, tabs, network, tracing, video, ...), driven by [Camoufox](https://camoufox.com) instead of stock Chromium/Firefox, so the browser doesn't look automated.
4
+
5
+ No fork of playwright-cli, no monkey-patching. Camoufox's stealth is just three standard Playwright launch options — `executablePath`, `firefoxUserPrefs`, and a `CAMOU_CONFIG_*` env blob — and playwright-cli already passes those through from a config file. This package generates that config for you and manages it as named **profiles**.
6
+
7
+ Short alias: `cpw` (same binary as `camoufox-playwright-cli`).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g camoufox-playwright-cli
13
+ cpw install
14
+ ```
15
+
16
+ `install` downloads the Camoufox browser binary (shared cache, only happens once even across other Camoufox-based tools) and generates a `default` profile.
17
+
18
+ ## Profiles
19
+
20
+ A profile is a fingerprint identity + its own isolated browser data (cookies, localStorage, login sessions) — think of it as "one persona". The `default` profile is created automatically and its fingerprint is generated from **your actual machine** where possible:
21
+
22
+ - Real GPU vendor (via `Get-CimInstance Win32_VideoController` on Windows) — matched to the closest vendor bucket in Camoufox's curated WebGL database. Camoufox only ships a fixed, vetted set of older reference GPU models per vendor (no RTX 20/30/40-series entries at all, for example), so the *vendor* will match your real hardware but the exact renderer string won't be your literal card model. That vendor-match is what fingerprinting scripts actually cross-check; matching the exact model isn't possible through Camoufox's public API without maintaining a separate spoofing database, which is out of scope here.
23
+ - Real screen resolution, locale, and timezone (via `Intl` + WMI).
24
+
25
+ If any of these can't be detected (non-Windows, sandboxed environment, etc.) that field silently falls back to Camoufox's own randomized default — it never errors out.
26
+
27
+ ```bash
28
+ cpw profile list
29
+ cpw profile create work
30
+ cpw profile rotate work # wipes cookies + regenerates fingerprint
31
+ cpw profile delete work
32
+ ```
33
+
34
+ Rotating or deleting a profile always wipes its browser session data first — keeping old cookies with a new fingerprint is itself an inconsistency signal, so we don't allow that combination.
35
+
36
+ ## Usage
37
+
38
+ Everything besides `install` and `profile` is forwarded verbatim to the real `playwright-cli`, with `--profile` mapped to its `-s=` session flag and (for `open`) `--config=` pointed at that profile's generated config:
39
+
40
+ ```bash
41
+ cpw open https://example.com
42
+ cpw snapshot
43
+ cpw click e3
44
+ cpw fill e5 "hello"
45
+ cpw screenshot --filename out.png
46
+ cpw close
47
+
48
+ cpw --profile=work open https://example.com # separate identity + separate cookies
49
+ ```
50
+
51
+ See the [playwright-cli command reference](https://github.com/microsoft/playwright-cli) for the full list — tabs, network routing, storage state, tracing, video recording, etc. all work unmodified.
52
+
53
+ ## How it works
54
+
55
+ - [`camoufox-js`](https://github.com/apify/camoufox-js)'s `launchOptions()` computes `{executablePath, env, firefoxUserPrefs}` for a given fingerprint.
56
+ - We write that into `~/.camoufox-playwright-cli/profiles/<name>/cli.config.json`, in the shape `@playwright/cli` expects (`browser.launchOptions`).
57
+ - `cpw <command>` is a thin wrapper: it ensures that config exists, then spawns the real `playwright-cli` binary with `-s=<profile>` (and `--config=` on `open`) injected.
58
+
59
+ Dependency versions (`@playwright/cli`, `camoufox-js`) are pinned exactly, not range-matched — both are young/prerelease packages and an unpinned `npm install` could silently pick up a breaking protocol change between Playwright's internal Firefox/Juggler driver and Camoufox's patched binary. Upgrades are deliberate, one version bump at a time, tested against the checks in this README before being released.
60
+
61
+ ## Security note
62
+
63
+ The `env` passed to the Camoufox process is an explicit allowlist (`PATH`, temp dirs, etc.) — never your full shell environment. Don't widen it without checking what secrets might be sitting in `process.env`.
package/bin/cli.js ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import {
4
+ DEFAULT_PROFILE,
5
+ ensureProfile,
6
+ profileConfigPath,
7
+ profileExists,
8
+ listProfiles,
9
+ deleteProfileFiles,
10
+ } from "../src/profiles.js";
11
+ import { playwrightCliBin, camoufoxJsBin } from "../src/paths.js";
12
+
13
+ function extractProfileFlag(argv) {
14
+ let profile = DEFAULT_PROFILE;
15
+ const rest = [];
16
+ for (const arg of argv) {
17
+ if (arg.startsWith("--profile=")) {
18
+ profile = arg.slice("--profile=".length);
19
+ } else {
20
+ rest.push(arg);
21
+ }
22
+ }
23
+ return { profile, rest };
24
+ }
25
+
26
+ async function main() {
27
+ const { profile, rest } = extractProfileFlag(process.argv.slice(2));
28
+ const [command, ...args] = rest;
29
+
30
+ switch (command) {
31
+ case "install":
32
+ return runInstall();
33
+ case "profile":
34
+ return runProfile(args);
35
+ case undefined:
36
+ case "-h":
37
+ case "--help":
38
+ return printHelp();
39
+ default:
40
+ return runPlaywrightCli(profile, rest);
41
+ }
42
+ }
43
+
44
+ function runCamoufoxFetch() {
45
+ const result = spawnSync(process.execPath, [camoufoxJsBin(), "fetch"], { stdio: "inherit" });
46
+ if (result.status !== 0) process.exit(result.status ?? 1);
47
+ }
48
+
49
+ // Stops the daemon (if running) and wipes its browser profile dir (cookies,
50
+ // storage). Used before rotate/delete so a new fingerprint never keeps the
51
+ // old identity's login state — a fingerprint change with stale cookies is
52
+ // itself an inconsistency signal.
53
+ function resetSessionData(profileName) {
54
+ spawnSync(process.execPath, [playwrightCliBin(), `-s=${profileName}`, "close"], { stdio: "ignore" });
55
+ spawnSync(process.execPath, [playwrightCliBin(), `-s=${profileName}`, "delete-data"], { stdio: "ignore" });
56
+ }
57
+
58
+ async function runInstall() {
59
+ console.log("Downloading Camoufox browser binary (this only needs to run once)...");
60
+ runCamoufoxFetch();
61
+
62
+ console.log("\nGenerating default profile from your machine's real parameters...");
63
+ const file = await ensureProfile(DEFAULT_PROFILE);
64
+ console.log(`Profile written to ${file}`);
65
+ console.log("\nDone. Try: camoufox-playwright-cli open https://example.com");
66
+ }
67
+
68
+ async function runProfile(args) {
69
+ const [sub, name] = args;
70
+ switch (sub) {
71
+ case "list": {
72
+ const profiles = await listProfiles();
73
+ if (profiles.length === 0) {
74
+ console.log("No profiles yet. Run `camoufox-playwright-cli install` first.");
75
+ return;
76
+ }
77
+ for (const p of profiles) {
78
+ console.log(`${p.name}${p.name === DEFAULT_PROFILE ? " (default)" : ""}`);
79
+ console.log(` created: ${p.createdAt}`);
80
+ console.log(` os: ${p.os} locale: ${p.locale} timezone: ${p.timezone}`);
81
+ console.log(` screen: ${JSON.stringify(p.screen)}`);
82
+ console.log(` gpu: ${JSON.stringify(p.gpu)}`);
83
+ }
84
+ return;
85
+ }
86
+ case "create": {
87
+ if (!name) return console.error("Usage: camoufox-playwright-cli profile create <name>");
88
+ if (profileExists(name)) return console.error(`Profile "${name}" already exists. Use \`profile rotate ${name}\` to regenerate it.`);
89
+ const file = await ensureProfile(name);
90
+ console.log(`Created profile "${name}" -> ${file}`);
91
+ return;
92
+ }
93
+ case "rotate": {
94
+ const target = name || DEFAULT_PROFILE;
95
+ if (!profileExists(target)) return console.error(`Profile "${target}" does not exist. Use \`profile create ${target}\` first.`);
96
+ resetSessionData(target);
97
+ const file = await ensureProfile(target, { rotate: true });
98
+ console.log(`Rotated profile "${target}" (browser data wiped, new fingerprint generated) -> ${file}`);
99
+ return;
100
+ }
101
+ case "delete": {
102
+ if (!name) return console.error("Usage: camoufox-playwright-cli profile delete <name>");
103
+ if (name === DEFAULT_PROFILE) return console.error(`Refusing to delete the "${DEFAULT_PROFILE}" profile. Use \`profile rotate\` instead.`);
104
+ resetSessionData(name);
105
+ const existed = await deleteProfileFiles(name);
106
+ console.log(existed ? `Deleted profile "${name}"` : `Profile "${name}" did not exist`);
107
+ return;
108
+ }
109
+ default:
110
+ console.log(`Usage:
111
+ camoufox-playwright-cli profile list
112
+ camoufox-playwright-cli profile create <name>
113
+ camoufox-playwright-cli profile rotate [name] (defaults to "default")
114
+ camoufox-playwright-cli profile delete <name>`);
115
+ }
116
+ }
117
+
118
+ async function runPlaywrightCli(profile, argv) {
119
+ if (!profileExists(profile)) {
120
+ if (profile === DEFAULT_PROFILE) {
121
+ console.log("No default profile yet - running setup first...");
122
+ runCamoufoxFetch();
123
+ await ensureProfile(DEFAULT_PROFILE);
124
+ } else {
125
+ console.error(`Profile "${profile}" does not exist. Create it with: camoufox-playwright-cli profile create ${profile}`);
126
+ process.exit(1);
127
+ }
128
+ }
129
+
130
+ const configFile = profileConfigPath(profile);
131
+ const finalArgs = [...argv];
132
+
133
+ if (!finalArgs.some((a) => a === "-s" || a.startsWith("-s="))) {
134
+ finalArgs.push(`-s=${profile}`);
135
+ }
136
+ if (finalArgs[0] === "open" && !finalArgs.some((a) => a.startsWith("--config"))) {
137
+ finalArgs.push(`--config=${configFile}`);
138
+ }
139
+
140
+ const result = spawnSync(process.execPath, [playwrightCliBin(), ...finalArgs], { stdio: "inherit" });
141
+ process.exit(result.status ?? 0);
142
+ }
143
+
144
+ function printHelp() {
145
+ console.log(`camoufox-playwright-cli (alias: cpw) - Playwright CLI (@playwright/cli), driven by Camoufox (stealth Firefox)
146
+
147
+ Setup (run once):
148
+ cpw install Download Camoufox + generate the "default" profile
149
+
150
+ Profiles (fingerprint + isolated browser data, one per identity):
151
+ cpw profile list
152
+ cpw profile create <name>
153
+ cpw profile rotate [name] Wipe cookies + regenerate fingerprint (defaults to "default")
154
+ cpw profile delete <name>
155
+
156
+ Everything else is forwarded to the real playwright-cli, pointed at Camoufox.
157
+ Use --profile=<name> to pick which identity to drive (defaults to "default"):
158
+ cpw open <url>
159
+ cpw --profile=work open <url>
160
+ cpw snapshot
161
+ cpw click <ref>
162
+ cpw fill <ref> <text>
163
+ cpw screenshot
164
+ cpw close
165
+
166
+ ("camoufox-playwright-cli" also works everywhere "cpw" does — same binary, two names.)
167
+
168
+ See https://github.com/microsoft/playwright-cli for the full command reference.
169
+ `);
170
+ }
171
+
172
+ main();
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "camoufox-playwright-cli",
3
+ "version": "0.1.0",
4
+ "description": "Playwright CLI (@playwright/cli), driven by Camoufox for stealth browsing. Profile-based: auto-generated default fingerprint on install, create/rotate/delete named profiles.",
5
+ "type": "module",
6
+ "bin": {
7
+ "camoufox-playwright-cli": "bin/cli.js",
8
+ "cpw": "bin/cli.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Shun",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ShunL12324/camoufox-playwright-cli.git"
24
+ },
25
+ "keywords": [
26
+ "camoufox",
27
+ "playwright",
28
+ "stealth",
29
+ "anti-detect",
30
+ "browser-automation",
31
+ "cli",
32
+ "profiles"
33
+ ],
34
+ "dependencies": {
35
+ "@playwright/cli": "0.1.18",
36
+ "better-sqlite3": "13.0.1",
37
+ "camoufox-js": "0.12.0"
38
+ }
39
+ }
package/src/host.js ADDED
@@ -0,0 +1,86 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ // Best-effort detection of real host parameters so the generated fingerprint
4
+ // matches the machine it's actually running on (real GPU vendor, real
5
+ // screen size, real locale/timezone) instead of a fully random combo that
6
+ // can look inconsistent (e.g. a 4K screen paired with a decade-old GPU).
7
+ // Every probe is independently wrapped in try/catch: if a probe fails
8
+ // (unsupported OS, no PowerShell, sandboxed env, ...) that field is simply
9
+ // omitted and camoufox-js falls back to its own randomized default for it.
10
+
11
+ function detectOS() {
12
+ switch (process.platform) {
13
+ case "win32":
14
+ return "windows";
15
+ case "darwin":
16
+ return "macos";
17
+ case "linux":
18
+ return "linux";
19
+ default:
20
+ return undefined;
21
+ }
22
+ }
23
+
24
+ function detectLocaleAndTimezone() {
25
+ try {
26
+ const opts = Intl.DateTimeFormat().resolvedOptions();
27
+ return { locale: opts.locale, timezone: opts.timeZone };
28
+ } catch {
29
+ return {};
30
+ }
31
+ }
32
+
33
+ function detectScreen() {
34
+ if (process.platform !== "win32") return undefined;
35
+ try {
36
+ const out = execFileSync(
37
+ "powershell",
38
+ [
39
+ "-NoProfile",
40
+ "-Command",
41
+ "Get-CimInstance Win32_VideoController | Select-Object -First 1 CurrentHorizontalResolution,CurrentVerticalResolution | ConvertTo-Json",
42
+ ],
43
+ { encoding: "utf-8", timeout: 10_000 },
44
+ );
45
+ const data = JSON.parse(out);
46
+ const width = data.CurrentHorizontalResolution;
47
+ const height = data.CurrentVerticalResolution;
48
+ if (!width || !height) return undefined;
49
+ return { width, height };
50
+ } catch {
51
+ return undefined;
52
+ }
53
+ }
54
+
55
+ // Maps a real GPU name string to one of the vendor buckets Camoufox ships
56
+ // curated (vendor, renderer) WebGL fingerprints for. Camoufox's database
57
+ // only covers a fixed, vetted set of older GPU models per vendor (see
58
+ // README for why) - we can match the real vendor, not the exact model.
59
+ function detectGpuVendor() {
60
+ if (process.platform !== "win32") return undefined;
61
+ try {
62
+ const name = execFileSync(
63
+ "powershell",
64
+ ["-NoProfile", "-Command", "(Get-CimInstance Win32_VideoController | Select-Object -First 1).Name"],
65
+ { encoding: "utf-8", timeout: 10_000 },
66
+ ).trim();
67
+ if (!name) return undefined;
68
+ if (/nvidia|geforce|quadro|rtx|gtx/i.test(name)) return { raw: name, vendor: "nvidia" };
69
+ if (/amd|radeon/i.test(name)) return { raw: name, vendor: "amd" };
70
+ if (/intel/i.test(name)) return { raw: name, vendor: "intel" };
71
+ return { raw: name, vendor: undefined };
72
+ } catch {
73
+ return undefined;
74
+ }
75
+ }
76
+
77
+ export function detectHost() {
78
+ const { locale, timezone } = detectLocaleAndTimezone();
79
+ return {
80
+ os: detectOS(),
81
+ locale,
82
+ timezone,
83
+ screen: detectScreen(),
84
+ gpu: detectGpuVendor(),
85
+ };
86
+ }
package/src/paths.js ADDED
@@ -0,0 +1,19 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+
4
+ const require = createRequire(import.meta.url);
5
+
6
+ export function playwrightCliBin() {
7
+ const pkgJsonPath = require.resolve("@playwright/cli/package.json");
8
+ const pkgDir = path.dirname(pkgJsonPath);
9
+ const pkg = require(pkgJsonPath);
10
+ return path.join(pkgDir, pkg.bin["playwright-cli"]);
11
+ }
12
+
13
+ export function camoufoxJsBin() {
14
+ const pkgJsonPath = require.resolve("camoufox-js/package.json");
15
+ const pkgDir = path.dirname(pkgJsonPath);
16
+ const pkg = require(pkgJsonPath);
17
+ const bin = typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin)[0];
18
+ return path.join(pkgDir, bin);
19
+ }
@@ -0,0 +1,145 @@
1
+ import { launchOptions } from "camoufox-js";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, writeFile, readFile, rm, readdir } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { detectHost } from "./host.js";
7
+ import { pickVendorMatchedWebgl } from "./webgl.js";
8
+
9
+ export const DEFAULT_PROFILE = "default";
10
+
11
+ // Only pass through what the Camoufox binary actually needs to start.
12
+ // launchOptions() defaults env to process.env, which would leak every
13
+ // secret in the caller's shell into the CAMOU_CONFIG env blob and into
14
+ // the config.json file on disk. Never widen this without a reason.
15
+ const SAFE_ENV_KEYS = [
16
+ "PATH",
17
+ "SYSTEMROOT",
18
+ "WINDIR",
19
+ "TEMP",
20
+ "TMP",
21
+ "USERPROFILE",
22
+ "HOME",
23
+ "DISPLAY", // Linux/X11 headed mode
24
+ ];
25
+
26
+ function safeEnv() {
27
+ const env = {};
28
+ for (const key of SAFE_ENV_KEYS) {
29
+ if (process.env[key] !== undefined) env[key] = process.env[key];
30
+ }
31
+ return env;
32
+ }
33
+
34
+ const ROOT_DIR = path.join(os.homedir(), ".camoufox-playwright-cli");
35
+ const PROFILES_DIR = path.join(ROOT_DIR, "profiles");
36
+
37
+ function isValidName(name) {
38
+ return /^[a-zA-Z0-9_-]+$/.test(name);
39
+ }
40
+
41
+ export function profileDir(name) {
42
+ if (!isValidName(name)) {
43
+ throw new Error(`Invalid profile name "${name}" (use letters, numbers, - and _ only)`);
44
+ }
45
+ return path.join(PROFILES_DIR, name);
46
+ }
47
+
48
+ export function profileConfigPath(name) {
49
+ return path.join(profileDir(name), "cli.config.json");
50
+ }
51
+
52
+ function profileMetaPath(name) {
53
+ return path.join(profileDir(name), "meta.json");
54
+ }
55
+
56
+ export function profileExists(name) {
57
+ return existsSync(profileConfigPath(name));
58
+ }
59
+
60
+ export async function listProfiles() {
61
+ if (!existsSync(PROFILES_DIR)) return [];
62
+ const names = await readdir(PROFILES_DIR);
63
+ const profiles = [];
64
+ for (const name of names) {
65
+ if (!profileExists(name)) continue;
66
+ const meta = JSON.parse(await readFile(profileMetaPath(name), "utf-8").catch(() => "{}"));
67
+ profiles.push({ name, ...meta });
68
+ }
69
+ return profiles;
70
+ }
71
+
72
+ export async function readProfileConfig(name) {
73
+ if (!profileExists(name)) return null;
74
+ return JSON.parse(await readFile(profileConfigPath(name), "utf-8"));
75
+ }
76
+
77
+ // Creates a profile if it doesn't exist, or regenerates it in place when
78
+ // `rotate` is true. The generated fingerprint prefers real host parameters
79
+ // (GPU vendor, screen size, locale/timezone) over random ones — see
80
+ // host.js and webgl.js for what's detected and why exact GPU model spoofing
81
+ // isn't possible. Any detection failure just falls back to camoufox-js's
82
+ // own randomized default for that field; it never throws.
83
+ export async function ensureProfile(name, { rotate = false } = {}) {
84
+ if (!rotate && profileExists(name)) {
85
+ return profileConfigPath(name);
86
+ }
87
+
88
+ await mkdir(profileDir(name), { recursive: true });
89
+
90
+ const host = detectHost();
91
+ const targetOs = host.os || "windows";
92
+ const webglConfig = host.gpu?.vendor
93
+ ? pickVendorMatchedWebgl(targetOs, host.gpu.vendor)
94
+ : undefined;
95
+
96
+ const opts = await launchOptions({
97
+ os: targetOs,
98
+ humanize: true,
99
+ locale: host.locale ? [host.locale] : undefined,
100
+ screen: host.screen
101
+ ? {
102
+ minWidth: host.screen.width,
103
+ maxWidth: host.screen.width,
104
+ minHeight: host.screen.height,
105
+ maxHeight: host.screen.height,
106
+ }
107
+ : undefined,
108
+ webgl_config: webglConfig,
109
+ env: safeEnv(),
110
+ });
111
+
112
+ const config = {
113
+ browser: {
114
+ browserName: "firefox",
115
+ launchOptions: {
116
+ executablePath: opts.executablePath,
117
+ args: opts.args,
118
+ env: opts.env,
119
+ firefoxUserPrefs: opts.firefoxUserPrefs,
120
+ headless: opts.headless,
121
+ },
122
+ },
123
+ };
124
+
125
+ const meta = {
126
+ createdAt: new Date().toISOString(),
127
+ os: targetOs,
128
+ locale: host.locale ?? "randomized (host detection failed)",
129
+ timezone: host.timezone ?? "randomized (host detection failed)",
130
+ screen: host.screen ?? "randomized (host detection failed)",
131
+ gpu: host.gpu
132
+ ? { real: host.gpu.raw, spoofedAs: webglConfig ? webglConfig[1] : "no vendor match in Camoufox DB, randomized" }
133
+ : "randomized (host detection failed or non-Windows)",
134
+ };
135
+
136
+ await writeFile(profileConfigPath(name), JSON.stringify(config, null, 2));
137
+ await writeFile(profileMetaPath(name), JSON.stringify(meta, null, 2));
138
+ return profileConfigPath(name);
139
+ }
140
+
141
+ export async function deleteProfileFiles(name) {
142
+ if (!existsSync(profileDir(name))) return false;
143
+ await rm(profileDir(name), { recursive: true, force: true });
144
+ return true;
145
+ }
package/src/webgl.js ADDED
@@ -0,0 +1,49 @@
1
+ import Database from "better-sqlite3";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+
5
+ const require = createRequire(import.meta.url);
6
+
7
+ // Camoufox ships a curated SQLite DB of (vendor, renderer) WebGL pairs that
8
+ // have been vetted to render a consistent, believable fingerprint. It does
9
+ // NOT cover every real GPU (no RTX 20/30/40-series entries, for example) -
10
+ // only a fixed set of older reference models per vendor bucket. We can't
11
+ // spoof the exact real GPU model through this API, but we CAN pick a combo
12
+ // whose *vendor* matches the real GPU, which is the part fingerprinting
13
+ // scripts actually cross-check against other signals (CPU core count,
14
+ // platform string, etc). Exact-model spoofing would require building and
15
+ // maintaining our own WebGL fingerprint database - out of scope here.
16
+
17
+ const VENDOR_BUCKET_TO_CAMOUFOX = {
18
+ nvidia: "Google Inc. (NVIDIA)",
19
+ amd: "Google Inc. (AMD)",
20
+ intel: "Google Inc. (Intel)",
21
+ };
22
+
23
+ const OS_COLUMN = { windows: "win", macos: "mac", linux: "lin" };
24
+
25
+ function dbPath() {
26
+ const camoufoxJsPkg = require.resolve("camoufox-js/package.json");
27
+ return path.join(path.dirname(camoufoxJsPkg), "dist", "data-files", "webgl_data.db");
28
+ }
29
+
30
+ // Returns a [vendor, renderer] tuple for camoufox-js's `webgl_config` option,
31
+ // or undefined if the real vendor couldn't be detected / has no bucket.
32
+ export function pickVendorMatchedWebgl(os, gpuVendorBucket) {
33
+ const camoufoxVendor = VENDOR_BUCKET_TO_CAMOUFOX[gpuVendorBucket];
34
+ const osColumn = OS_COLUMN[os];
35
+ if (!camoufoxVendor || !osColumn) return undefined;
36
+
37
+ const db = new Database(dbPath(), { readonly: true });
38
+ try {
39
+ const rows = db
40
+ .prepare(
41
+ `SELECT vendor, renderer FROM webgl_fingerprints WHERE vendor = ? AND ${osColumn} > 0`,
42
+ )
43
+ .all(camoufoxVendor);
44
+ if (rows.length === 0) return undefined;
45
+ return [rows[0].vendor, rows[0].renderer];
46
+ } finally {
47
+ db.close();
48
+ }
49
+ }