chromiumfish 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,30 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arman Hossain
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.
22
+
23
+ ---
24
+
25
+ This package distributes builds of the Chromium project, which is licensed
26
+ under the BSD 3-Clause License and includes third-party components under their
27
+ respective licenses. The full Chromium license text and credits are bundled
28
+ with each browser release. "Chromium" and "Google Chrome" are trademarks of
29
+ Google LLC. ChromiumFish is an independent fork and is not affiliated with,
30
+ sponsored by, or endorsed by Google LLC.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # chromiumfish (Node)
2
+
3
+ Stealth Chromium with a drop-in [Playwright](https://playwright.dev) harness.
4
+
5
+ ```bash
6
+ npm install chromiumfish playwright-core
7
+ npx chromiumfish fetch # download + cache the browser build
8
+ ```
9
+
10
+ ## Usage
11
+
12
+ ```javascript
13
+ import { ChromiumFish } from "chromiumfish";
14
+
15
+ const browser = await ChromiumFish({ personaSeed: 27182, headless: true });
16
+ const page = await browser.newPage();
17
+ await page.goto("https://abrahamjuliot.github.io/creepjs/");
18
+ await page.screenshot({ path: "fp.png" });
19
+ await browser.close();
20
+ ```
21
+
22
+ `ChromiumFish()` returns a standard Playwright `Browser`, so `newContext`,
23
+ `newPage`, routing, tracing, etc. all work as usual.
24
+
25
+ ## Options
26
+
27
+ | Option | Default | Description |
28
+ |--------|---------|-------------|
29
+ | `personaSeed` | — | Integer seed for a stable, internally-consistent fingerprint persona. |
30
+ | `headless` | `true` | Run headless (SwiftShader). |
31
+ | `proxy` | — | Playwright proxy object, e.g. `{ server, username, password }`. |
32
+ | `windowSize` | `[1920, 1080]` | Window dimensions (`null` to omit). |
33
+ | `version` | pinned | Override the browser build version. |
34
+ | `download` | `true` | Auto-download the build if missing. |
35
+ | `args` | — | Extra Chromium flags. |
36
+ | _...rest_ | — | Forwarded to `chromium.launch()`. |
37
+
38
+ ## CLI
39
+
40
+ ```bash
41
+ npx chromiumfish fetch [--browser-version X] [--force] # download + cache
42
+ npx chromiumfish path # print binary path
43
+ npx chromiumfish clear # wipe the cache
44
+ npx chromiumfish --version
45
+ ```
46
+
47
+ Builds are cached under `~/.cache/chromiumfish/<version>/` (override with
48
+ `CHROMIUMFISH_CACHE_DIR`). Pin a build with `CHROMIUMFISH_VERSION`.
49
+
50
+ ## License
51
+
52
+ MIT © Arman Hossain. See the [repository](https://github.com/arman-bd/chromiumfish).
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from "node:fs";
3
+ import { binaryPath, cacheRoot, fetchBrowser } from "./fetch.js";
4
+ import { SDK_VERSION, browserVersion } from "./version.js";
5
+ async function main(argv) {
6
+ const cmd = argv[2];
7
+ switch (cmd) {
8
+ case "fetch": {
9
+ const force = argv.includes("--force");
10
+ const vIdx = argv.indexOf("--browser-version");
11
+ const version = vIdx >= 0 ? argv[vIdx + 1] : undefined;
12
+ console.log(await fetchBrowser(version, force));
13
+ return 0;
14
+ }
15
+ case "path":
16
+ console.log(await binaryPath());
17
+ return 0;
18
+ case "clear": {
19
+ const root = cacheRoot();
20
+ if (fs.existsSync(root)) {
21
+ fs.rmSync(root, { recursive: true, force: true });
22
+ console.log(`removed ${root}`);
23
+ }
24
+ else {
25
+ console.log("nothing to remove");
26
+ }
27
+ return 0;
28
+ }
29
+ case "--version":
30
+ case "-V":
31
+ console.log(`chromiumfish ${SDK_VERSION} (browser ${browserVersion()})`);
32
+ return 0;
33
+ default:
34
+ console.log([
35
+ "chromiumfish — fetch and manage the ChromiumFish browser build",
36
+ "",
37
+ "Usage:",
38
+ " chromiumfish fetch [--browser-version X] [--force] download + cache",
39
+ " chromiumfish path print binary path",
40
+ " chromiumfish clear wipe the cache",
41
+ " chromiumfish --version",
42
+ ].join("\n"));
43
+ return cmd ? 0 : 1;
44
+ }
45
+ }
46
+ main(process.argv)
47
+ .then((code) => process.exit(code))
48
+ .catch((err) => {
49
+ console.error(err?.message || err);
50
+ process.exit(1);
51
+ });
@@ -0,0 +1,6 @@
1
+ export declare function cacheRoot(): string;
2
+ export declare function platformSlug(): string;
3
+ export declare function installDir(version?: string): string;
4
+ export declare function findBinary(root: string): string | null;
5
+ export declare function fetchBrowser(version?: string, force?: boolean): Promise<string>;
6
+ export declare function binaryPath(version?: string, download?: boolean): Promise<string>;
package/dist/fetch.js ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Download, verify, and cache the ChromiumFish browser build.
3
+ *
4
+ * Resolves `version × platform` to a GitHub Release asset, verifies its
5
+ * SHA-256, extracts it to a per-version cache dir, and returns the path to the
6
+ * launchable binary.
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import { spawnSync } from "node:child_process";
10
+ import * as fs from "node:fs";
11
+ import * as https from "node:https";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+ import { browserVersion, releaseBaseUrl } from "./version.js";
15
+ export function cacheRoot() {
16
+ const env = process.env.CHROMIUMFISH_CACHE_DIR;
17
+ if (env)
18
+ return env;
19
+ if (process.platform === "darwin")
20
+ return path.join(os.homedir(), "Library", "Caches", "chromiumfish");
21
+ if (process.platform === "win32")
22
+ return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "chromiumfish");
23
+ return path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"), "chromiumfish");
24
+ }
25
+ export function platformSlug() {
26
+ const arch = { x64: "x64", arm64: "arm64" }[process.arch];
27
+ if (!arch)
28
+ throw new Error(`unsupported architecture: ${process.arch}`);
29
+ if (process.platform === "linux")
30
+ return `linux-${arch}`;
31
+ if (process.platform === "darwin")
32
+ return `mac-${arch}`;
33
+ if (process.platform === "win32")
34
+ return `win-${arch}`;
35
+ throw new Error(`unsupported platform: ${process.platform}`);
36
+ }
37
+ function assetName(version) {
38
+ const slug = platformSlug();
39
+ const ext = slug.startsWith("win") ? "zip" : "tar.gz";
40
+ return `chromiumfish-${version}-${slug}.${ext}`;
41
+ }
42
+ export function installDir(version = browserVersion()) {
43
+ return path.join(cacheRoot(), version, platformSlug());
44
+ }
45
+ const BINARY_NAMES = ["chromiumfish", "chrome", "chromiumfish.exe", "chrome.exe", "ChromiumFish"];
46
+ export function findBinary(root) {
47
+ if (!fs.existsSync(root))
48
+ return null;
49
+ for (const name of BINARY_NAMES) {
50
+ const direct = path.join(root, name);
51
+ if (fs.existsSync(direct) && fs.statSync(direct).isFile())
52
+ return direct;
53
+ }
54
+ const stack = [root];
55
+ while (stack.length) {
56
+ const dir = stack.pop();
57
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
58
+ const full = path.join(dir, entry.name);
59
+ if (entry.isDirectory())
60
+ stack.push(full);
61
+ else if (BINARY_NAMES.includes(entry.name))
62
+ return full;
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ function download(url, dest) {
68
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
69
+ return new Promise((resolve, reject) => {
70
+ const get = (u) => {
71
+ https.get(u, (res) => {
72
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
73
+ res.resume();
74
+ return get(res.headers.location);
75
+ }
76
+ if (res.statusCode !== 200) {
77
+ res.resume();
78
+ return reject(new Error(`download failed (${res.statusCode}) for ${u}`));
79
+ }
80
+ const total = Number(res.headers["content-length"] || 0);
81
+ let read = 0;
82
+ const out = fs.createWriteStream(dest);
83
+ res.on("data", (c) => {
84
+ read += c.length;
85
+ if (total)
86
+ process.stderr.write(`\r[chromiumfish] ${Math.floor((read / total) * 100)}%`);
87
+ });
88
+ res.pipe(out);
89
+ out.on("finish", () => { process.stderr.write("\n"); out.close(() => resolve()); });
90
+ out.on("error", reject);
91
+ }).on("error", reject);
92
+ };
93
+ process.stderr.write(`[chromiumfish] downloading ${url}\n`);
94
+ get(url);
95
+ });
96
+ }
97
+ function fetchText(url) {
98
+ return new Promise((resolve, reject) => {
99
+ const get = (u) => https.get(u, (res) => {
100
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
101
+ res.resume();
102
+ return get(res.headers.location);
103
+ }
104
+ if (res.statusCode !== 200) {
105
+ res.resume();
106
+ return reject(new Error(`HTTP ${res.statusCode}`));
107
+ }
108
+ let data = "";
109
+ res.setEncoding("utf8");
110
+ res.on("data", (c) => (data += c));
111
+ res.on("end", () => resolve(data));
112
+ }).on("error", reject);
113
+ get(url);
114
+ });
115
+ }
116
+ function sha256(file) {
117
+ return createHash("sha256").update(fs.readFileSync(file)).digest("hex");
118
+ }
119
+ function extract(archive, dest) {
120
+ fs.mkdirSync(dest, { recursive: true });
121
+ // Modern tar (incl. Windows 10+ bsdtar) extracts both .tar.gz and .zip.
122
+ const r = spawnSync("tar", ["-xf", archive, "-C", dest], { stdio: "inherit" });
123
+ if (r.status !== 0)
124
+ throw new Error(`extraction failed for ${archive}`);
125
+ }
126
+ export async function fetchBrowser(version = browserVersion(), force = false) {
127
+ const target = installDir(version);
128
+ if (force && fs.existsSync(target))
129
+ fs.rmSync(target, { recursive: true, force: true });
130
+ const cached = findBinary(target);
131
+ if (cached)
132
+ return cached;
133
+ const base = releaseBaseUrl(version);
134
+ const asset = assetName(version);
135
+ const archive = path.join(cacheRoot(), version, asset);
136
+ await download(`${base}/${asset}`, archive);
137
+ try {
138
+ const expected = (await fetchText(`${base}/${asset}.sha256`)).trim().split(/\s+/)[0];
139
+ const actual = sha256(archive);
140
+ if (actual !== expected) {
141
+ fs.rmSync(archive, { force: true });
142
+ throw new Error(`checksum mismatch for ${asset}: ${actual} !== ${expected}`);
143
+ }
144
+ }
145
+ catch (e) {
146
+ if (String(e?.message || e).includes("HTTP"))
147
+ process.stderr.write("[chromiumfish] warning: no .sha256 published, skipping verification\n");
148
+ else
149
+ throw e;
150
+ }
151
+ extract(archive, target);
152
+ fs.rmSync(archive, { force: true });
153
+ macosPrepare(target);
154
+ const bin = findBinary(target);
155
+ if (!bin)
156
+ throw new Error(`no browser binary found in extracted build at ${target}`);
157
+ if (process.platform !== "win32")
158
+ fs.chmodSync(bin, 0o755);
159
+ process.stderr.write(`[chromiumfish] ready: ${bin}\n`);
160
+ return bin;
161
+ }
162
+ /**
163
+ * Identity-clean macOS prep: strip the download
164
+ * quarantine flag, and ensure an ad-hoc signature so Apple Silicon will run the
165
+ * binary — no certificate, name, or identity attached. Release builds ship
166
+ * ad-hoc signed; this is a defensive fallback.
167
+ */
168
+ function macosPrepare(target) {
169
+ if (process.platform !== "darwin")
170
+ return;
171
+ spawnSync("xattr", ["-dr", "com.apple.quarantine", target], { stdio: "ignore" });
172
+ const app = fs.readdirSync(target).find((n) => n.endsWith(".app"));
173
+ const signTarget = app ? path.join(target, app) : findBinary(target);
174
+ if (signTarget) {
175
+ const valid = spawnSync("codesign", ["--verify", "--quiet", signTarget]).status === 0;
176
+ if (!valid)
177
+ spawnSync("codesign", ["--force", "--deep", "--sign", "-", signTarget], { stdio: "ignore" });
178
+ }
179
+ }
180
+ export async function binaryPath(version = browserVersion(), download = true) {
181
+ const existing = findBinary(installDir(version));
182
+ if (existing)
183
+ return existing;
184
+ if (!download)
185
+ throw new Error(`ChromiumFish ${version} not installed. Run \`npx chromiumfish fetch\`.`);
186
+ return fetchBrowser(version);
187
+ }
@@ -0,0 +1,4 @@
1
+ export { ChromiumFish, buildArgs, BASE_ARGS } from "./launcher.js";
2
+ export type { ChromiumFishOptions } from "./launcher.js";
3
+ export { fetchBrowser, binaryPath, installDir, cacheRoot, platformSlug, findBinary } from "./fetch.js";
4
+ export { SDK_VERSION, DEFAULT_BROWSER_VERSION, RELEASE_REPO, browserVersion, releaseBaseUrl, } from "./version.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { ChromiumFish, buildArgs, BASE_ARGS } from "./launcher.js";
2
+ export { fetchBrowser, binaryPath, installDir, cacheRoot, platformSlug, findBinary } from "./fetch.js";
3
+ export { SDK_VERSION, DEFAULT_BROWSER_VERSION, RELEASE_REPO, browserVersion, releaseBaseUrl, } from "./version.js";
@@ -0,0 +1,27 @@
1
+ import { type Browser, type LaunchOptions } from "playwright-core";
2
+ /**
3
+ * Flags that keep the GPU-less / SwiftShader path working and the persona
4
+ * engine happy. Mirrors the production launch_lean.sh defaults (minus anything
5
+ * baked into the build / bundled addon).
6
+ */
7
+ export declare const BASE_ARGS: string[];
8
+ export interface ChromiumFishOptions extends Omit<LaunchOptions, "executablePath"> {
9
+ /** Integer seed for a stable, internally-consistent fingerprint persona. */
10
+ personaSeed?: number;
11
+ /** Run headless (SwiftShader). Defaults to true. */
12
+ headless?: boolean;
13
+ /** Window dimensions; defaults to [1920, 1080]. Pass null to omit. */
14
+ windowSize?: [number, number] | null;
15
+ /** Override the browser build version. */
16
+ version?: string;
17
+ /** Auto-download the build if missing. Defaults to true. */
18
+ download?: boolean;
19
+ }
20
+ export declare function buildArgs(opts: ChromiumFishOptions): string[];
21
+ /**
22
+ * Launch ChromiumFish and return a standard Playwright `Browser`.
23
+ *
24
+ * import { ChromiumFish } from "chromiumfish";
25
+ * const browser = await ChromiumFish({ personaSeed: 27182, headless: true });
26
+ */
27
+ export declare function ChromiumFish(opts?: ChromiumFishOptions): Promise<Browser>;
@@ -0,0 +1,42 @@
1
+ import { chromium } from "playwright-core";
2
+ import { binaryPath } from "./fetch.js";
3
+ /**
4
+ * Flags that keep the GPU-less / SwiftShader path working and the persona
5
+ * engine happy. Mirrors the production launch_lean.sh defaults (minus anything
6
+ * baked into the build / bundled addon).
7
+ */
8
+ export const BASE_ARGS = [
9
+ "--no-sandbox",
10
+ "--no-zygote",
11
+ "--disable-dev-shm-usage",
12
+ "--use-gl=angle",
13
+ "--use-angle=swiftshader",
14
+ "--enable-unsafe-swiftshader",
15
+ ];
16
+ export function buildArgs(opts) {
17
+ const args = [...BASE_ARGS];
18
+ if (opts.personaSeed !== undefined)
19
+ args.push(`--persona-seed=${opts.personaSeed}`);
20
+ const ws = opts.windowSize === undefined ? [1920, 1080] : opts.windowSize;
21
+ if (ws)
22
+ args.push(`--window-size=${ws[0]},${ws[1]}`);
23
+ if (opts.args)
24
+ args.push(...opts.args);
25
+ return args;
26
+ }
27
+ /**
28
+ * Launch ChromiumFish and return a standard Playwright `Browser`.
29
+ *
30
+ * import { ChromiumFish } from "chromiumfish";
31
+ * const browser = await ChromiumFish({ personaSeed: 27182, headless: true });
32
+ */
33
+ export async function ChromiumFish(opts = {}) {
34
+ const { personaSeed, headless = true, windowSize, version, download = true, args, ...launch } = opts;
35
+ const executablePath = await binaryPath(version, download);
36
+ return chromium.launch({
37
+ executablePath,
38
+ headless,
39
+ args: buildArgs({ personaSeed, windowSize, args }),
40
+ ...launch,
41
+ });
42
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Pinned browser build + release coordinates.
3
+ *
4
+ * The browser is built privately and published to this repo's GitHub Releases.
5
+ * `DEFAULT_BROWSER_VERSION` is the release tag (without the leading `v`) the
6
+ * SDK downloads by default; override it with `CHROMIUMFISH_VERSION`.
7
+ */
8
+ /** SDK package version (kept in sync with package.json). */
9
+ export declare const SDK_VERSION = "0.1.0";
10
+ /** Default ChromiumFish browser build to fetch. Matches src/chrome/VERSION. */
11
+ export declare const DEFAULT_BROWSER_VERSION = "150.0.7844";
12
+ /** Public repo hosting the release assets. */
13
+ export declare const RELEASE_REPO = "arman-bd/chromiumfish";
14
+ export declare function browserVersion(): string;
15
+ export declare function releaseBaseUrl(version?: string): string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pinned browser build + release coordinates.
3
+ *
4
+ * The browser is built privately and published to this repo's GitHub Releases.
5
+ * `DEFAULT_BROWSER_VERSION` is the release tag (without the leading `v`) the
6
+ * SDK downloads by default; override it with `CHROMIUMFISH_VERSION`.
7
+ */
8
+ /** SDK package version (kept in sync with package.json). */
9
+ export const SDK_VERSION = "0.1.0";
10
+ /** Default ChromiumFish browser build to fetch. Matches src/chrome/VERSION. */
11
+ export const DEFAULT_BROWSER_VERSION = "150.0.7844";
12
+ /** Public repo hosting the release assets. */
13
+ export const RELEASE_REPO = "arman-bd/chromiumfish";
14
+ export function browserVersion() {
15
+ return process.env.CHROMIUMFISH_VERSION || DEFAULT_BROWSER_VERSION;
16
+ }
17
+ export function releaseBaseUrl(version = browserVersion()) {
18
+ return `https://github.com/${RELEASE_REPO}/releases/download/v${version}`;
19
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "chromiumfish",
3
+ "version": "0.1.0",
4
+ "description": "Stealth Chromium build with a drop-in Playwright harness — fetches and launches the ChromiumFish browser.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "chromiumfish": "./dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "playwright",
28
+ "chromium",
29
+ "stealth",
30
+ "fingerprint",
31
+ "automation",
32
+ "anti-detect"
33
+ ],
34
+ "author": "Arman Hossain",
35
+ "license": "MIT",
36
+ "homepage": "https://chromiumfish.com",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/arman-bd/chromiumfish.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/arman-bd/chromiumfish/issues"
43
+ },
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "peerDependencies": {
48
+ "playwright-core": ">=1.40"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^20.0.0",
52
+ "typescript": "^5.4.0"
53
+ }
54
+ }