import { createLogger } from "../../utils/logger/index.js";
import { init_logger } from "../../utils/logger.js";
import { init_paths, resolveBinDir } from "../../config/paths.js";
import { assertCacheDir, expandHome, init_cache_dir_policy } from "../cache-dir-policy.js";
import { loadPlaywrightCoreModule } from "./playwright-doctor.js";
import { WEBDRIVER_OVERRIDE_SCRIPT, buildStealthArgs, filterCloakBrowserExtraArgs, generateFingerprintSeed, makeExecutable, removeQuarantineAttr } from "../stealth.js";
import { pickFreePort } from "../free-port.js";
import { createHash } from "node:crypto";
import { createReadStream, createWriteStream } from "node:fs";
import { mkdir, mkdtemp, rm, stat } from "node:fs/promises";
import { join, resolve } from "node:path";
import { arch, platform, tmpdir } from "node:os";
import { spawn } from "node:child_process";
import { Readable, Transform } from "node:stream";
import AdmZip from "adm-zip";
import { pipeline } from "node:stream/promises";
//#region src/browser/providers/cloakbrowser.ts
/**
* CloakBrowser provider — anti-fingerprint Chromium with stealth capabilities.
*
* Manages CloakBrowser binary download, stealth launch, CDP connection,
* keep-open process reuse, and temporary profile lifecycle.
*
* Ported from brocli's cloak.rs to TypeScript, using Playwright's connectOverCDP
* to produce a standard Browser/BrowserContext pair.
*/
init_logger();
init_paths();
init_cache_dir_policy();
const log = createLogger("CloakBrowser");
const DOWNLOAD_BASE_URL = "https://cloakbrowser.dev";
const GITHUB_DOWNLOAD_BASE_URL = "https://github.com/CloakHQ/CloakBrowser/releases/download";
const XOPC_CLOAKBROWSER_PROXY_BASE = process.env.XOPC_CLOAKBROWSER_DOWNLOAD_BASE?.trim().replace(/\/$/, "") || "https://xopc.ai/api/cloakbrowser/download";
const READY_TIMEOUT_MS = 45e3;
const READY_POLL_INTERVAL_MS = 300;
const DEFAULT_KEEP_OPEN_CDP_PORT = 9222;
const PLATFORMS = {
	"darwin-arm64": {
		tag: "darwin-arm64",
		chromiumVersion: "145.0.7632.109.2",
		archiveExt: ".tar.gz",
		executableRelativePath: "Chromium.app/Contents/MacOS/Chromium",
		fingerprintPlatform: "macos",
		expectedSha256: ""
	},
	"darwin-x64": {
		tag: "darwin-x64",
		chromiumVersion: "145.0.7632.109.2",
		archiveExt: ".tar.gz",
		executableRelativePath: "Chromium.app/Contents/MacOS/Chromium",
		fingerprintPlatform: "macos",
		expectedSha256: ""
	},
	"linux-arm64": {
		tag: "linux-arm64",
		chromiumVersion: "146.0.7680.177.4",
		archiveExt: ".tar.gz",
		executableRelativePath: "chrome",
		fingerprintPlatform: "windows",
		expectedSha256: ""
	},
	"linux-x64": {
		tag: "linux-x64",
		chromiumVersion: "146.0.7680.177.4",
		archiveExt: ".tar.gz",
		executableRelativePath: "chrome",
		fingerprintPlatform: "windows",
		expectedSha256: ""
	},
	"win32-x64": {
		tag: "windows-x64",
		chromiumVersion: "146.0.7680.177.4",
		archiveExt: ".zip",
		executableRelativePath: "chrome.exe",
		fingerprintPlatform: "windows",
		expectedSha256: ""
	}
};
function detectPlatform() {
	const os = platform();
	const architecture = arch();
	const info = PLATFORMS[`${os}-${{
		arm64: "arm64",
		x64: "x64"
	}[architecture] ?? architecture}`];
	if (!info) throw new Error(`Unsupported CloakBrowser platform: ${os}/${architecture}`);
	return info;
}
/** Test-only: enumerate platform manifests. */
function listCloakBrowserPlatforms() {
	return Object.values(PLATFORMS);
}
function archiveDownloadUrls(platformInfo) {
	const archiveName = `cloakbrowser-${platformInfo.tag}${platformInfo.archiveExt}`;
	return [
		`${XOPC_CLOAKBROWSER_PROXY_BASE}/${archiveName}`,
		`${GITHUB_DOWNLOAD_BASE_URL}/chromium-v${platformInfo.chromiumVersion}/${archiveName}`,
		`${DOWNLOAD_BASE_URL}/download/${archiveName}`
	];
}
/** Test-only: resolved download URLs for a platform manifest (proxy first). */
function cloakBrowserArchiveDownloadUrls(platformInfo) {
	return archiveDownloadUrls(platformInfo);
}
async function sha256OfFile(path) {
	const hash = createHash("sha256");
	await pipeline(createReadStream(path), hash);
	return hash.digest("hex");
}
const CLOAKBROWSER_DIR_NAME = "cloakbrowser";
/** Default CloakBrowser home: ~/.xopc/bin/cloakbrowser (chromium-v*, profiles/, …). */
function defaultCloakBrowserCacheDir() {
	return join(resolveBinDir(), CLOAKBROWSER_DIR_NAME);
}
/** Resolve configured or default CloakBrowser cache root. */
function resolveCloakBrowserCacheDir(configured) {
	if (configured?.trim()) {
		const resolved = assertCacheDir(configured.trim());
		if (resolve(resolved) === resolve(resolveBinDir())) return defaultCloakBrowserCacheDir();
		return resolved;
	}
	return defaultCloakBrowserCacheDir();
}
async function resolveCloakExecutablePath(cacheDir, platformInfo, configuredBinaryPath) {
	const trimmed = configuredBinaryPath?.trim();
	const autoPath = binaryPath(cacheDir, platformInfo);
	if (!trimmed) return {
		execPath: autoPath,
		installed: await fileExists(autoPath),
		customBinaryPath: false
	};
	const customPath = resolve(expandHome(trimmed));
	if (await fileExists(customPath)) return {
		execPath: customPath,
		installed: true,
		customBinaryPath: true
	};
	if (await fileExists(autoPath)) return {
		execPath: autoPath,
		installed: true,
		customBinaryPath: true
	};
	return {
		execPath: customPath,
		installed: false,
		customBinaryPath: true
	};
}
function binaryDir(cacheDir, platformInfo) {
	return join(cacheDir, `chromium-v${platformInfo.chromiumVersion}`);
}
function binaryPath(cacheDir, platformInfo) {
	return join(binaryDir(cacheDir, platformInfo), platformInfo.executableRelativePath);
}
async function fileExists(path) {
	try {
		await stat(path);
		return true;
	} catch {
		return false;
	}
}
function runCommand(command, args) {
	return new Promise((resolve, reject) => {
		const child = spawn(command, args, { stdio: [
			"ignore",
			"ignore",
			"pipe"
		] });
		let stderr = "";
		child.stderr?.on("data", (data) => {
			stderr += data.toString();
		});
		child.once("error", reject);
		child.once("exit", (code, signal) => {
			if (code === 0) {
				resolve();
				return;
			}
			const reason = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
			const detail = stderr.trim() ? `: ${stderr.trim().slice(0, 500)}` : "";
			reject(/* @__PURE__ */ new Error(`${command} failed with ${reason}${detail}`));
		});
	});
}
async function extractArchive(archivePath, targetDir, platformInfo) {
	if (platformInfo.archiveExt === ".tar.gz") {
		await runCommand("tar", [
			"-xzf",
			archivePath,
			"-C",
			targetDir
		]);
		return;
	}
	if (platformInfo.archiveExt === ".zip") {
		new AdmZip(archivePath).extractAllTo(targetDir, true);
		return;
	}
	throw new Error(`Unsupported CloakBrowser archive format: ${platformInfo.archiveExt}`);
}
/**
* Download and extract CloakBrowser binary.
*
* After download, verifies SHA-256 against the platform manifest when one is
* present. Set `XOPC_CLOAKBROWSER_SKIP_HASH=1` to bypass (development only;
* the gateway logs a warning when the env var is honoured).
*/
async function downloadArchiveToFile(url, archivePath, onProgress, signal) {
	if (signal?.aborted) throw new Error("Install cancelled");
	const response = await fetch(url, {
		redirect: "follow",
		signal
	});
	if (!response.ok || !response.body) throw new Error(`download returned HTTP ${response.status}`);
	const contentLength = response.headers.get("content-length");
	const totalBytes = contentLength && Number.isFinite(Number(contentLength)) ? Number(contentLength) : null;
	let bytesReceived = 0;
	let lastReportAt = 0;
	const report = (force = false) => {
		const now = Date.now();
		if (!force && now - lastReportAt < 250) return;
		lastReportAt = now;
		onProgress?.({
			phase: "downloading",
			message: "Downloading CloakBrowser archive",
			bytesReceived,
			totalBytes,
			percent: totalBytes && totalBytes > 0 ? Math.min(100, Math.round(bytesReceived / totalBytes * 100)) : null
		});
	};
	report(true);
	await pipeline(Readable.fromWeb(response.body), new Transform({ transform(chunk, _encoding, callback) {
		bytesReceived += chunk.length;
		report();
		callback(null, chunk);
	} }), createWriteStream(archivePath));
	report(true);
}
async function downloadBinary(cacheDir, platformInfo, onProgress, signal) {
	const targetDir = binaryDir(cacheDir, platformInfo);
	const execPath = binaryPath(cacheDir, platformInfo);
	if (await fileExists(execPath)) {
		log.info({ path: execPath }, "CloakBrowser binary already cached");
		await onProgress?.({
			phase: "ready",
			message: "CloakBrowser binary already cached",
			percent: 100
		});
		return execPath;
	}
	await mkdir(cacheDir, { recursive: true });
	await onProgress?.({
		phase: "starting",
		message: "Preparing CloakBrowser download"
	});
	const archiveName = `cloakbrowser-${platformInfo.tag}${platformInfo.archiveExt}`;
	const urls = archiveDownloadUrls(platformInfo);
	const expectedSha256 = platformInfo.expectedSha256.trim().toLowerCase();
	const skipHash = process.env.XOPC_CLOAKBROWSER_SKIP_HASH === "1";
	log.info({
		version: platformInfo.chromiumVersion,
		platform: platformInfo.tag
	}, "Downloading CloakBrowser...");
	let downloadError;
	for (const url of urls) {
		if (signal?.aborted) throw new Error("Install cancelled");
		const stagingDir = await mkdtemp(join(cacheDir, ".download-"));
		const archivePath = join(stagingDir, archiveName);
		try {
			await downloadArchiveToFile(url, archivePath, onProgress, signal);
			if (expectedSha256) {
				if (signal?.aborted) throw new Error("Install cancelled");
				await onProgress?.({
					phase: "verifying",
					message: "Verifying SHA-256 checksum"
				});
				const actual = await sha256OfFile(archivePath);
				if (actual !== expectedSha256) throw new Error(`SHA-256 mismatch: expected ${expectedSha256}, got ${actual}. Aborted to avoid running unverified binary.`);
				log.info({ archivePath }, "SHA-256 verified");
			} else if (skipHash) log.warn({
				platform: platformInfo.tag,
				version: platformInfo.chromiumVersion
			}, "CloakBrowser SHA-256 verification skipped (XOPC_CLOAKBROWSER_SKIP_HASH=1)");
			else log.warn({
				platform: platformInfo.tag,
				version: platformInfo.chromiumVersion
			}, "CloakBrowser manifest has no expectedSha256; integrity NOT verified");
			await onProgress?.({
				phase: "extracting",
				message: "Extracting CloakBrowser archive"
			});
			log.info({ archivePath }, "Extracting CloakBrowser archive...");
			await rm(targetDir, {
				recursive: true,
				force: true
			}).catch(() => {});
			await mkdir(targetDir, { recursive: true });
			await extractArchive(archivePath, targetDir, platformInfo);
			if (!await fileExists(execPath)) throw new Error(`archive did not contain expected executable: ${execPath}`);
			await makeExecutable(execPath);
			await removeQuarantineAttr(execPath);
			await rm(stagingDir, {
				recursive: true,
				force: true
			}).catch(() => {});
			await onProgress?.({
				phase: "ready",
				message: "CloakBrowser binary ready",
				percent: 100
			});
			log.info({ path: execPath }, "CloakBrowser binary ready");
			return execPath;
		} catch (e) {
			await rm(stagingDir, {
				recursive: true,
				force: true
			}).catch(() => {});
			await rm(targetDir, {
				recursive: true,
				force: true
			}).catch(() => {});
			downloadError = e instanceof Error ? e : new Error(String(e));
			log.debug({
				url,
				errorMessage: downloadError.message
			}, "Download attempt failed");
		}
	}
	throw new Error(`Failed to download CloakBrowser v${platformInfo.chromiumVersion} for ${platformInfo.tag}: ${downloadError?.message ?? "all URLs failed"}`);
}
async function waitForCdpEndpoint(port) {
	const deadline = Date.now() + READY_TIMEOUT_MS;
	while (Date.now() < deadline) {
		try {
			const response = await fetch(`http://127.0.0.1:${port}/json/version`);
			if (response.ok) {
				const data = await response.json();
				if (data.webSocketDebuggerUrl) return data.webSocketDebuggerUrl;
			}
		} catch {}
		await new Promise((resolve) => setTimeout(resolve, READY_POLL_INTERVAL_MS));
	}
	throw new Error(`CloakBrowser did not expose CDP page within ${READY_TIMEOUT_MS / 1e3}s on port ${port}`);
}
/** Try to find an existing CDP page endpoint on the given port. */
async function reuseOrCreatePageEndpoint(port) {
	try {
		const response = await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(2e3) });
		if (!response.ok) return null;
		const page = (await response.json()).find((p) => p.type === "page" && p.webSocketDebuggerUrl);
		if (page?.webSocketDebuggerUrl) return page.webSocketDebuggerUrl;
		const newResponse = await fetch(`http://127.0.0.1:${port}/json/new`);
		if (newResponse.ok) {
			const newPage = await newResponse.json();
			if (newPage.webSocketDebuggerUrl) return newPage.webSocketDebuggerUrl;
		}
	} catch {}
	return null;
}
function resolveCloakBrowserProfilePaths(config, cacheDir) {
	if (config.userDataDir) return {
		userDataDir: config.userDataDir,
		temporaryProfileDir: null
	};
	if (config.temporaryProfile) {
		const userDataDir = join(tmpdir(), `xopc-cloakbrowser-${process.pid}-${generateFingerprintSeed()}`);
		return {
			userDataDir,
			temporaryProfileDir: userDataDir
		};
	}
	return {
		userDataDir: join(cacheDir, "profiles", "default"),
		temporaryProfileDir: null
	};
}
/** Resolve the persistent profile directory agents use (not ephemeral temp dirs). */
function resolveCloakBrowserPersistentProfileDir(config = {}) {
	if (config.userDataDir) return config.userDataDir;
	return join(resolveCloakBrowserCacheDir(config.cacheDir), "profiles", "default");
}
async function probeCdpPort(port) {
	try {
		return (await fetch(`http://127.0.0.1:${port}/json/version`, { signal: AbortSignal.timeout(2e3) })).ok;
	} catch {
		return false;
	}
}
/** Probe whether CloakBrowser CDP is listening on the configured keep-open port. */
async function probeCloakBrowserRuntime(config = {}) {
	const keepOpen = config.keepOpen ?? true;
	const cdpPort = config.cdpPort ?? (keepOpen ? DEFAULT_KEEP_OPEN_CDP_PORT : DEFAULT_KEEP_OPEN_CDP_PORT);
	const { userDataDir } = resolveCloakBrowserProfilePaths(config, resolveCloakBrowserCacheDir(config.cacheDir));
	return {
		running: await probeCdpPort(cdpPort),
		port: cdpPort,
		userDataDir,
		temporaryProfile: config.temporaryProfile === true
	};
}
function launchResultMeta(cdpPort, userDataDir, reused, pid, childProcess, temporaryProfileDir) {
	return {
		cdpPort,
		userDataDir,
		reused,
		pid,
		childProcess,
		temporaryProfileDir
	};
}
/**
* Launch or connect to a CloakBrowser instance and return a Playwright Browser + Context.
*/
async function launchCloakBrowser(config = {}) {
	const platformInfo = detectPlatform();
	const cacheDir = resolveCloakBrowserCacheDir(config.cacheDir);
	const keepOpen = config.keepOpen ?? true;
	const reuseExisting = config.reuseExisting ?? keepOpen;
	const configuredBinary = config.binaryPath?.trim() || void 0;
	const execPath = configuredBinary ? (await resolveCloakExecutablePath(cacheDir, platformInfo, configuredBinary)).execPath : await downloadBinary(cacheDir, platformInfo, config.onProgress, config.signal);
	if (configuredBinary) {
		await makeExecutable(execPath);
		await removeQuarantineAttr(execPath);
	}
	const cdpPort = config.cdpPort ?? (keepOpen ? DEFAULT_KEEP_OPEN_CDP_PORT : await pickFreePort());
	const skipPlaywrightConnect = config.skipPlaywrightConnect === true;
	const { userDataDir, temporaryProfileDir: plannedTempProfileDir } = resolveCloakBrowserProfilePaths(config, cacheDir);
	if (reuseExisting) {
		if (await reuseOrCreatePageEndpoint(cdpPort)) {
			log.info({ port: cdpPort }, "Reusing existing CloakBrowser instance");
			const meta = launchResultMeta(cdpPort, userDataDir, true, null, null, null);
			if (skipPlaywrightConnect) return meta;
			const pw = await loadPlaywrightCoreModule();
			const chromium = pw.chromium ?? pw.default?.chromium;
			if (!chromium?.connectOverCDP) throw new Error("playwright-core does not support connectOverCDP");
			const wsUrl = `ws://127.0.0.1:${cdpPort}`;
			const browserWsUrl = (await (await fetch(`http://127.0.0.1:${cdpPort}/json/version`)).json()).webSocketDebuggerUrl ?? wsUrl;
			const browser = await chromium.connectOverCDP(browserWsUrl);
			const contexts = browser.contexts();
			const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
			await context.addInitScript(WEBDRIVER_OVERRIDE_SCRIPT);
			return {
				browser,
				context,
				...meta
			};
		}
	}
	let temporaryProfileDir = plannedTempProfileDir;
	await mkdir(userDataDir, { recursive: true });
	const stealthArgs = buildStealthArgs(filterCloakBrowserExtraArgs(config.extraArgs ?? []), {
		timezone: config.timezone,
		locale: config.locale,
		webrtcIp: config.webrtcIp,
		fingerprintPlatform: config.fingerprintPlatform ?? platformInfo.fingerprintPlatform
	});
	const launchArgs = [
		`--remote-debugging-address=127.0.0.1`,
		`--remote-debugging-port=${cdpPort}`,
		`--user-data-dir=${userDataDir}`,
		"--no-first-run",
		"--no-default-browser-check",
		...stealthArgs
	];
	if (config.headless) launchArgs.push("--headless=new");
	if (platform() === "darwin") launchArgs.push("--use-mock-keychain");
	await config.onProgress?.({
		phase: "running",
		message: "Launching CloakBrowser for verification"
	});
	if (config.signal?.aborted) throw new Error("Install cancelled");
	log.info({
		execPath,
		port: cdpPort,
		headless: !!config.headless,
		keepOpen
	}, "Launching CloakBrowser");
	const child = spawn(execPath, launchArgs, {
		stdio: [
			"ignore",
			"ignore",
			"ignore"
		],
		detached: keepOpen
	});
	let onLaunchError = null;
	let onLaunchExit = null;
	const launchFailure = new Promise((_resolve, reject) => {
		onLaunchError = (error) => {
			reject(/* @__PURE__ */ new Error(`Failed to launch CloakBrowser at ${execPath}: ${error.message}`));
		};
		onLaunchExit = (code, signal) => {
			const reason = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
			reject(/* @__PURE__ */ new Error(`CloakBrowser exited before CDP became ready (${reason})`));
		};
		child.once("error", onLaunchError);
		child.once("exit", onLaunchExit);
	});
	if (keepOpen) child.unref();
	let browserWsUrl;
	try {
		browserWsUrl = await Promise.race([waitForCdpEndpoint(cdpPort), launchFailure]);
		if (onLaunchError) child.off("error", onLaunchError);
		if (onLaunchExit) child.off("exit", onLaunchExit);
	} catch (e) {
		if (onLaunchError) child.off("error", onLaunchError);
		if (onLaunchExit) child.off("exit", onLaunchExit);
		child.kill();
		if (temporaryProfileDir) await rm(temporaryProfileDir, {
			recursive: true,
			force: true
		}).catch(() => {});
		throw e;
	}
	let browserLevelWsUrl;
	try {
		browserLevelWsUrl = (await (await fetch(`http://127.0.0.1:${cdpPort}/json/version`)).json()).webSocketDebuggerUrl ?? browserWsUrl;
	} catch {
		browserLevelWsUrl = browserWsUrl;
	}
	const meta = launchResultMeta(cdpPort, userDataDir, false, child.pid ?? null, keepOpen ? null : child, temporaryProfileDir);
	if (skipPlaywrightConnect) {
		log.info({
			port: cdpPort,
			pid: child.pid
		}, "CloakBrowser launched (CDP only)");
		return meta;
	}
	const pw = await loadPlaywrightCoreModule();
	const chromium = pw.chromium ?? pw.default?.chromium;
	if (!chromium?.connectOverCDP) {
		child.kill();
		throw new Error("playwright-core does not support connectOverCDP");
	}
	const browser = await chromium.connectOverCDP(browserLevelWsUrl);
	const contexts = browser.contexts();
	const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
	await context.addInitScript(WEBDRIVER_OVERRIDE_SCRIPT);
	log.info({
		port: cdpPort,
		pid: child.pid
	}, "CloakBrowser launched and connected");
	return {
		browser,
		context,
		...meta
	};
}
/**
* Cleanup a CloakBrowser session — kill process and remove temp profile if applicable.
*/
async function cleanupCloakBrowser(childProcess, temporaryProfileDir) {
	if (childProcess) {
		childProcess.kill();
		await new Promise((resolve) => {
			const timer = setTimeout(resolve, 2e3);
			childProcess.once("exit", () => {
				clearTimeout(timer);
				resolve();
			});
		});
	}
	if (temporaryProfileDir) {
		await rm(temporaryProfileDir, {
			recursive: true,
			force: true
		}).catch(() => {});
		log.debug({ dir: temporaryProfileDir }, "Cleaned up temporary profile");
	}
}
/**
* Download (if needed), launch headlessly to verify, then return doctor status.
* Used by gateway install endpoints and CLI.
*/
async function installCloakBrowser(config = {}) {
	const result = await launchCloakBrowser({
		headless: true,
		temporaryProfile: true,
		keepOpen: false,
		cacheDir: config.cacheDir,
		binaryPath: config.binaryPath,
		onProgress: config.onProgress,
		signal: config.signal
	});
	await result.browser?.close().catch(() => {});
	await cleanupCloakBrowser(result.childProcess, result.temporaryProfileDir);
	return cloakBrowserDoctor({
		cacheDir: config.cacheDir,
		binaryPath: config.binaryPath
	});
}
/** Check CloakBrowser installation status. */
async function cloakBrowserDoctor(config = {}) {
	const platformInfo = detectPlatform();
	const cacheDir = resolveCloakBrowserCacheDir(config.cacheDir);
	const { execPath, installed, customBinaryPath } = await resolveCloakExecutablePath(cacheDir, platformInfo, config.binaryPath);
	const [primary, ...fallbacks] = archiveDownloadUrls(platformInfo);
	return {
		installed,
		version: installed ? platformInfo.chromiumVersion : null,
		binaryPath: installed ? execPath : null,
		platform: platformInfo.tag,
		cacheDir,
		expectedSha256: platformInfo.expectedSha256,
		downloadUrl: primary ?? "",
		fallbackUrls: fallbacks,
		customBinaryPath
	};
}
//#endregion
export { cleanupCloakBrowser, cloakBrowserArchiveDownloadUrls, cloakBrowserDoctor, defaultCloakBrowserCacheDir, installCloakBrowser, launchCloakBrowser, listCloakBrowserPlatforms, probeCloakBrowserRuntime, resolveCloakBrowserCacheDir, resolveCloakBrowserPersistentProfileDir };

//# sourceMappingURL=cloakbrowser.js.map