headfox-js 0.1.1

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 (42) hide show
  1. package/LICENSE.md +373 -0
  2. package/README.md +121 -0
  3. package/bin/headfox-js.mjs +17 -0
  4. package/dist/__main__.d.ts +2 -0
  5. package/dist/__main__.js +131 -0
  6. package/dist/__version__.d.ts +8 -0
  7. package/dist/__version__.js +10 -0
  8. package/dist/addons.d.ts +17 -0
  9. package/dist/addons.js +74 -0
  10. package/dist/data-files/territoryInfo.xml +2024 -0
  11. package/dist/data-files/webgl_data.db +0 -0
  12. package/dist/exceptions.d.ts +82 -0
  13. package/dist/exceptions.js +165 -0
  14. package/dist/fingerprints.d.ts +4 -0
  15. package/dist/fingerprints.js +82 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.js +4 -0
  18. package/dist/ip.d.ts +25 -0
  19. package/dist/ip.js +90 -0
  20. package/dist/locale.d.ts +26 -0
  21. package/dist/locale.js +285 -0
  22. package/dist/mappings/browserforge.config.d.ts +47 -0
  23. package/dist/mappings/browserforge.config.js +72 -0
  24. package/dist/mappings/fonts.config.d.ts +6 -0
  25. package/dist/mappings/fonts.config.js +822 -0
  26. package/dist/mappings/warnings.config.d.ts +16 -0
  27. package/dist/mappings/warnings.config.js +27 -0
  28. package/dist/pkgman.d.ts +67 -0
  29. package/dist/pkgman.js +421 -0
  30. package/dist/server.d.ts +7 -0
  31. package/dist/server.js +24 -0
  32. package/dist/sync_api.d.ts +10 -0
  33. package/dist/sync_api.js +35 -0
  34. package/dist/utils.d.ts +109 -0
  35. package/dist/utils.js +540 -0
  36. package/dist/virtdisplay.d.ts +20 -0
  37. package/dist/virtdisplay.js +123 -0
  38. package/dist/warnings.d.ts +4 -0
  39. package/dist/warnings.js +33 -0
  40. package/dist/webgl/sample.d.ts +19 -0
  41. package/dist/webgl/sample.js +121 -0
  42. package/package.json +94 -0
@@ -0,0 +1,123 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { randomInt } from "node:crypto";
3
+ import { existsSync, statSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { globSync } from "glob";
7
+ import { CannotExecuteXvfb, CannotFindXvfb, VirtualDisplayNotSupported, } from "./exceptions.js";
8
+ import { OS_NAME } from "./pkgman.js";
9
+ export class VirtualDisplay {
10
+ debug;
11
+ proc = null;
12
+ _display = null;
13
+ // private _lock = new Lock();
14
+ constructor(debug = false) {
15
+ this.debug = debug;
16
+ }
17
+ get xvfb_args() {
18
+ return [
19
+ "-screen",
20
+ "0",
21
+ "1x1x24",
22
+ "-ac",
23
+ "-nolisten",
24
+ "tcp",
25
+ "-extension",
26
+ "RENDER",
27
+ "+extension",
28
+ "GLX",
29
+ "-extension",
30
+ "COMPOSITE",
31
+ "-extension",
32
+ "XVideo",
33
+ "-extension",
34
+ "XVideo-MotionCompensation",
35
+ "-extension",
36
+ "XINERAMA",
37
+ "-shmem",
38
+ "-fp",
39
+ "built-ins",
40
+ "-nocursor",
41
+ "-br",
42
+ ];
43
+ }
44
+ get xvfb_path() {
45
+ const path = execFileSync("which", ["Xvfb"]).toString().trim();
46
+ if (!path) {
47
+ throw new CannotFindXvfb("Please install Xvfb to use headless mode.");
48
+ }
49
+ if (!existsSync(path) || !execFileSync("test", ["-x", path])) {
50
+ throw new CannotExecuteXvfb(`I do not have permission to execute Xvfb: ${path}`);
51
+ }
52
+ return path;
53
+ }
54
+ get xvfb_cmd() {
55
+ return [this.xvfb_path, `:${this.display}`, ...this.xvfb_args];
56
+ }
57
+ execute_xvfb() {
58
+ if (this.debug) {
59
+ console.log("Starting virtual display:", this.xvfb_cmd.join(" "));
60
+ }
61
+ this.proc = spawn(this.xvfb_cmd[0], this.xvfb_cmd.slice(1), {
62
+ stdio: this.debug ? "inherit" : "ignore",
63
+ detached: true,
64
+ });
65
+ }
66
+ get() {
67
+ VirtualDisplay.assert_linux();
68
+ // this._lock.runExclusive(() => {
69
+ if (!this.proc) {
70
+ this.execute_xvfb();
71
+ }
72
+ else if (this.debug) {
73
+ console.log(`Using virtual display: ${this.display}`);
74
+ }
75
+ // });
76
+ return `:${this.display}`;
77
+ }
78
+ kill() {
79
+ // this._lock.runExclusive(() => {
80
+ if (this.proc && !this.proc.killed) {
81
+ if (this.debug) {
82
+ console.log("Terminating virtual display:", this.display);
83
+ }
84
+ this.proc.kill();
85
+ }
86
+ // });
87
+ }
88
+ /**
89
+ * Get list of lock files in /tmp
90
+ * @returns List of lock file paths
91
+ */
92
+ static _get_lock_files() {
93
+ const tmpd = process.env.TMPDIR || tmpdir();
94
+ try {
95
+ return globSync(path.join(tmpd, ".X*-lock")).filter((p) => {
96
+ try {
97
+ return statSync(p).isFile();
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ });
103
+ }
104
+ catch {
105
+ return [];
106
+ }
107
+ }
108
+ static _free_display() {
109
+ const ls = VirtualDisplay._get_lock_files().map((x) => parseInt(x.split("X")[1].split("-")[0], 10));
110
+ return ls.length ? Math.max(99, Math.max(...ls) + randomInt(3, 20)) : 99;
111
+ }
112
+ get display() {
113
+ if (this._display === null) {
114
+ this._display = VirtualDisplay._free_display();
115
+ }
116
+ return this._display;
117
+ }
118
+ static assert_linux() {
119
+ if (OS_NAME !== "lin") {
120
+ throw new VirtualDisplayNotSupported("Virtual display is only supported on Linux.");
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,4 @@
1
+ export declare class LeakWarning extends Error {
2
+ constructor(message: string);
3
+ static warn(warningKey: string, iKnowWhatImDoing?: boolean): void;
4
+ }
@@ -0,0 +1,33 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import WARNINGS_DATA from "./mappings/warnings.config.js";
4
+ const currentDir = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
5
+ export class LeakWarning extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "LeakWarning";
9
+ }
10
+ static warn(warningKey, iKnowWhatImDoing) {
11
+ let warning = WARNINGS_DATA[warningKey];
12
+ if (iKnowWhatImDoing) {
13
+ return;
14
+ }
15
+ if (iKnowWhatImDoing !== undefined) {
16
+ warning += "\nIf this is intentional, pass `iKnowWhatImDoing=true`.";
17
+ }
18
+ const currentModule = currentDir;
19
+ const originalStackTrace = Error.prepareStackTrace;
20
+ Error.prepareStackTrace = (_, stack) => stack;
21
+ const err = new Error();
22
+ const stack = err.stack;
23
+ Error.prepareStackTrace = originalStackTrace;
24
+ for (const frame of stack) {
25
+ const frameFileName = frame.getFileName();
26
+ if (frameFileName && !frameFileName.startsWith(currentModule)) {
27
+ console.warn(`${warning} at ${frameFileName}:${frame.getLineNumber()}`);
28
+ return;
29
+ }
30
+ }
31
+ console.warn(warning);
32
+ }
33
+ }
@@ -0,0 +1,19 @@
1
+ interface WebGLData {
2
+ vendor: string;
3
+ renderer: string;
4
+ data: string;
5
+ win: number;
6
+ mac: number;
7
+ lin: number;
8
+ webGl2Enabled: boolean;
9
+ }
10
+ export declare function sampleWebGL(os: "win" | "mac" | "lin", vendor?: string, renderer?: string): Promise<WebGLData>;
11
+ interface VendorRenderer {
12
+ vendor: string;
13
+ renderer: string;
14
+ }
15
+ interface PossiblePairs {
16
+ [key: string]: Array<VendorRenderer>;
17
+ }
18
+ export declare function getPossiblePairs(): Promise<PossiblePairs>;
19
+ export {};
@@ -0,0 +1,121 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { WebGLFingerprintUnavailable } from "../exceptions.js";
4
+ import { OS_ARCH_MATRIX } from "../pkgman.js";
5
+ // Cached Database constructor - loaded lazily on first use
6
+ let DatabaseConstructor = null;
7
+ /**
8
+ * Opens a SQLite database using the appropriate driver for the runtime.
9
+ * Uses bun:sqlite in Bun, better-sqlite3 in Node.js.
10
+ * The Database constructor is cached after first load.
11
+ */
12
+ async function openDatabase(pathName) {
13
+ try {
14
+ if (!DatabaseConstructor) {
15
+ if (typeof Bun !== "undefined") {
16
+ // @ts-expect-error - bun:sqlite only exists in Bun runtime
17
+ const { Database: BunDatabase } = await import("bun:sqlite");
18
+ DatabaseConstructor = BunDatabase;
19
+ }
20
+ else {
21
+ const { default: NodeDatabase } = await import("better-sqlite3");
22
+ DatabaseConstructor = NodeDatabase;
23
+ }
24
+ }
25
+ if (!DatabaseConstructor) {
26
+ throw new Error("No SQLite database constructor is available.");
27
+ }
28
+ return new DatabaseConstructor(pathName);
29
+ }
30
+ catch (error) {
31
+ throw new WebGLFingerprintUnavailable("WebGL fingerprint sampling data could not be loaded. Install optional SQLite support or continue with WebGL disabled.", { cause: error });
32
+ }
33
+ }
34
+ // Get database path relative to this file
35
+ const currentDir = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
36
+ const DB_PATH = path.join(currentDir, "..", "data-files", "webgl_data.db");
37
+ export async function sampleWebGL(os, vendor, renderer) {
38
+ if (!OS_ARCH_MATRIX[os]) {
39
+ throw new Error(`Invalid OS: ${os}. Must be one of: win, mac, lin`);
40
+ }
41
+ const db = await openDatabase(DB_PATH);
42
+ let query = "";
43
+ let params = [];
44
+ if (vendor && renderer) {
45
+ query = `SELECT vendor, renderer, data, ${os} FROM webgl_fingerprints WHERE vendor = ? AND renderer = ?`;
46
+ params = [vendor, renderer];
47
+ }
48
+ else {
49
+ query = `SELECT vendor, renderer, data, ${os} FROM webgl_fingerprints WHERE ${os} > 0`;
50
+ }
51
+ return new Promise((resolve, reject) => {
52
+ try {
53
+ const rows = db.prepare(query).all(...params);
54
+ if (rows.length === 0) {
55
+ reject(new Error(`No WebGL data found for OS: ${os}`));
56
+ return;
57
+ }
58
+ if (vendor && renderer) {
59
+ const result = rows[0];
60
+ if (!result) {
61
+ reject(new Error(`No WebGL data found for OS: ${os}`));
62
+ return;
63
+ }
64
+ const osWeight = result[os];
65
+ if (osWeight === undefined || osWeight <= 0) {
66
+ const pairs = db
67
+ .prepare(`SELECT DISTINCT vendor, renderer FROM webgl_fingerprints WHERE ${os} > 0`)
68
+ .all();
69
+ reject(new Error(`Vendor "${vendor}" and renderer "${renderer}" combination not valid for ${os}. Possible pairs: ${pairs.map((pair) => `${pair.vendor}, ${pair.renderer}`).join(", ")}`));
70
+ return;
71
+ }
72
+ resolve(JSON.parse(result.data));
73
+ }
74
+ else {
75
+ const dataStrs = rows.map((row) => row.data);
76
+ const probs = rows.map((row) => row[os]);
77
+ const probsArray = probs.map((p) => p / probs.reduce((a, b) => a + b, 0));
78
+ function weightedRandomChoice(weights) {
79
+ const sum = weights.reduce((acc, weight) => acc + weight, 0);
80
+ const threshold = Math.random() * sum;
81
+ let cumulativeSum = 0;
82
+ for (let i = 0; i < weights.length; i++) {
83
+ cumulativeSum += weights[i];
84
+ if (cumulativeSum >= threshold) {
85
+ return i;
86
+ }
87
+ }
88
+ return weights.length - 1; // Fallback in case of rounding errors
89
+ }
90
+ const idx = weightedRandomChoice(probsArray);
91
+ resolve(JSON.parse(dataStrs[idx]));
92
+ }
93
+ }
94
+ catch (err) {
95
+ reject(err);
96
+ }
97
+ }).finally(() => {
98
+ db.close();
99
+ });
100
+ }
101
+ export async function getPossiblePairs() {
102
+ const db = await openDatabase(DB_PATH);
103
+ const result = {};
104
+ return new Promise((resolve, reject) => {
105
+ try {
106
+ const osTypes = Object.keys(OS_ARCH_MATRIX);
107
+ osTypes.forEach((os_type) => {
108
+ const rows = db
109
+ .prepare(`SELECT DISTINCT vendor, renderer FROM webgl_fingerprints WHERE ${os_type} > 0 ORDER BY ${os_type} DESC`)
110
+ .all();
111
+ result[os_type] = rows;
112
+ });
113
+ resolve(result);
114
+ }
115
+ catch (err) {
116
+ reject(err);
117
+ }
118
+ }).finally(() => {
119
+ db.close();
120
+ });
121
+ }
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "headfox-js",
3
+ "version": "0.1.1",
4
+ "description": "TypeScript launcher and Playwright tooling for Headfox, currently powered by Camoufox-compatible browser bundles.",
5
+ "license": "MPL-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "bin": {
17
+ "headfox-js": "./bin/headfox-js.mjs",
18
+ "camoufox-js": "./bin/headfox-js.mjs"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "bin",
23
+ "README.md",
24
+ "LICENSE.md"
25
+ ],
26
+ "keywords": [
27
+ "headfox",
28
+ "firefox",
29
+ "camoufox",
30
+ "camoufox-js",
31
+ "playwright",
32
+ "fingerprinting",
33
+ "browser-automation",
34
+ "headlessx"
35
+ ],
36
+ "author": "saifyxpro (https://saify.me)",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/saifyxpro/HeadlessX.git",
40
+ "directory": "packages/headfox-js"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/saifyxpro/HeadlessX/issues"
44
+ },
45
+ "homepage": "https://github.com/saifyxpro/HeadlessX/tree/develop/packages/headfox-js",
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "packageManager": "pnpm@10.32.1",
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "clean": "rimraf dist",
55
+ "copy-files": "node ./copy-data-files.mjs",
56
+ "build": "pnpm run clean && tsc -p tsconfig.json && pnpm run copy-files",
57
+ "check": "biome check .",
58
+ "prepack": "pnpm run build",
59
+ "typecheck": "tsc -p tsconfig.json --noEmit",
60
+ "test": "vitest"
61
+ },
62
+ "dependencies": {
63
+ "adm-zip": "^0.5.16",
64
+ "cli-progress": "^3.12.0",
65
+ "commander": "^14.0.0",
66
+ "fingerprint-generator": "^2.1.66",
67
+ "glob": "^13.0.0",
68
+ "impit": "^0.11.0",
69
+ "language-tags": "^2.0.1",
70
+ "maxmind": "^5.0.0",
71
+ "pretty-bytes": "^7.1.0",
72
+ "ua-parser-js": "^2.0.2",
73
+ "xml2js": "^0.6.2"
74
+ },
75
+ "devDependencies": {
76
+ "@biomejs/biome": "2.4.7",
77
+ "@types/adm-zip": "^0.5.7",
78
+ "@types/better-sqlite3": "^7.6.13",
79
+ "@types/cli-progress": "^3.11.6",
80
+ "@types/language-tags": "^1.0.4",
81
+ "@types/node": "^24.0.0",
82
+ "@types/xml2js": "^0.4.14",
83
+ "playwright-core": "^1.58.2",
84
+ "rimraf": "^6.0.1",
85
+ "typescript": "^5.8.3",
86
+ "vitest": "^4.0.0"
87
+ },
88
+ "optionalDependencies": {
89
+ "better-sqlite3": "^12.2.0"
90
+ },
91
+ "peerDependencies": {
92
+ "playwright-core": ">=1.53.0"
93
+ }
94
+ }