feat: per-platform Chromium versioning and build number support

- Add PLATFORM_CHROMIUM_VERSIONS map (Linux=v145, macOS=v142)
- Add get_chromium_version()/getChromiumVersion() for platform-specific version
- Make auto-update check release assets before offering updates
- Scope version markers per-platform (latest_version_linux-x64)
- Support 5th version segment for hotfix builds (e.g. 145.0.7632.109.2)
- Derive AVAILABLE_PLATFORMS from version map
This commit is contained in:
CloakHQ
2026-03-02 08:23:44 +01:00
parent 4c2e06682b
commit fc10bdf13e
9 changed files with 296 additions and 75 deletions
+25 -10
View File
@@ -8,10 +8,20 @@ import os from "node:os";
import path from "node:path";
// ---------------------------------------------------------------------------
// Chromium version shipped with this release
// Chromium version shipped with this release.
// Different platforms may ship different versions (e.g. Linux gets v145 first,
// macOS stays on v142 until Mac builds are ready).
// CHROMIUM_VERSION is the latest across all platforms (for display/reference).
// Use getChromiumVersion() for the current platform's actual version.
// ---------------------------------------------------------------------------
export const CHROMIUM_VERSION = "145.0.7632.109";
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
"linux-x64": "145.0.7632.109",
"darwin-arm64": "142.0.7444.175",
"darwin-x64": "142.0.7444.175",
};
// ---------------------------------------------------------------------------
// Platform detection
// ---------------------------------------------------------------------------
@@ -22,9 +32,13 @@ const SUPPORTED_PLATFORMS: Record<string, string> = {
"darwin-x64": "darwin-x64",
};
// Platforms with pre-built binaries available for download.
// Update this set as new platform builds are released.
const AVAILABLE_PLATFORMS = new Set(["linux-x64", "darwin-arm64", "darwin-x64"]);
// Platforms with pre-built binaries available for download (derived from version map).
const AVAILABLE_PLATFORMS = new Set(Object.keys(PLATFORM_CHROMIUM_VERSIONS));
export function getChromiumVersion(): string {
const tag = getPlatformTag();
return PLATFORM_CHROMIUM_VERSIONS[tag] ?? CHROMIUM_VERSION;
}
export function getPlatformTag(): string {
const platform = process.platform;
@@ -56,7 +70,7 @@ export function getCacheDir(): string {
}
export function getBinaryDir(version?: string): string {
return path.join(getCacheDir(), `chromium-${version || CHROMIUM_VERSION}`);
return path.join(getCacheDir(), `chromium-${version || getChromiumVersion()}`);
}
export function getBinaryPath(version?: string): string {
@@ -95,23 +109,24 @@ export const GITHUB_DOWNLOAD_BASE_URL =
"https://github.com/CloakHQ/cloakbrowser/releases/download";
export function getDownloadUrl(version?: string): string {
const v = version || CHROMIUM_VERSION;
const v = version || getChromiumVersion();
const tag = getPlatformTag();
return `${DOWNLOAD_BASE_URL}/chromium-v${v}/cloakbrowser-${tag}.tar.gz`;
}
export function getFallbackDownloadUrl(version?: string): string {
const v = version || CHROMIUM_VERSION;
const v = version || getChromiumVersion();
const tag = getPlatformTag();
return `${GITHUB_DOWNLOAD_BASE_URL}/chromium-v${v}/cloakbrowser-${tag}.tar.gz`;
}
export function getEffectiveVersion(): string {
const marker = path.join(getCacheDir(), "latest_version");
const base = getChromiumVersion();
const marker = path.join(getCacheDir(), `latest_version_${getPlatformTag()}`);
try {
if (fs.existsSync(marker)) {
const version = fs.readFileSync(marker, "utf-8").trim();
if (version && versionNewer(version, CHROMIUM_VERSION)) {
if (version && versionNewer(version, base)) {
const binary = getBinaryPath(version);
if (fs.existsSync(binary)) {
return version;
@@ -121,7 +136,7 @@ export function getEffectiveVersion(): string {
} catch {
// Marker unreadable — fall back to hardcoded
}
return CHROMIUM_VERSION;
return base;
}
export function parseVersion(v: string): number[] {
+22 -12
View File
@@ -14,7 +14,6 @@ import { extract as tarExtract } from "tar";
import type { BinaryInfo } from "./types.js";
import {
CHROMIUM_VERSION,
DOWNLOAD_BASE_URL,
GITHUB_API_URL,
GITHUB_DOWNLOAD_BASE_URL,
@@ -22,6 +21,7 @@ import {
getBinaryDir,
getBinaryPath,
getCacheDir,
getChromiumVersion,
getDownloadUrl,
getEffectiveVersion,
getFallbackDownloadUrl,
@@ -66,8 +66,9 @@ export async function ensureBinary(): Promise<string> {
return binaryPath;
}
// Fall back to hardcoded version if effective version binary doesn't exist
if (effective !== CHROMIUM_VERSION) {
// Fall back to platform's hardcoded version if effective version binary doesn't exist
const platformVersion = getChromiumVersion();
if (effective !== platformVersion) {
const fallbackPath = getBinaryPath();
if (fs.existsSync(fallbackPath) && isExecutable(fallbackPath)) {
maybeTriggerUpdateCheck();
@@ -75,9 +76,9 @@ export async function ensureBinary(): Promise<string> {
}
}
// Download hardcoded version
// Download platform's hardcoded version
console.log(
`[cloakbrowser] Stealth Chromium ${CHROMIUM_VERSION} not found. Downloading for ${getPlatformTag()}...`
`[cloakbrowser] Stealth Chromium ${platformVersion} not found. Downloading for ${getPlatformTag()}...`
);
await downloadAndExtract();
@@ -120,7 +121,7 @@ export function binaryInfo(): BinaryInfo {
/** Manually check for a newer Chromium version. Returns new version or null. */
export async function checkForUpdate(): Promise<string | null> {
const latest = await getLatestChromiumVersion();
if (!latest || !versionNewer(latest, CHROMIUM_VERSION)) return null;
if (!latest || !versionNewer(latest, getChromiumVersion())) return null;
const binaryDir = getBinaryDir(latest);
if (fs.existsSync(binaryDir)) {
@@ -209,7 +210,7 @@ async function verifyDownloadChecksum(filePath: string, version?: string): Promi
}
async function fetchChecksums(version?: string): Promise<Map<string, string> | null> {
const v = version || CHROMIUM_VERSION;
const v = version || getChromiumVersion();
const hasCustomUrl = !!process.env.CLOAKBROWSER_DOWNLOAD_URL;
// Respect custom URL contract — no GitHub fallback when custom URL is set
@@ -437,7 +438,8 @@ function shouldCheckForUpdate(): boolean {
return true;
}
async function getLatestChromiumVersion(): Promise<string | null> {
/** @internal Exported for testing only. */
export async function getLatestChromiumVersion(): Promise<string | null> {
try {
const resp = await fetch(`${GITHUB_API_URL}?per_page=10`, {
signal: AbortSignal.timeout(10_000),
@@ -446,10 +448,17 @@ async function getLatestChromiumVersion(): Promise<string | null> {
const releases = (await resp.json()) as Array<{
tag_name: string;
draft: boolean;
assets: Array<{ name: string }>;
}>;
const platformTarball = `cloakbrowser-${getPlatformTag()}.tar.gz`;
for (const release of releases) {
if (release.tag_name.startsWith("chromium-v") && !release.draft) {
return release.tag_name.replace("chromium-v", "");
const assetNames = new Set(
(release.assets ?? []).map((a) => a.name)
);
if (assetNames.has(platformTarball)) {
return release.tag_name.replace(/^chromium-v/, "");
}
}
}
return null;
@@ -461,7 +470,7 @@ async function getLatestChromiumVersion(): Promise<string | null> {
function writeVersionMarker(version: string): void {
const cacheDir = getCacheDir();
fs.mkdirSync(cacheDir, { recursive: true });
const marker = path.join(cacheDir, "latest_version");
const marker = path.join(cacheDir, `latest_version_${getPlatformTag()}`);
const tmp = `${marker}.tmp`;
fs.writeFileSync(tmp, version);
fs.renameSync(tmp, marker);
@@ -477,8 +486,9 @@ async function checkAndDownloadUpdate(): Promise<void> {
String(Date.now())
);
const platformVersion = getChromiumVersion();
const latest = await getLatestChromiumVersion();
if (!latest || !versionNewer(latest, CHROMIUM_VERSION)) return;
if (!latest || !versionNewer(latest, platformVersion)) return;
// Already downloaded?
if (fs.existsSync(getBinaryDir(latest))) {
@@ -487,7 +497,7 @@ async function checkAndDownloadUpdate(): Promise<void> {
}
console.log(
`[cloakbrowser] Newer Chromium available: ${latest} (current: ${CHROMIUM_VERSION}). Downloading in background...`
`[cloakbrowser] Newer Chromium available: ${latest} (current: ${platformVersion}). Downloading in background...`
);
await downloadAndExtract(latest);
writeVersionMarker(latest);
+6 -5
View File
@@ -1,6 +1,7 @@
import { describe, it, expect } from "vitest";
import {
CHROMIUM_VERSION,
getChromiumVersion,
getDefaultStealthArgs,
getCacheDir,
getBinaryDir,
@@ -10,7 +11,7 @@ import { _buildArgsForTest } from "../src/playwright.js";
describe("config", () => {
it("CHROMIUM_VERSION matches expected format", () => {
expect(CHROMIUM_VERSION).toMatch(/^\d+\.\d+\.\d+\.\d+$/);
expect(CHROMIUM_VERSION).toMatch(/^\d+\.\d+\.\d+\.\d+(\.\d+)?$/);
});
it("getDefaultStealthArgs returns expected flags", () => {
@@ -53,14 +54,14 @@ describe("config", () => {
expect(dir).toContain(".cloakbrowser");
});
it("getBinaryDir includes version", () => {
it("getBinaryDir includes platform version", () => {
const dir = getBinaryDir();
expect(dir).toContain(`chromium-${CHROMIUM_VERSION}`);
expect(dir).toContain(`chromium-${getChromiumVersion()}`);
});
it("getDownloadUrl contains version and platform tag", () => {
it("getDownloadUrl contains platform version and platform tag", () => {
const url = getDownloadUrl();
expect(url).toContain(CHROMIUM_VERSION);
expect(url).toContain(getChromiumVersion());
expect(url).toContain("cloakbrowser-");
expect(url).toContain(".tar.gz");
expect(url).toContain("cloakbrowser.dev");
+2 -2
View File
@@ -1,12 +1,12 @@
import { describe, it, expect } from "vitest";
import { binaryInfo } from "../src/download.js";
import { CHROMIUM_VERSION } from "../src/config.js";
import { getChromiumVersion } from "../src/config.js";
describe("binaryInfo", () => {
it("returns correct structure", () => {
const info = binaryInfo();
expect(info.version).toBe(CHROMIUM_VERSION);
expect(info.version).toBe(getChromiumVersion());
expect(info.platform).toMatch(/^(linux|darwin)-(x64|arm64)$/);
expect(info.binaryPath).toBeTruthy();
expect(typeof info.installed).toBe("boolean");
+100 -4
View File
@@ -1,11 +1,14 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi, afterEach } from "vitest";
import {
CHROMIUM_VERSION,
getChromiumVersion,
getDownloadUrl,
getEffectiveVersion,
getPlatformTag,
parseVersion,
versionNewer,
} from "../src/config.js";
import { getLatestChromiumVersion } from "../src/download.js";
describe("version comparison", () => {
it("parseVersion handles 4-part versions", () => {
@@ -32,13 +35,29 @@ describe("version comparison", () => {
it("major bump wins over minor", () => {
expect(versionNewer("143.0.0.0", "142.9.9999.999")).toBe(true);
});
it("parseVersion handles 5-part build numbers", () => {
expect(parseVersion("145.0.7632.109.2")).toEqual([145, 0, 7632, 109, 2]);
});
it("build bump detected", () => {
expect(versionNewer("145.0.7632.109.3", "145.0.7632.109.2")).toBe(true);
});
it("build suffix newer than no suffix", () => {
expect(versionNewer("145.0.7632.109.2", "145.0.7632.109")).toBe(true);
});
it("no suffix older than build suffix", () => {
expect(versionNewer("145.0.7632.109", "145.0.7632.109.2")).toBe(false);
});
});
describe("download URL", () => {
it("uses chromium-v prefix and cloakbrowser repo", () => {
const url = getDownloadUrl();
expect(url).toContain("cloakbrowser.dev");
expect(url).toContain(`chromium-v${CHROMIUM_VERSION}`);
expect(url).toContain(`chromium-v${getChromiumVersion()}`);
expect(url.endsWith(".tar.gz")).toBe(true);
});
@@ -53,9 +72,86 @@ describe("download URL", () => {
});
});
describe("latest version (platform-aware)", () => {
const platformTarball = `cloakbrowser-${getPlatformTag()}.tar.gz`;
function makeAssets(platforms: string[]) {
return platforms.map((p) => ({ name: `cloakbrowser-${p}.tar.gz` }));
}
function mockFetch(releases: Array<Record<string, unknown>>) {
return vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => releases,
} as Response);
}
afterEach(() => {
vi.restoreAllMocks();
});
it("returns version when release has platform asset", async () => {
mockFetch([
{
tag_name: "chromium-v145.0.7718.0",
draft: false,
assets: makeAssets(["linux-x64", "darwin-arm64", "darwin-x64"]),
},
]);
expect(await getLatestChromiumVersion()).toBe("145.0.7718.0");
});
it("skips release without platform asset", async () => {
const spy = mockFetch([
{
tag_name: "chromium-v145.0.7718.0",
draft: false,
assets: makeAssets(["linux-x64"]), // Linux only
},
{
tag_name: "chromium-v142.0.7444.175",
draft: false,
assets: makeAssets(["linux-x64", "darwin-arm64", "darwin-x64"]),
},
]);
const result = await getLatestChromiumVersion();
const tag = getPlatformTag();
if (tag === "linux-x64") {
expect(result).toBe("145.0.7718.0");
} else {
expect(result).toBe("142.0.7444.175");
}
});
it("returns null when no release has platform asset", async () => {
mockFetch([
{
tag_name: "chromium-v145.0.7718.0",
draft: false,
assets: [{ name: "cloakbrowser-windows-x64.tar.gz" }],
},
]);
expect(await getLatestChromiumVersion()).toBeNull();
});
it("skips draft releases", async () => {
const all = ["linux-x64", "darwin-arm64", "darwin-x64"];
mockFetch([
{ tag_name: "chromium-v999.0.0.0", draft: true, assets: makeAssets(all) },
{ tag_name: "chromium-v145.0.7718.0", draft: false, assets: makeAssets(all) },
]);
expect(await getLatestChromiumVersion()).toBe("145.0.7718.0");
});
it("returns null on network error", async () => {
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("timeout"));
expect(await getLatestChromiumVersion()).toBeNull();
});
});
describe("effective version", () => {
it("returns CHROMIUM_VERSION when no marker exists", () => {
it("returns platform version when no marker exists", () => {
// Default behavior — no marker file in test environment
expect(getEffectiveVersion()).toBe(CHROMIUM_VERSION);
expect(getEffectiveVersion()).toBe(getChromiumVersion());
});
});