mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
feat: add wrapper version update checks for PyPI and npm
Check for newer wrapper versions on startup (once per process). Python queries PyPI, JS queries npm registry. Respects CLOAKBROWSER_AUTO_UPDATE=false and CLOAKBROWSER_DOWNLOAD_URL (custom mirror mode skips external registry calls). Includes unit tests for both languages covering: update detection, env var gating, network error handling, and once-per-process guard.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloakbrowser",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"description": "Stealth Chromium that passes every bot detection test. Drop-in Playwright/Puppeteer replacement with source-level fingerprint patches.",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -6,6 +6,20 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// Read wrapper version from package.json (single source of truth)
|
||||
let WRAPPER_VERSION = "0.0.0";
|
||||
try {
|
||||
const _configDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const _pkgPath = path.resolve(_configDir, "..", "package.json");
|
||||
const _pkg = JSON.parse(fs.readFileSync(_pkgPath, "utf-8")) as { version: string };
|
||||
WRAPPER_VERSION = _pkg.version;
|
||||
} catch {
|
||||
// Fallback — package.json not found (bundled or unusual layout).
|
||||
// Wrapper update check will compare against 0.0.0 and always suggest updating.
|
||||
}
|
||||
export { WRAPPER_VERSION };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chromium version shipped with this release.
|
||||
|
||||
+37
-1
@@ -17,6 +17,7 @@ import {
|
||||
DOWNLOAD_BASE_URL,
|
||||
GITHUB_API_URL,
|
||||
GITHUB_DOWNLOAD_BASE_URL,
|
||||
WRAPPER_VERSION,
|
||||
checkPlatformAvailable,
|
||||
getBinaryDir,
|
||||
getBinaryPath,
|
||||
@@ -477,6 +478,36 @@ function writeVersionMarker(version: string): void {
|
||||
fs.renameSync(tmp, marker);
|
||||
}
|
||||
|
||||
let wrapperUpdateChecked = false;
|
||||
|
||||
/** @internal Exported for testing only. */
|
||||
export function resetWrapperUpdateChecked(): void {
|
||||
wrapperUpdateChecked = false;
|
||||
}
|
||||
|
||||
/** @internal Exported for testing only. */
|
||||
export async function checkWrapperUpdate(): Promise<void> {
|
||||
if (wrapperUpdateChecked) return;
|
||||
wrapperUpdateChecked = true;
|
||||
if (process.env.CLOAKBROWSER_AUTO_UPDATE?.toLowerCase() === "false") return;
|
||||
if (process.env.CLOAKBROWSER_DOWNLOAD_URL) return;
|
||||
try {
|
||||
const resp = await fetch("https://registry.npmjs.org/cloakbrowser/latest", {
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
});
|
||||
if (!resp.ok) return;
|
||||
const data = (await resp.json()) as { version: string };
|
||||
if (data.version && versionNewer(data.version, WRAPPER_VERSION)) {
|
||||
console.warn(
|
||||
`[cloakbrowser] Update available: ${WRAPPER_VERSION} → ${data.version}. ` +
|
||||
`Run: npm install cloakbrowser@latest`
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal — never block binary update check
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAndDownloadUpdate(): Promise<void> {
|
||||
try {
|
||||
// Record check timestamp first (rate limiting)
|
||||
@@ -514,7 +545,12 @@ async function checkAndDownloadUpdate(): Promise<void> {
|
||||
}
|
||||
|
||||
function maybeTriggerUpdateCheck(): void {
|
||||
// Wrapper update: once per process, not rate-limited
|
||||
if (!wrapperUpdateChecked) {
|
||||
checkWrapperUpdate().catch(() => {});
|
||||
}
|
||||
|
||||
// Binary update: rate-limited to once per hour
|
||||
if (!shouldCheckForUpdate()) return;
|
||||
// Fire-and-forget — don't await
|
||||
checkAndDownloadUpdate().catch(() => {});
|
||||
}
|
||||
|
||||
+86
-2
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
||||
import {
|
||||
CHROMIUM_VERSION,
|
||||
getChromiumVersion,
|
||||
@@ -8,7 +8,12 @@ import {
|
||||
parseVersion,
|
||||
versionNewer,
|
||||
} from "../src/config.js";
|
||||
import { getLatestChromiumVersion, parseChecksums } from "../src/download.js";
|
||||
import {
|
||||
checkWrapperUpdate,
|
||||
getLatestChromiumVersion,
|
||||
parseChecksums,
|
||||
resetWrapperUpdateChecked,
|
||||
} from "../src/download.js";
|
||||
|
||||
describe("version comparison", () => {
|
||||
it("parseVersion handles 4-part versions", () => {
|
||||
@@ -149,6 +154,85 @@ describe("latest version (platform-aware)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("wrapper update check", () => {
|
||||
beforeEach(() => {
|
||||
resetWrapperUpdateChecked();
|
||||
delete process.env.CLOAKBROWSER_AUTO_UPDATE;
|
||||
delete process.env.CLOAKBROWSER_DOWNLOAD_URL;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
delete process.env.CLOAKBROWSER_AUTO_UPDATE;
|
||||
delete process.env.CLOAKBROWSER_DOWNLOAD_URL;
|
||||
});
|
||||
|
||||
it("warns when newer version available", async () => {
|
||||
const spy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ version: "99.0.0" }),
|
||||
} as Response);
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
await checkWrapperUpdate();
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Update available"));
|
||||
});
|
||||
|
||||
it("silent when current version", async () => {
|
||||
const { WRAPPER_VERSION } = await import("../src/config.js");
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ version: WRAPPER_VERSION }),
|
||||
} as Response);
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
await checkWrapperUpdate();
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("disabled by CLOAKBROWSER_AUTO_UPDATE=false", async () => {
|
||||
process.env.CLOAKBROWSER_AUTO_UPDATE = "false";
|
||||
const spy = vi.spyOn(globalThis, "fetch");
|
||||
|
||||
await checkWrapperUpdate();
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("disabled by CLOAKBROWSER_DOWNLOAD_URL", async () => {
|
||||
process.env.CLOAKBROWSER_DOWNLOAD_URL = "https://mirror.example.com";
|
||||
const spy = vi.spyOn(globalThis, "fetch");
|
||||
|
||||
await checkWrapperUpdate();
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("silent on network error", async () => {
|
||||
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("timeout"));
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
await checkWrapperUpdate();
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs only once per process", async () => {
|
||||
const spy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ version: "0.0.1" }),
|
||||
} as Response);
|
||||
|
||||
await checkWrapperUpdate();
|
||||
await checkWrapperUpdate();
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseChecksums", () => {
|
||||
// Valid 64-char hex strings for testing
|
||||
const HASH_A = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
||||
|
||||
Reference in New Issue
Block a user