mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
feat: move binary releases to wrapper repo, add auto-update check
- Binary downloads now served from CloakHQ/cloakbrowser releases (chromium-v* tags) - Auto-update: background version check on launch, downloads newer binary for next use - Graceful error on macOS/Windows (Linux-only binaries for now) - Rate-limited (1hr), opt-out via CLOAKBROWSER_AUTO_UPDATE=false - Add release-binary.yml workflow for anonymous binary releases
This commit is contained in:
+64
-8
@@ -3,6 +3,7 @@
|
||||
* Mirrors Python cloakbrowser/config.py.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -21,6 +22,10 @@ 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"]);
|
||||
|
||||
export function getPlatformTag(): string {
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
@@ -50,28 +55,79 @@ export function getCacheDir(): string {
|
||||
return path.join(os.homedir(), ".cloakbrowser");
|
||||
}
|
||||
|
||||
export function getBinaryDir(): string {
|
||||
return path.join(getCacheDir(), `chromium-${CHROMIUM_VERSION}`);
|
||||
export function getBinaryDir(version?: string): string {
|
||||
return path.join(getCacheDir(), `chromium-${version || CHROMIUM_VERSION}`);
|
||||
}
|
||||
|
||||
export function getBinaryPath(): string {
|
||||
const binaryDir = getBinaryDir();
|
||||
export function getBinaryPath(version?: string): string {
|
||||
const binaryDir = getBinaryDir(version);
|
||||
if (process.platform === "darwin") {
|
||||
return path.join(binaryDir, "Chromium.app", "Contents", "MacOS", "Chromium");
|
||||
}
|
||||
return path.join(binaryDir, "chrome");
|
||||
}
|
||||
|
||||
export function checkPlatformAvailable(): void {
|
||||
if (getLocalBinaryOverride()) return;
|
||||
|
||||
const tag = getPlatformTag(); // throws if unsupported entirely
|
||||
if (!AVAILABLE_PLATFORMS.has(tag)) {
|
||||
const available = [...AVAILABLE_PLATFORMS].sort().join(", ");
|
||||
throw new Error(
|
||||
`CloakBrowser is in active development. ` +
|
||||
`Pre-built binaries are currently only available for: ${available}.\n` +
|
||||
`macOS and Windows builds are coming soon.\n\n` +
|
||||
`To use CloakBrowser now, run in Docker (see README).`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Download URL
|
||||
// ---------------------------------------------------------------------------
|
||||
const DOWNLOAD_BASE_URL =
|
||||
export const DOWNLOAD_BASE_URL =
|
||||
process.env.CLOAKBROWSER_DOWNLOAD_URL ||
|
||||
"https://github.com/CloakHQ/chromium-stealth-builds/releases/download";
|
||||
"https://github.com/CloakHQ/cloakbrowser/releases/download";
|
||||
|
||||
export function getDownloadUrl(): string {
|
||||
export const GITHUB_API_URL =
|
||||
"https://api.github.com/repos/CloakHQ/cloakbrowser/releases";
|
||||
|
||||
export function getDownloadUrl(version?: string): string {
|
||||
const v = version || CHROMIUM_VERSION;
|
||||
const tag = getPlatformTag();
|
||||
return `${DOWNLOAD_BASE_URL}/v${CHROMIUM_VERSION}/cloakbrowser-${tag}.tar.gz`;
|
||||
return `${DOWNLOAD_BASE_URL}/chromium-v${v}/cloakbrowser-${tag}.tar.gz`;
|
||||
}
|
||||
|
||||
export function getEffectiveVersion(): string {
|
||||
const marker = path.join(getCacheDir(), "latest_version");
|
||||
try {
|
||||
if (fs.existsSync(marker)) {
|
||||
const version = fs.readFileSync(marker, "utf-8").trim();
|
||||
if (version && versionNewer(version, CHROMIUM_VERSION)) {
|
||||
const binary = getBinaryPath(version);
|
||||
if (fs.existsSync(binary)) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Marker unreadable — fall back to hardcoded
|
||||
}
|
||||
return CHROMIUM_VERSION;
|
||||
}
|
||||
|
||||
export function parseVersion(v: string): number[] {
|
||||
return v.split(".").map(Number);
|
||||
}
|
||||
|
||||
export function versionNewer(a: string, b: string): boolean {
|
||||
const va = parseVersion(a);
|
||||
const vb = parseVersion(b);
|
||||
for (let i = 0; i < Math.max(va.length, vb.length); i++) {
|
||||
if ((va[i] ?? 0) > (vb[i] ?? 0)) return true;
|
||||
if ((va[i] ?? 0) < (vb[i] ?? 0)) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+153
-20
@@ -13,15 +13,20 @@ import { extract as tarExtract } from "tar";
|
||||
import type { BinaryInfo } from "./types.js";
|
||||
import {
|
||||
CHROMIUM_VERSION,
|
||||
GITHUB_API_URL,
|
||||
checkPlatformAvailable,
|
||||
getBinaryDir,
|
||||
getBinaryPath,
|
||||
getCacheDir,
|
||||
getDownloadUrl,
|
||||
getEffectiveVersion,
|
||||
getLocalBinaryOverride,
|
||||
getPlatformTag,
|
||||
getCacheDir,
|
||||
versionNewer,
|
||||
} from "./config.js";
|
||||
|
||||
const DOWNLOAD_TIMEOUT_MS = 600_000; // 10 minutes
|
||||
const UPDATE_CHECK_INTERVAL_MS = 3_600_000; // 1 hour
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
@@ -44,27 +49,44 @@ export async function ensureBinary(): Promise<string> {
|
||||
return localOverride;
|
||||
}
|
||||
|
||||
// Check if binary is cached
|
||||
const binaryPath = getBinaryPath();
|
||||
// Fail fast if no binary available for this platform
|
||||
checkPlatformAvailable();
|
||||
|
||||
// Check for auto-updated version first, then fall back to hardcoded
|
||||
const effective = getEffectiveVersion();
|
||||
const binaryPath = getBinaryPath(effective);
|
||||
|
||||
if (fs.existsSync(binaryPath) && isExecutable(binaryPath)) {
|
||||
maybeTriggerUpdateCheck();
|
||||
return binaryPath;
|
||||
}
|
||||
|
||||
// Download
|
||||
// Fall back to hardcoded version if effective version binary doesn't exist
|
||||
if (effective !== CHROMIUM_VERSION) {
|
||||
const fallbackPath = getBinaryPath();
|
||||
if (fs.existsSync(fallbackPath) && isExecutable(fallbackPath)) {
|
||||
maybeTriggerUpdateCheck();
|
||||
return fallbackPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Download hardcoded version
|
||||
console.log(
|
||||
`[cloakbrowser] Stealth Chromium ${CHROMIUM_VERSION} not found. Downloading for ${getPlatformTag()}...`
|
||||
);
|
||||
await downloadAndExtract();
|
||||
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
const downloadedPath = getBinaryPath();
|
||||
if (!fs.existsSync(downloadedPath)) {
|
||||
throw new Error(
|
||||
`Download completed but binary not found at expected path: ${binaryPath}. ` +
|
||||
`Download completed but binary not found at expected path: ${downloadedPath}. ` +
|
||||
`This may indicate a packaging issue. Please report at ` +
|
||||
`https://github.com/CloakHQ/cloakbrowser/issues`
|
||||
);
|
||||
}
|
||||
|
||||
return binaryPath;
|
||||
maybeTriggerUpdateCheck();
|
||||
return downloadedPath;
|
||||
}
|
||||
|
||||
/** Remove all cached binaries. Forces re-download on next launch. */
|
||||
@@ -78,24 +100,43 @@ export function clearCache(): void {
|
||||
|
||||
/** Return info about the current binary installation. */
|
||||
export function binaryInfo(): BinaryInfo {
|
||||
const binaryPath = getBinaryPath();
|
||||
const effective = getEffectiveVersion();
|
||||
const binaryPath = getBinaryPath(effective);
|
||||
return {
|
||||
version: CHROMIUM_VERSION,
|
||||
version: effective,
|
||||
platform: getPlatformTag(),
|
||||
binaryPath,
|
||||
installed: fs.existsSync(binaryPath),
|
||||
cacheDir: getBinaryDir(),
|
||||
downloadUrl: getDownloadUrl(),
|
||||
cacheDir: getBinaryDir(effective),
|
||||
downloadUrl: getDownloadUrl(effective),
|
||||
};
|
||||
}
|
||||
|
||||
/** 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;
|
||||
|
||||
const binaryDir = getBinaryDir(latest);
|
||||
if (fs.existsSync(binaryDir)) {
|
||||
writeVersionMarker(latest);
|
||||
return latest;
|
||||
}
|
||||
|
||||
console.log(`[cloakbrowser] Downloading Chromium ${latest}...`);
|
||||
await downloadAndExtract(latest);
|
||||
writeVersionMarker(latest);
|
||||
return latest;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function downloadAndExtract(): Promise<void> {
|
||||
const url = getDownloadUrl();
|
||||
const binaryDir = getBinaryDir();
|
||||
async function downloadAndExtract(version?: string): Promise<void> {
|
||||
const url = getDownloadUrl(version);
|
||||
const binaryDir = getBinaryDir(version);
|
||||
const binaryPath = getBinaryPath(version);
|
||||
|
||||
// Create cache dir
|
||||
fs.mkdirSync(path.dirname(binaryDir), { recursive: true });
|
||||
@@ -108,7 +149,7 @@ async function downloadAndExtract(): Promise<void> {
|
||||
|
||||
try {
|
||||
await downloadFile(url, tmpPath);
|
||||
await extractArchive(tmpPath, binaryDir);
|
||||
await extractArchive(tmpPath, binaryDir, binaryPath);
|
||||
} finally {
|
||||
// Clean up temp file
|
||||
if (fs.existsSync(tmpPath)) {
|
||||
@@ -180,7 +221,8 @@ async function downloadFile(url: string, dest: string): Promise<void> {
|
||||
|
||||
async function extractArchive(
|
||||
archivePath: string,
|
||||
destDir: string
|
||||
destDir: string,
|
||||
binaryPath?: string
|
||||
): Promise<void> {
|
||||
console.log(`[cloakbrowser] Extracting to ${destDir}`);
|
||||
|
||||
@@ -212,10 +254,10 @@ async function extractArchive(
|
||||
flattenSingleSubdir(destDir);
|
||||
|
||||
// Make binary executable
|
||||
const binaryPath = getBinaryPath();
|
||||
if (fs.existsSync(binaryPath)) {
|
||||
fs.chmodSync(binaryPath, 0o755);
|
||||
console.log(`[cloakbrowser] Binary ready: ${binaryPath}`);
|
||||
const bp = binaryPath || getBinaryPath();
|
||||
if (fs.existsSync(bp)) {
|
||||
fs.chmodSync(bp, 0o755);
|
||||
console.log(`[cloakbrowser] Binary ready: ${bp}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,3 +290,94 @@ function isExecutable(filePath: string): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auto-update
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function shouldCheckForUpdate(): boolean {
|
||||
if (process.env.CLOAKBROWSER_AUTO_UPDATE?.toLowerCase() === "false")
|
||||
return false;
|
||||
if (getLocalBinaryOverride()) return false;
|
||||
if (process.env.CLOAKBROWSER_DOWNLOAD_URL) return false;
|
||||
|
||||
const checkFile = path.join(getCacheDir(), ".last_update_check");
|
||||
try {
|
||||
const lastCheck = Number(fs.readFileSync(checkFile, "utf-8").trim());
|
||||
if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return false;
|
||||
} catch {
|
||||
/* file doesn't exist or unreadable */
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getLatestChromiumVersion(): Promise<string | null> {
|
||||
try {
|
||||
const resp = await fetch(`${GITHUB_API_URL}?per_page=10`, {
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
const releases = (await resp.json()) as Array<{
|
||||
tag_name: string;
|
||||
draft: boolean;
|
||||
}>;
|
||||
for (const release of releases) {
|
||||
if (release.tag_name.startsWith("chromium-v") && !release.draft) {
|
||||
return release.tag_name.replace("chromium-v", "");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeVersionMarker(version: string): void {
|
||||
const cacheDir = getCacheDir();
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
const marker = path.join(cacheDir, "latest_version");
|
||||
const tmp = `${marker}.tmp`;
|
||||
fs.writeFileSync(tmp, version);
|
||||
fs.renameSync(tmp, marker);
|
||||
}
|
||||
|
||||
async function checkAndDownloadUpdate(): Promise<void> {
|
||||
try {
|
||||
// Record check timestamp first (rate limiting)
|
||||
const cacheDir = getCacheDir();
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(cacheDir, ".last_update_check"),
|
||||
String(Date.now())
|
||||
);
|
||||
|
||||
const latest = await getLatestChromiumVersion();
|
||||
if (!latest || !versionNewer(latest, CHROMIUM_VERSION)) return;
|
||||
|
||||
// Already downloaded?
|
||||
if (fs.existsSync(getBinaryDir(latest))) {
|
||||
writeVersionMarker(latest);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[cloakbrowser] Newer Chromium available: ${latest} (current: ${CHROMIUM_VERSION}). Downloading in background...`
|
||||
);
|
||||
await downloadAndExtract(latest);
|
||||
writeVersionMarker(latest);
|
||||
console.log(
|
||||
`[cloakbrowser] Background update complete: Chromium ${latest} ready. Will use on next launch.`
|
||||
);
|
||||
} catch (err) {
|
||||
// Background update failed — don't disrupt the user
|
||||
if (process.env.DEBUG) {
|
||||
console.error("[cloakbrowser] Background update failed:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maybeTriggerUpdateCheck(): void {
|
||||
if (!shouldCheckForUpdate()) return;
|
||||
// Fire-and-forget — don't await
|
||||
checkAndDownloadUpdate().catch(() => {});
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
export { launch, launchContext } from "./playwright.js";
|
||||
|
||||
// Binary management
|
||||
export { ensureBinary, clearCache, binaryInfo } from "./download.js";
|
||||
export { ensureBinary, clearCache, binaryInfo, checkForUpdate } from "./download.js";
|
||||
|
||||
// Config
|
||||
export { CHROMIUM_VERSION, getDefaultStealthArgs } from "./config.js";
|
||||
|
||||
Reference in New Issue
Block a user