headfox-js 0.1.1 → 0.1.12

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/README.md CHANGED
@@ -79,27 +79,48 @@ const browser = await firefox.connect(server.wsEndpoint());
79
79
 
80
80
  ## Browser source
81
81
 
82
- By default, `headfox-js` downloads the currently maintained Camoufox-compatible browser bundles.
82
+ By default, `headfox-js` downloads the official Camoufox browser bundles from `daijro/camoufox`.
83
83
 
84
84
  You can override the release source when you need to test a different fork or future Headfox-native releases:
85
85
 
86
86
  ```bash
87
87
  HEADFOX_JS_RELEASE_REPO=owner/repo
88
- HEADFOX_JS_ASSET_PREFIXES=headfox,camoufox
88
+ HEADFOX_JS_RELEASE_TAG=v135.0.1-beta.24
89
89
  ```
90
90
 
91
91
  Useful env vars:
92
92
 
93
93
  - `HEADFOX_JS_RELEASE_REPO`
94
+ - `HEADFOX_JS_RELEASE_TAG`
95
+ - `HEADFOX_JS_ASSET_NAME`
94
96
  - `HEADFOX_JS_ASSET_PREFIXES`
95
97
  - `HEADFOX_JS_CACHE_DIR`
96
98
 
97
99
  Legacy compatibility env vars are still accepted:
98
100
 
99
101
  - `CAMOUFOX_JS_RELEASE_REPO`
102
+ - `CAMOUFOX_JS_RELEASE_TAG`
103
+ - `CAMOUFOX_JS_ASSET_NAME`
100
104
  - `CAMOUFOX_JS_ASSET_PREFIXES`
101
105
  - `CAMOUFOX_JS_CACHE_DIR`
102
106
 
107
+ For deterministic installs in Docker or CI, pin a known tag and let `headfox-js` pick the matching platform asset:
108
+
109
+ ```bash
110
+ HEADFOX_JS_RELEASE_REPO=daijro/camoufox
111
+ HEADFOX_JS_RELEASE_TAG=v135.0.1-beta.24
112
+ npx headfox-js fetch
113
+ ```
114
+
115
+ If you need a custom fork with irregular asset names, set both the tag and exact asset name:
116
+
117
+ ```bash
118
+ HEADFOX_JS_RELEASE_REPO=CloverLabsAI/camoufox
119
+ HEADFOX_JS_RELEASE_TAG=webrtc-ip-by-context
120
+ HEADFOX_JS_ASSET_NAME=roverfox-webrtc-fix.zip
121
+ npx headfox-js fetch
122
+ ```
123
+
103
124
  ## Optional WebGL data support
104
125
 
105
126
  For best fingerprint fidelity, `headfox-js` will use the bundled WebGL sampling database when the optional SQLite dependency is available.
package/dist/pkgman.d.ts CHANGED
@@ -1,11 +1,21 @@
1
1
  import type { PathLike } from "node:fs";
2
2
  import type { Writable } from "node:stream";
3
3
  export declare const OS_NAME: "mac" | "win" | "lin";
4
+ export declare const DEFAULT_RELEASE_REPO = "daijro/camoufox";
5
+ export declare function resolveReleaseRepo(): string;
6
+ export declare function resolveReleaseTag(): string | undefined;
7
+ export declare function resolveAssetName(): string | undefined;
8
+ export declare function resolveAssetPrefixes(): string[];
4
9
  export declare const INSTALL_DIR: string;
5
10
  export declare const LOCAL_DATA: PathLike;
6
11
  export declare const OS_ARCH_MATRIX: {
7
12
  [key: string]: string[];
8
13
  };
14
+ export declare function buildAssetPattern(prefixes: string[], osName: "mac" | "win" | "lin", arch: string): RegExp;
15
+ export declare function parseVersionParts(...sources: Array<string | undefined | null>): {
16
+ version: string;
17
+ release: string;
18
+ } | null;
9
19
  declare class Version {
10
20
  release: string;
11
21
  version?: string;
@@ -20,11 +30,21 @@ declare class Version {
20
30
  static isSupportedPath(path: PathLike): boolean;
21
31
  static buildMinMax(): [Version, Version];
22
32
  }
33
+ type GitHubAsset = {
34
+ name: string;
35
+ browser_download_url: string;
36
+ };
37
+ type GitHubRelease = {
38
+ name?: string;
39
+ tag_name?: string;
40
+ assets: GitHubAsset[];
41
+ };
23
42
  export declare class GitHubDownloader {
24
43
  githubRepo: string;
25
44
  apiUrl: string;
26
- constructor(githubRepo: string);
27
- checkAsset(asset: any): any;
45
+ releaseTag?: string;
46
+ constructor(githubRepo: string, releaseTag?: string);
47
+ checkAsset(asset: GitHubAsset, _release: GitHubRelease): any;
28
48
  missingAssetError(): void;
29
49
  getAsset({ retries }?: {
30
50
  retries: number;
@@ -35,9 +55,10 @@ export declare class HeadfoxFetcher extends GitHubDownloader {
35
55
  _version_obj?: Version;
36
56
  pattern: RegExp;
37
57
  _url?: string;
58
+ assetNameOverride?: string;
38
59
  constructor();
39
60
  init(): Promise<void>;
40
- checkAsset(asset: any): [Version, string] | null;
61
+ checkAsset(asset: GitHubAsset, release: GitHubRelease): [Version, string] | null;
41
62
  missingAssetError(): void;
42
63
  static getPlatformArch(): string;
43
64
  fetchLatest(): Promise<void>;
package/dist/pkgman.js CHANGED
@@ -24,18 +24,33 @@ if (!(process.platform in OS_MAP)) {
24
24
  throw new UnsupportedOS(`OS ${process.platform} is not supported`);
25
25
  }
26
26
  export const OS_NAME = OS_MAP[process.platform];
27
- const RELEASE_REPO = process.env.HEADFOX_JS_RELEASE_REPO ??
28
- process.env.CAMOUFOX_JS_RELEASE_REPO ??
29
- "CloverLabsAI/camoufox";
30
- const CACHE_DIR_NAME = process.env.HEADFOX_JS_CACHE_DIR ??
31
- process.env.CAMOUFOX_JS_CACHE_DIR ??
32
- "headfox";
33
- const ASSET_PREFIXES = Array.from(new Set((process.env.HEADFOX_JS_ASSET_PREFIXES ??
34
- process.env.CAMOUFOX_JS_ASSET_PREFIXES ??
35
- "camoufox,headfox")
36
- .split(",")
37
- .map((value) => value.trim())
38
- .filter(Boolean)));
27
+ export const DEFAULT_RELEASE_REPO = "daijro/camoufox";
28
+ function readEnvValue(...names) {
29
+ for (const name of names) {
30
+ const value = process.env[name]?.trim();
31
+ if (value) {
32
+ return value;
33
+ }
34
+ }
35
+ return undefined;
36
+ }
37
+ export function resolveReleaseRepo() {
38
+ return (readEnvValue("HEADFOX_JS_RELEASE_REPO", "CAMOUFOX_JS_RELEASE_REPO") ??
39
+ DEFAULT_RELEASE_REPO);
40
+ }
41
+ export function resolveReleaseTag() {
42
+ return readEnvValue("HEADFOX_JS_RELEASE_TAG", "CAMOUFOX_JS_RELEASE_TAG");
43
+ }
44
+ export function resolveAssetName() {
45
+ return readEnvValue("HEADFOX_JS_ASSET_NAME", "CAMOUFOX_JS_ASSET_NAME");
46
+ }
47
+ export function resolveAssetPrefixes() {
48
+ return Array.from(new Set((readEnvValue("HEADFOX_JS_ASSET_PREFIXES", "CAMOUFOX_JS_ASSET_PREFIXES") ?? "camoufox,headfox")
49
+ .split(",")
50
+ .map((value) => value.trim())
51
+ .filter(Boolean)));
52
+ }
53
+ const CACHE_DIR_NAME = readEnvValue("HEADFOX_JS_CACHE_DIR", "CAMOUFOX_JS_CACHE_DIR") ?? "headfox";
39
54
  const APP_BUNDLE_CANDIDATES = ["Headfox.app", "Camoufox.app"];
40
55
  const currentDir = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url));
41
56
  export const INSTALL_DIR = userCacheDir(CACHE_DIR_NAME);
@@ -51,6 +66,28 @@ const LAUNCH_FILE_CANDIDATES = {
51
66
  mac: ["../MacOS/headfox", "../MacOS/camoufox"],
52
67
  lin: ["headfox-bin", "camoufox-bin"],
53
68
  };
69
+ function escapeRegex(value) {
70
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
71
+ }
72
+ export function buildAssetPattern(prefixes, osName, arch) {
73
+ return new RegExp(`(?:${prefixes.map(escapeRegex).join("|")})-(.+)-(.+)-${escapeRegex(osName)}\\.${escapeRegex(arch)}\\.zip$`, "i");
74
+ }
75
+ export function parseVersionParts(...sources) {
76
+ const pattern = /(\d+(?:\.\d+){0,2})-((?:alpha|beta|rc)\.\d+)/i;
77
+ for (const source of sources) {
78
+ if (!source) {
79
+ continue;
80
+ }
81
+ const match = source.match(pattern);
82
+ if (match) {
83
+ return {
84
+ version: match[1],
85
+ release: match[2].toLowerCase(),
86
+ };
87
+ }
88
+ }
89
+ return null;
90
+ }
54
91
  function installMissingMessage(product = "Headfox") {
55
92
  return `${product} is not installed at ${INSTALL_DIR}. Please run \`headfox-js fetch\` to install.`;
56
93
  }
@@ -139,11 +176,15 @@ const [VERSION_MIN, VERSION_MAX] = Version.buildMinMax();
139
176
  export class GitHubDownloader {
140
177
  githubRepo;
141
178
  apiUrl;
142
- constructor(githubRepo) {
179
+ releaseTag;
180
+ constructor(githubRepo, releaseTag) {
143
181
  this.githubRepo = githubRepo;
144
- this.apiUrl = `https://api.github.com/repos/${githubRepo}/releases`;
182
+ this.releaseTag = releaseTag;
183
+ this.apiUrl = releaseTag
184
+ ? `https://api.github.com/repos/${githubRepo}/releases/tags/${encodeURIComponent(releaseTag)}`
185
+ : `https://api.github.com/repos/${githubRepo}/releases`;
145
186
  }
146
- checkAsset(asset) {
187
+ checkAsset(asset, _release) {
147
188
  return asset.browser_download_url;
148
189
  }
149
190
  missingAssetError() {
@@ -167,10 +208,13 @@ export class GitHubDownloader {
167
208
  if (!response || !response.ok) {
168
209
  throw new Error(`Failed to fetch releases from ${this.apiUrl} after ${retries} attempts`);
169
210
  }
170
- const releases = await response.json();
211
+ const releaseData = await response.json();
212
+ const releases = Array.isArray(releaseData)
213
+ ? releaseData
214
+ : [releaseData];
171
215
  for (const release of releases) {
172
216
  for (const asset of release.assets) {
173
- const data = this.checkAsset(asset);
217
+ const data = this.checkAsset(asset, release);
174
218
  if (data) {
175
219
  return data;
176
220
  }
@@ -184,25 +228,47 @@ export class HeadfoxFetcher extends GitHubDownloader {
184
228
  _version_obj;
185
229
  pattern;
186
230
  _url;
231
+ assetNameOverride;
187
232
  constructor() {
188
- super(RELEASE_REPO);
233
+ super(resolveReleaseRepo(), resolveReleaseTag());
189
234
  this.arch = HeadfoxFetcher.getPlatformArch();
190
- this.pattern = new RegExp(`(?:${ASSET_PREFIXES.join("|")})-(.+)-(.+)-${OS_NAME}\\.${this.arch}\\.zip`);
235
+ this.assetNameOverride = resolveAssetName();
236
+ this.pattern = buildAssetPattern(resolveAssetPrefixes(), OS_NAME, this.arch);
191
237
  }
192
238
  async init() {
193
239
  await this.fetchLatest();
194
240
  }
195
- checkAsset(asset) {
196
- const match = asset.name.match(this.pattern);
197
- if (!match)
241
+ checkAsset(asset, release) {
242
+ if (this.assetNameOverride && asset.name !== this.assetNameOverride) {
243
+ return null;
244
+ }
245
+ if (!this.assetNameOverride && !asset.name.match(this.pattern)) {
198
246
  return null;
199
- const version = new Version(match[2], match[1]);
200
- if (!version.isSupported())
247
+ }
248
+ const versionData = parseVersionParts(asset.name, release.name, release.tag_name);
249
+ if (!versionData) {
250
+ if (this.assetNameOverride) {
251
+ throw new MissingRelease(`Configured asset "${asset.name}" was found in ${this.githubRepo}, but headfox-js could not infer its browser version from the asset or release metadata.`);
252
+ }
201
253
  return null;
254
+ }
255
+ const version = new Version(versionData.release, versionData.version);
256
+ if (!version.isSupported()) {
257
+ if (this.assetNameOverride) {
258
+ throw new UnsupportedVersion(`Configured asset "${asset.name}" resolved to unsupported browser release v${version.fullString}. Supported range: (${CONSTRAINTS.asRange()}).`);
259
+ }
260
+ return null;
261
+ }
202
262
  return [version, asset.browser_download_url];
203
263
  }
204
264
  missingAssetError() {
205
- throw new MissingRelease(`No matching release found for ${OS_NAME} ${this.arch} in the supported range: (${CONSTRAINTS.asRange()}). Please update the library.`);
265
+ const location = this.releaseTag
266
+ ? `${this.githubRepo} release tag "${this.releaseTag}"`
267
+ : this.githubRepo;
268
+ if (this.assetNameOverride) {
269
+ throw new MissingRelease(`Could not find configured asset "${this.assetNameOverride}" in ${location}.`);
270
+ }
271
+ throw new MissingRelease(`No matching release asset found in ${location} for ${OS_NAME} ${this.arch} in the supported range: (${CONSTRAINTS.asRange()}). Set HEADFOX_JS_RELEASE_TAG to pin a known working release when using custom forks.`);
206
272
  }
207
273
  static getPlatformArch() {
208
274
  const platArch = os.arch().toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "headfox-js",
3
- "version": "0.1.1",
3
+ "version": "0.1.12",
4
4
  "description": "TypeScript launcher and Playwright tooling for Headfox, currently powered by Camoufox-compatible browser bundles.",
5
5
  "license": "MPL-2.0",
6
6
  "type": "module",