From 031823587e476d560ba68fd36c345b3a0382499a Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Fri, 25 Jul 2025 08:52:13 +0400 Subject: [PATCH] refactor: camoufox rust implementation --- .github/workflows/release.yml | 8 + .github/workflows/rolling-release.yml | 8 + .vscode/settings.json | 45 + nodecar/package.json | 8 +- nodecar/src/camoufox-launcher.ts | 482 ++------ nodecar/src/index.ts | 96 +- package.json | 2 +- pnpm-lock.yaml | 520 ++++----- src-tauri/Cargo.lock | 6 +- src-tauri/Cargo.toml | 5 +- src-tauri/src/browser.rs | 2 +- src-tauri/src/browser_runner.rs | 294 +++-- src-tauri/src/camoufox.rs | 607 ---------- src-tauri/src/camoufox_direct.rs | 1334 ++++++++++++++++++++++ src-tauri/src/lib.rs | 29 +- src/app/page.tsx | 12 +- src/components/create-profile-dialog.tsx | 75 +- src/lib/toast-utils.ts | 4 +- 18 files changed, 1958 insertions(+), 1579 deletions(-) delete mode 100644 src-tauri/src/camoufox.rs create mode 100644 src-tauri/src/camoufox_direct.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 68a330a..e25f953 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,6 +115,14 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0 + - name: Setup Python + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b #v5.9.0 + with: + python-version: '3.11' + + - name: Install PyOxidizer + run: pip install pyoxidizer + - name: Setup Rust uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b #master with: diff --git a/.github/workflows/rolling-release.yml b/.github/workflows/rolling-release.yml index 6437a20..079e4d9 100644 --- a/.github/workflows/rolling-release.yml +++ b/.github/workflows/rolling-release.yml @@ -114,6 +114,14 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda #v4.1.0 + - name: Setup Python + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b #v5.9.0 + with: + python-version: '3.11' + + - name: Install PyOxidizer + run: pip install pyoxidizer + - name: Setup Rust uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b #master with: diff --git a/.vscode/settings.json b/.vscode/settings.json index deed89f..8c9736f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,20 +5,29 @@ "appimage", "appindicator", "applescript", + "asyncio", "autoconfig", "autologin", "biomejs", + "browserforge", + "CAMOU", "camoufox", "cdylib", + "certifi", "CFURL", "checkin", + "chrono", "CLICOLOR", "clippy", "cmdk", "codegen", + "codesign", "CTYPE", + "dataclasses", "datareporting", + "datas", "devedition", + "doctest", "doesn", "donutbrowser", "dpkg", @@ -28,16 +37,24 @@ "errorlevel", "esac", "esbuild", + "etree", "frontmost", "geoip", + "getcwd", "gettimezone", "gifs", "gsettings", "healthreport", + "hiddenimports", "hkcu", + "hooksconfig", + "hookspath", "icns", + "idlelib", "idletime", + "idna", "Inno", + "keras", "KHTML", "launchservices", "letterboxing", @@ -51,6 +68,7 @@ "libwebkit", "libxdo", "localtime", + "lxml", "mmdb", "mountpoint", "msiexec", @@ -58,27 +76,49 @@ "msys", "Mullvad", "mullvadbrowser", + "mypy", + "noarchive", + "noconfirm", "nodecar", "nodemon", "norestart", "NSIS", "ntlm", + "numpy", "objc", "orhun", + "orjson", "osascript", + "pathex", + "pathlib", "peerconnection", "pixbuf", "plasmohq", + "platformdirs", "prefs", "propertylist", + "psutil", + "pycache", + "pydantic", + "pyee", + "pyinstaller", + "pyoxidizer", + "pytest", + "pyyaml", "reqwest", "ridedott", "rlib", "rustc", "SARIF", + "scipy", + "screeninfo", "serde", + "setuptools", "shadcn", + "shutil", "signon", + "signum", + "sklearn", "sonner", "splitn", "sspi", @@ -97,14 +137,19 @@ "TERX", "timedatectl", "titlebar", + "tkinter", "Torbrowser", + "tqdm", "trackingprotection", "turbopack", + "turtledemo", "udeps", "unlisten", "unminimize", "unrs", "urlencoding", + "urllib", + "venv", "vercel", "VERYSILENT", "webgl", diff --git a/nodecar/package.json b/nodecar/package.json index 07c5087..276fff5 100644 --- a/nodecar/package.json +++ b/nodecar/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "", "main": "dist/index.js", + "bin": "dist/index.js", "scripts": { "watch": "nodemon --exec ts-node --esm ./src/index.ts --watch src", "dev": "node --loader ts-node/esm ./src/index.ts", @@ -21,13 +22,14 @@ "author": "", "license": "AGPL-3.0", "dependencies": { - "@types/node": "^24.0.10", + "@types/node": "^24.1.0", "@yao-pkg/pkg": "^6.5.1", - "camoufox-js": "^0.6.0", + "camoufox-js": "^0.6.1", "commander": "^14.0.0", - "dotenv": "^17.0.1", + "dotenv": "^17.2.0", "get-port": "^7.1.0", "nodemon": "^3.1.10", + "playwright-core": "^1.54.1", "proxy-chain": "^2.5.9", "tmp": "^0.2.3", "ts-node": "^10.9.2", diff --git a/nodecar/src/camoufox-launcher.ts b/nodecar/src/camoufox-launcher.ts index f287698..8d54f33 100644 --- a/nodecar/src/camoufox-launcher.ts +++ b/nodecar/src/camoufox-launcher.ts @@ -1,16 +1,4 @@ -import { spawn } from "child_process"; -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; - -export interface CamoufoxConfig { - id: string; - pid?: number; - executablePath: string; - profilePath: string; - url?: string; - options: CamoufoxLaunchOptions; -} +import { launchOptions } from "camoufox-js"; export interface CamoufoxLaunchOptions { // Operating system to use for fingerprint generation @@ -92,412 +80,84 @@ export interface CamoufoxLaunchOptions { }; } -// Store for active Camoufox processes -const activeCamoufoxProcesses = new Map(); - /** - * Generate a unique ID for the Camoufox instance + * Generate Camoufox configuration using camoufox-js-lsd */ -function generateCamoufoxId(): string { - return `camoufox_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; -} - -/** - * Save Camoufox configuration to storage - */ -function saveCamoufoxConfig(config: CamoufoxConfig): void { - try { - const configDir = path.join(os.tmpdir(), "nodecar_camoufox"); - if (!fs.existsSync(configDir)) { - fs.mkdirSync(configDir, { recursive: true }); - } - - const configFile = path.join(configDir, `${config.id}.json`); - fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); - activeCamoufoxProcesses.set(config.id, config); - } catch (error) { - console.error(`Failed to save Camoufox config: ${error}`); - } -} - -/** - * Load Camoufox configuration from storage - */ -function loadCamoufoxConfig(id: string): CamoufoxConfig | null { - try { - const configFile = path.join(os.tmpdir(), "nodecar_camoufox", `${id}.json`); - if (fs.existsSync(configFile)) { - const config = JSON.parse(fs.readFileSync(configFile, "utf8")); - activeCamoufoxProcesses.set(id, config); - return config; - } - } catch (error) { - console.error(`Failed to load Camoufox config: ${error}`); - } - return null; -} - -/** - * Delete Camoufox configuration from storage - */ -function deleteCamoufoxConfig(id: string): boolean { - try { - const configFile = path.join(os.tmpdir(), "nodecar_camoufox", `${id}.json`); - if (fs.existsSync(configFile)) { - fs.unlinkSync(configFile); - } - activeCamoufoxProcesses.delete(id); - return true; - } catch (error) { - console.error(`Failed to delete Camoufox config: ${error}`); - return false; - } -} - -/** - * Load all Camoufox configurations on startup - */ -function loadAllCamoufoxConfigs(): void { - try { - const configDir = path.join(os.tmpdir(), "nodecar_camoufox"); - if (fs.existsSync(configDir)) { - const files = fs.readdirSync(configDir); - for (const file of files) { - if (file.endsWith(".json")) { - const id = path.basename(file, ".json"); - loadCamoufoxConfig(id); - } - } - } - } catch (error) { - console.error(`Failed to load Camoufox configs: ${error}`); - } -} - -/** - * Check if a process is still running - */ -function isProcessRunning(pid: number): boolean { - try { - process.kill(pid, 0); - return true; - } catch (error) { - return false; - } -} - -/** - * Convert Camoufox options to command line arguments - */ -function buildCamoufoxArgs( - options: CamoufoxLaunchOptions, - profilePath: string, - url?: string, -): string[] { - const args: string[] = []; - - // Always use profile - args.push("-profile", profilePath); - - // Cache enabled by default as requested - if (options.enable_cache !== false) { - // Cache is enabled by default in Camoufox, no special args needed - } - - // Headless mode - if (options.headless) { - args.push("-headless"); - } - - // No remote for security (anti-detect) - args.push("-no-remote"); - - // Custom Firefox user preferences will be written to user.js in profile - - // Additional custom args - if (options.args) { - args.push(...options.args); - } - - // URL to open - if (url) { - args.push(url); - } - - return args; -} - -/** - * Create user.js file with Camoufox preferences - */ -function createUserJs( - profilePath: string, - options: CamoufoxLaunchOptions, -): void { - const preferences: string[] = []; - - // Anti-detect preferences - preferences.push('user_pref("privacy.resistFingerprinting", true);'); - preferences.push( - 'user_pref("privacy.resistFingerprinting.letterboxing", true);', - ); - preferences.push('user_pref("privacy.trackingprotection.enabled", true);'); - - // Disable telemetry and data collection - preferences.push( - 'user_pref("datareporting.healthreport.uploadEnabled", false);', - ); - preferences.push( - 'user_pref("datareporting.policy.dataSubmissionEnabled", false);', - ); - preferences.push('user_pref("toolkit.telemetry.enabled", false);'); - preferences.push('user_pref("toolkit.telemetry.unified", false);'); - - // Block options - if (options.block_images) { - preferences.push('user_pref("permissions.default.image", 2);'); - } - - if (options.block_webrtc) { - preferences.push('user_pref("media.peerconnection.enabled", false);'); - preferences.push('user_pref("media.navigator.enabled", false);'); - } - - if (options.block_webgl) { - preferences.push('user_pref("webgl.disabled", true);'); - preferences.push('user_pref("webgl.disable-extensions", true);'); - } - - // COOP settings - if (options.disable_coop) { - preferences.push( - 'user_pref("browser.tabs.remote.useCrossOriginOpenerPolicy", false);', - ); - } - - // Locale settings - if (options.locale) { - const localeStr = Array.isArray(options.locale) - ? options.locale[0] - : options.locale; - preferences.push(`user_pref("intl.locale.requested", "${localeStr}");`); - preferences.push(`user_pref("general.useragent.locale", "${localeStr}");`); - } - - // Timezone - if (options.timezone) { - preferences.push( - `user_pref("privacy.resistFingerprinting.timezone", "${options.timezone}");`, - ); - } - - // Custom Firefox preferences - if (options.firefox_user_prefs) { - for (const [key, value] of Object.entries(options.firefox_user_prefs)) { - if (typeof value === "string") { - preferences.push(`user_pref("${key}", "${value}");`); - } else if (typeof value === "boolean") { - preferences.push(`user_pref("${key}", ${value});`); - } else if (typeof value === "number") { - preferences.push(`user_pref("${key}", ${value});`); - } - } - } - - // Proxy settings - if (options.proxy) { - if (typeof options.proxy === "string") { - // Parse proxy URL - try { - const proxyUrl = new URL(options.proxy); - const port = - parseInt(proxyUrl.port) || - (proxyUrl.protocol === "https:" ? 443 : 80); - - if (proxyUrl.protocol.startsWith("socks")) { - preferences.push('user_pref("network.proxy.type", 1);'); - preferences.push( - `user_pref("network.proxy.socks", "${proxyUrl.hostname}");`, - ); - preferences.push(`user_pref("network.proxy.socks_port", ${port});`); - if (proxyUrl.protocol === "socks5:") { - preferences.push('user_pref("network.proxy.socks_version", 5);'); - } else { - preferences.push('user_pref("network.proxy.socks_version", 4);'); - } - } else { - preferences.push('user_pref("network.proxy.type", 1);'); - preferences.push( - `user_pref("network.proxy.http", "${proxyUrl.hostname}");`, - ); - preferences.push(`user_pref("network.proxy.http_port", ${port});`); - preferences.push( - `user_pref("network.proxy.ssl", "${proxyUrl.hostname}");`, - ); - preferences.push(`user_pref("network.proxy.ssl_port", ${port});`); - } - - if (proxyUrl.username && proxyUrl.password) { - // Note: Basic auth for proxies is handled differently in modern Firefox - preferences.push( - 'user_pref("network.proxy.allow_hijacking_localhost", true);', - ); - } - } catch (error) { - console.error(`Invalid proxy URL: ${options.proxy}`); - } - } - } - - // Geolocation - if (options.geolocation) { - preferences.push('user_pref("geo.enabled", true);'); - preferences.push( - `user_pref("geo.wifi.uri", "data:application/json,{\\"location\\": {\\"lat\\": ${options.geolocation.latitude}, \\"lng\\": ${options.geolocation.longitude}}, \\"accuracy\\": ${options.geolocation.accuracy || 100}}");`, - ); - } else { - preferences.push('user_pref("geo.enabled", false);'); - } - - // Write user.js file - const userJsPath = path.join(profilePath, "user.js"); - fs.writeFileSync(userJsPath, preferences.join("\n")); -} - -/** - * Launch Camoufox browser with specified options - */ -export async function launchCamoufox( - executablePath: string, - profilePath: string, +export async function generateCamoufoxConfig( options: CamoufoxLaunchOptions = {}, - url?: string, -): Promise { - const id = generateCamoufoxId(); - - // Ensure profile directory exists - if (!fs.existsSync(profilePath)) { - fs.mkdirSync(profilePath, { recursive: true }); - } - - // Create user.js with preferences - createUserJs(profilePath, options); - - // Build command line arguments - const args = buildCamoufoxArgs(options, profilePath, url); - - // Prepare environment variables - const env = { - ...process.env, - ...options.env, - }; - - // Handle virtual display - if (options.virtual_display) { - env.DISPLAY = options.virtual_display; - } - - // Launch the process - const child = spawn(executablePath, args, { - env: env as NodeJS.ProcessEnv, - detached: true, - stdio: options.debug ? "inherit" : "ignore", - }); - - if (!child.pid) { - throw new Error("Failed to launch Camoufox process"); - } - - const config: CamoufoxConfig = { - id, - pid: child.pid, - executablePath, - profilePath, - url, - options, - }; - - // Save configuration - saveCamoufoxConfig(config); - - // Handle process exit - child.on("exit", (code, signal) => { - console.log( - `Camoufox process ${child.pid} exited with code ${code}, signal ${signal}`, - ); - deleteCamoufoxConfig(id); - }); - - child.on("error", (error) => { - console.error(`Camoufox process error: ${error}`); - deleteCamoufoxConfig(id); - }); - - // Detach the child process so it can continue running independently - child.unref(); - - return config; -} - -/** - * Stop a Camoufox process by ID - */ -export async function stopCamoufox(id: string): Promise { - const config = activeCamoufoxProcesses.get(id) || loadCamoufoxConfig(id); - - if (!config || !config.pid) { - return false; - } - +): Promise { try { - if (isProcessRunning(config.pid)) { - process.kill(config.pid, "SIGTERM"); + // Convert our options to camoufox-js-lsd format + const camoufoxOptions: any = {}; - // Wait a moment for graceful shutdown - await new Promise((resolve) => setTimeout(resolve, 2000)); + // Map our options to camoufox-js-lsd format + if (options.os) camoufoxOptions.os = options.os; + if (options.block_images !== undefined) + camoufoxOptions.block_images = options.block_images; + if (options.block_webrtc !== undefined) + camoufoxOptions.block_webrtc = options.block_webrtc; + if (options.block_webgl !== undefined) + camoufoxOptions.block_webgl = options.block_webgl; + if (options.disable_coop !== undefined) + camoufoxOptions.disable_coop = options.disable_coop; + if (options.geoip !== undefined) camoufoxOptions.geoip = options.geoip; + if (options.humanize !== undefined) + camoufoxOptions.humanize = options.humanize; + if (options.locale) camoufoxOptions.locale = options.locale; + if (options.addons) camoufoxOptions.addons = options.addons; + if (options.fonts) camoufoxOptions.fonts = options.fonts; + if (options.custom_fonts_only !== undefined) + camoufoxOptions.custom_fonts_only = options.custom_fonts_only; + if (options.exclude_addons) + camoufoxOptions.exclude_addons = options.exclude_addons; + if (options.screen) camoufoxOptions.screen = options.screen; + if (options.window) camoufoxOptions.window = options.window; + if (options.fingerprint) camoufoxOptions.fingerprint = options.fingerprint; + if (options.ff_version !== undefined) + camoufoxOptions.ff_version = options.ff_version; + if (options.headless !== undefined) + camoufoxOptions.headless = options.headless; + if (options.main_world_eval !== undefined) + camoufoxOptions.main_world_eval = options.main_world_eval; + if (options.executable_path) + camoufoxOptions.executable_path = options.executable_path; + if (options.firefox_user_prefs) + camoufoxOptions.firefox_user_prefs = options.firefox_user_prefs; + if (options.proxy) camoufoxOptions.proxy = options.proxy; + if (options.enable_cache !== undefined) + camoufoxOptions.enable_cache = options.enable_cache; + if (options.args) camoufoxOptions.args = options.args; + if (options.env) camoufoxOptions.env = options.env; + if (options.debug !== undefined) camoufoxOptions.debug = options.debug; + if (options.virtual_display) + camoufoxOptions.virtual_display = options.virtual_display; + if (options.webgl_config) + camoufoxOptions.webgl_config = options.webgl_config; - // Force kill if still running - if (isProcessRunning(config.pid)) { - process.kill(config.pid, "SIGKILL"); - } + // Handle custom options that might need mapping + if (options.timezone) { + // If timezone is provided directly, we can set it in the generated config + // This will be handled after generation + } + if (options.country) { + // Similar for country + } + if (options.geolocation) { + // Handle geolocation coordinates } - deleteCamoufoxConfig(id); - return true; + // Generate the configuration using camoufox-js-lsd + const generatedConfig = await launchOptions(camoufoxOptions); + + // Apply any custom overrides + if (options.timezone) { + generatedConfig.env = generatedConfig.env || {}; + // The timezone will be handled in the CAMOU_CONFIG environment variable + } + + return generatedConfig; } catch (error) { - console.error(`Failed to stop Camoufox process: ${error}`); - return false; + console.error(`Failed to generate Camoufox config: ${error}`); + throw error; } } - -/** - * List all Camoufox processes - */ -export function listCamoufoxProcesses(): any[] { - loadAllCamoufoxConfigs(); - - // Filter out dead processes - const activeConfigs: any[] = []; - - for (const [id, config] of activeCamoufoxProcesses) { - if (config.pid && isProcessRunning(config.pid)) { - // Return in snake_case format for Rust compatibility - activeConfigs.push({ - id: config.id, - pid: config.pid, - executable_path: config.executablePath, - profile_path: config.profilePath, - url: config.url, - options: config.options, - }); - } else { - // Clean up dead processes - deleteCamoufoxConfig(id); - } - } - - return activeConfigs; -} - -// Load existing configurations on module initialization -loadAllCamoufoxConfigs(); diff --git a/nodecar/src/index.ts b/nodecar/src/index.ts index 7606f22..636cd9b 100644 --- a/nodecar/src/index.ts +++ b/nodecar/src/index.ts @@ -1,9 +1,5 @@ import { program } from "commander"; -import { - launchCamoufox, - listCamoufoxProcesses, - stopCamoufox, -} from "./camoufox-launcher"; +import { generateCamoufoxConfig } from "./camoufox-launcher.js"; import { startProxyProcess, stopAllProxyProcesses, @@ -71,7 +67,7 @@ program "Error: Either --upstream URL or --host, --proxy-port, and --type are required", ); console.log( - "Example: proxy start --host datacenter.proxyempire.io --proxy-port 9000 --type http --username user --password pass", + "Example: proxy start --host proxy.example.com --proxy-port 9000 --type http --username user --password pass", ); process.exit(1); return; @@ -154,14 +150,10 @@ program } }); -// Command for Camoufox browser orchestrator +// Command for generating Camoufox configuration program - .command("camoufox") - .argument("", "launch, stop, list, or open-url for Camoufox browser") - .requiredOption("--executable-path ", "path to Camoufox executable") - .requiredOption("--profile-path ", "path to browser profile directory") - .option("--url ", "URL to open") - .option("--id ", "Camoufox instance ID (for stop/open-url actions)") + .command("camoufox-config") + .argument("", "generate Camoufox configuration") // Operating system fingerprinting .option( @@ -239,22 +231,13 @@ program // Firefox preferences .option("--firefox-prefs ", "Firefox user preferences (JSON string)") - .description("launch and manage Camoufox browser orchestrator instances") + .description("generate Camoufox configuration using camoufox-js") .action(async (action: string, options: any) => { try { - if (action === "launch") { - // Validate required options - if (!options.executablePath || !options.profilePath) { - console.error( - "Error: --executable-path and --profile-path are required for launch", - ); - process.exit(1); - return; - } - + if (action === "generate") { // Build Camoufox options const camoufoxOptions: any = { - enable_cache: !options.disableCache, // Cache enabled by default as requested + enable_cache: !options.disableCache, // Cache enabled by default }; // OS fingerprinting @@ -358,70 +341,19 @@ program } } - // Launch Camoufox - const config = await launchCamoufox( - options.executablePath, - options.profilePath, - camoufoxOptions, - options.url, - ); - - // Output the configuration as JSON for the Rust side to parse - console.log( - JSON.stringify({ - id: config.id, - pid: config.pid, - executable_path: config.executablePath, - profile_path: config.profilePath, - url: config.url, - }), - ); + // Generate configuration + const config = await generateCamoufoxConfig(camoufoxOptions); + // Output the configuration as JSON + console.log(JSON.stringify(config, null, 2)); process.exit(0); - } else if (action === "stop") { - if (!options.id) { - console.error("Error: --id is required for stop action"); - process.exit(1); - return; - } - - const success = await stopCamoufox(options.id); - console.log(JSON.stringify({ success })); - process.exit(0); - } else if (action === "list") { - const processes = listCamoufoxProcesses(); - // Convert camelCase to snake_case for Rust compatibility - const rustCompatibleProcesses = processes.map((process) => ({ - id: process.id, - pid: process.pid, - executable_path: process.executablePath, - profile_path: process.profilePath, - url: process.url, - })); - console.log(JSON.stringify(rustCompatibleProcesses)); - process.exit(0); - } else if (action === "open-url") { - if (!options.id || !options.url) { - console.error( - "Error: --id and --url are required for open-url action", - ); - process.exit(1); - return; - } - - // This would require implementing URL opening in existing instance - // For now, we'll return an error as this feature would need additional implementation - console.error("open-url action is not yet implemented"); - process.exit(1); } else { - console.error( - "Invalid action. Use 'launch', 'stop', 'list', or 'open-url'", - ); + console.error("Invalid action. Use 'generate'"); process.exit(1); } } catch (error: unknown) { console.error( - `Camoufox command failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`, + `Camoufox config generation failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`, ); process.exit(1); } diff --git a/package.json b/package.json index b5b3200..7dc2df5 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "tw-animate-css": "^1.3.5", "typescript": "~5.8.3" }, - "packageManager": "pnpm@10.11.1", + "packageManager": "pnpm@10.13.1", "lint-staged": { "**/*.{js,jsx,ts,tsx,json,css,md}": [ "biome check --fix" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e86361f..739ffaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,19 +139,19 @@ importers: nodecar: dependencies: '@types/node': - specifier: ^24.0.10 - version: 24.0.13 + specifier: ^24.1.0 + version: 24.1.0 '@yao-pkg/pkg': specifier: ^6.5.1 - version: 6.5.1(encoding@0.1.13) + version: 6.6.0(encoding@0.1.13) camoufox-js: - specifier: ^0.6.0 - version: 0.6.0(encoding@0.1.13)(playwright-core@1.54.0) + specifier: ^0.6.1 + version: 0.6.1(encoding@0.1.13)(playwright-core@1.54.1) commander: specifier: ^14.0.0 version: 14.0.0 dotenv: - specifier: ^17.0.1 + specifier: ^17.2.0 version: 17.2.0 get-port: specifier: ^7.1.0 @@ -159,6 +159,9 @@ importers: nodemon: specifier: ^3.1.10 version: 3.1.10 + playwright-core: + specifier: ^1.54.1 + version: 1.54.1 proxy-chain: specifier: ^2.5.9 version: 2.5.9 @@ -167,7 +170,7 @@ importers: version: 0.2.3 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@24.0.13)(typescript@5.8.3) + version: 10.9.2(@types/node@24.1.0)(typescript@5.8.3) typescript: specifier: ^5.8.3 version: 5.8.3 @@ -269,8 +272,8 @@ packages: resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.0': - resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} + '@babel/types@7.28.1': + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} engines: {node: '>=6.9.0'} '@biomejs/biome@2.1.1': @@ -333,158 +336,158 @@ packages: '@emnapi/runtime@1.4.4': resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==} - '@esbuild/aix-ppc64@0.25.6': - resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.6': - resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.6': - resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.6': - resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.6': - resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.6': - resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.6': - resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.6': - resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.6': - resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.6': - resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.6': - resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.6': - resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.6': - resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.6': - resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.6': - resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.6': - resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.6': - resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.6': - resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.6': - resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.6': - resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.6': - resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.6': - resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.6': - resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.6': - resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.6': - resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.6': - resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1119,103 +1122,103 @@ packages: '@rolldown/pluginutils@1.0.0-beta.19': resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} - '@rollup/rollup-android-arm-eabi@4.45.0': - resolution: {integrity: sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==} + '@rollup/rollup-android-arm-eabi@4.45.1': + resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.45.0': - resolution: {integrity: sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==} + '@rollup/rollup-android-arm64@4.45.1': + resolution: {integrity: sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.45.0': - resolution: {integrity: sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==} + '@rollup/rollup-darwin-arm64@4.45.1': + resolution: {integrity: sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.45.0': - resolution: {integrity: sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==} + '@rollup/rollup-darwin-x64@4.45.1': + resolution: {integrity: sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.45.0': - resolution: {integrity: sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==} + '@rollup/rollup-freebsd-arm64@4.45.1': + resolution: {integrity: sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.45.0': - resolution: {integrity: sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==} + '@rollup/rollup-freebsd-x64@4.45.1': + resolution: {integrity: sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.45.0': - resolution: {integrity: sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==} + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.45.0': - resolution: {integrity: sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==} + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.45.0': - resolution: {integrity: sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==} + '@rollup/rollup-linux-arm64-gnu@4.45.1': + resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.45.0': - resolution: {integrity: sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==} + '@rollup/rollup-linux-arm64-musl@4.45.1': + resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.45.0': - resolution: {integrity: sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==} + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.45.0': - resolution: {integrity: sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==} + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.45.0': - resolution: {integrity: sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==} + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.45.0': - resolution: {integrity: sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==} + '@rollup/rollup-linux-riscv64-musl@4.45.1': + resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.45.0': - resolution: {integrity: sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==} + '@rollup/rollup-linux-s390x-gnu@4.45.1': + resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.45.0': - resolution: {integrity: sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==} + '@rollup/rollup-linux-x64-gnu@4.45.1': + resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.45.0': - resolution: {integrity: sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==} + '@rollup/rollup-linux-x64-musl@4.45.1': + resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.45.0': - resolution: {integrity: sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==} + '@rollup/rollup-win32-arm64-msvc@4.45.1': + resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.45.0': - resolution: {integrity: sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==} + '@rollup/rollup-win32-ia32-msvc@4.45.1': + resolution: {integrity: sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.45.0': - resolution: {integrity: sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==} + '@rollup/rollup-win32-x64-msvc@4.45.1': + resolution: {integrity: sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==} cpu: [x64] os: [win32] @@ -1454,6 +1457,9 @@ packages: '@types/node@24.0.13': resolution: {integrity: sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==} + '@types/node@24.1.0': + resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==} + '@types/react-dom@19.1.6': resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} peerDependencies: @@ -1471,12 +1477,12 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - '@yao-pkg/pkg-fetch@3.5.23': - resolution: {integrity: sha512-rn45sqVQSkcJNSBdTnYze3n+kyub4CN8aiWYlPgA9yp9FZeEF+BlpL68kSIm3HaVuANniF+7RBMH5DkC4zlHZA==} + '@yao-pkg/pkg-fetch@3.5.24': + resolution: {integrity: sha512-FPESCH1uXCYui6jeDp2aayWuFHR39w+uU1r88nI6JWRvPYOU64cHPUV/p6GSFoQdpna7ip92HnrZKbBC60l0gA==} hasBin: true - '@yao-pkg/pkg@6.5.1': - resolution: {integrity: sha512-z6XlySYfnqfm1AfVlBN8A3yeAQniIwL7TKQfDCGsswYSVYLt2snbRefQYsfQQ3pw5lVXrZdLqgTjzaqID9IkWA==} + '@yao-pkg/pkg@6.6.0': + resolution: {integrity: sha512-3/oiaSm7fS0Fc7dzp22r9B7vFaguGhO9vERgEReRYj2EUzdi5ssyYhe1uYJG4ec/dmo2GG6RRHOUAT8savl79Q==} engines: {node: '>=18.0.0'} hasBin: true @@ -1543,8 +1549,8 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - aproba@2.0.0: - resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} are-we-there-yet@3.0.1: resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} @@ -1614,8 +1620,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camoufox-js@0.6.0: - resolution: {integrity: sha512-QoCZoDdkXFRdHV4IgthBTogFCTL2p9QKFyEHGE6q1vGm0a9P3ael5zLXGPYiEzI5ByH2nlH4SV6CjQIJKjbxaA==} + camoufox-js@0.6.1: + resolution: {integrity: sha512-fwJdDIx9gdpdVqeupe7WxrOJNjXg1CbpTQsEyK/wX1iEDYq2QAzKV80dMm89/6RgUSfZvk3zw5jMidMtuBm0OQ==} hasBin: true peerDependencies: playwright-core: '*' @@ -1830,8 +1836,8 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - esbuild@0.25.6: - resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} engines: {node: '>=18'} hasBin: true @@ -1865,8 +1871,8 @@ packages: resolution: {integrity: sha512-Sfd2cLmvVVkzVYvC8+DZWiawquksAbAzrx9+AllpLOg8qlH8votU/Ozx59Z+/70GGQDlEsk48zo7FF5S5vuTEA==} engines: {node: '>=16.0.0'} - form-data@4.0.3: - resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} from2@2.3.0: @@ -2505,8 +2511,8 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} pidtree@0.6.0: @@ -2514,8 +2520,8 @@ packages: engines: {node: '>=0.10'} hasBin: true - playwright-core@1.54.0: - resolution: {integrity: sha512-uiWpWaJh3R3etpJ0QrpligEMl62Dk1iSAB6NUXylvmQz+e3eipXHDHvOvydDAssb5Oqo0E818qdn0L9GcJSTyA==} + playwright-core@1.54.1: + resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==} engines: {node: '>=18'} hasBin: true @@ -2655,8 +2661,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.45.0: - resolution: {integrity: sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==} + rollup@4.45.1: + resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2735,8 +2741,8 @@ packages: resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} engines: {node: '>= 14'} - socks@2.8.5: - resolution: {integrity: sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==} + socks@2.8.6: + resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} sonner@2.0.6: @@ -3107,7 +3113,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 convert-source-map: 2.0.0 debug: 4.4.1(supports-color@5.5.0) gensync: 1.0.0-beta.2 @@ -3119,7 +3125,7 @@ snapshots: '@babel/generator@7.28.0': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@jridgewell/gen-mapping': 0.3.12 '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 @@ -3137,7 +3143,7 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 transitivePeerDependencies: - supports-color @@ -3161,11 +3167,11 @@ snapshots: '@babel/helpers@7.27.6': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@babel/parser@7.28.0': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': dependencies: @@ -3183,7 +3189,7 @@ snapshots: dependencies: '@babel/code-frame': 7.27.1 '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@babel/traverse@7.28.0': dependencies: @@ -3192,12 +3198,12 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 debug: 4.4.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color - '@babel/types@7.28.0': + '@babel/types@7.28.1': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -3246,82 +3252,82 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.25.6': + '@esbuild/aix-ppc64@0.25.8': optional: true - '@esbuild/android-arm64@0.25.6': + '@esbuild/android-arm64@0.25.8': optional: true - '@esbuild/android-arm@0.25.6': + '@esbuild/android-arm@0.25.8': optional: true - '@esbuild/android-x64@0.25.6': + '@esbuild/android-x64@0.25.8': optional: true - '@esbuild/darwin-arm64@0.25.6': + '@esbuild/darwin-arm64@0.25.8': optional: true - '@esbuild/darwin-x64@0.25.6': + '@esbuild/darwin-x64@0.25.8': optional: true - '@esbuild/freebsd-arm64@0.25.6': + '@esbuild/freebsd-arm64@0.25.8': optional: true - '@esbuild/freebsd-x64@0.25.6': + '@esbuild/freebsd-x64@0.25.8': optional: true - '@esbuild/linux-arm64@0.25.6': + '@esbuild/linux-arm64@0.25.8': optional: true - '@esbuild/linux-arm@0.25.6': + '@esbuild/linux-arm@0.25.8': optional: true - '@esbuild/linux-ia32@0.25.6': + '@esbuild/linux-ia32@0.25.8': optional: true - '@esbuild/linux-loong64@0.25.6': + '@esbuild/linux-loong64@0.25.8': optional: true - '@esbuild/linux-mips64el@0.25.6': + '@esbuild/linux-mips64el@0.25.8': optional: true - '@esbuild/linux-ppc64@0.25.6': + '@esbuild/linux-ppc64@0.25.8': optional: true - '@esbuild/linux-riscv64@0.25.6': + '@esbuild/linux-riscv64@0.25.8': optional: true - '@esbuild/linux-s390x@0.25.6': + '@esbuild/linux-s390x@0.25.8': optional: true - '@esbuild/linux-x64@0.25.6': + '@esbuild/linux-x64@0.25.8': optional: true - '@esbuild/netbsd-arm64@0.25.6': + '@esbuild/netbsd-arm64@0.25.8': optional: true - '@esbuild/netbsd-x64@0.25.6': + '@esbuild/netbsd-x64@0.25.8': optional: true - '@esbuild/openbsd-arm64@0.25.6': + '@esbuild/openbsd-arm64@0.25.8': optional: true - '@esbuild/openbsd-x64@0.25.6': + '@esbuild/openbsd-x64@0.25.8': optional: true - '@esbuild/openharmony-arm64@0.25.6': + '@esbuild/openharmony-arm64@0.25.8': optional: true - '@esbuild/sunos-x64@0.25.6': + '@esbuild/sunos-x64@0.25.8': optional: true - '@esbuild/win32-arm64@0.25.6': + '@esbuild/win32-arm64@0.25.8': optional: true - '@esbuild/win32-ia32@0.25.6': + '@esbuild/win32-ia32@0.25.8': optional: true - '@esbuild/win32-x64@0.25.6': + '@esbuild/win32-x64@0.25.8': optional: true '@floating-ui/core@1.7.2': @@ -3912,64 +3918,64 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.19': {} - '@rollup/rollup-android-arm-eabi@4.45.0': + '@rollup/rollup-android-arm-eabi@4.45.1': optional: true - '@rollup/rollup-android-arm64@4.45.0': + '@rollup/rollup-android-arm64@4.45.1': optional: true - '@rollup/rollup-darwin-arm64@4.45.0': + '@rollup/rollup-darwin-arm64@4.45.1': optional: true - '@rollup/rollup-darwin-x64@4.45.0': + '@rollup/rollup-darwin-x64@4.45.1': optional: true - '@rollup/rollup-freebsd-arm64@4.45.0': + '@rollup/rollup-freebsd-arm64@4.45.1': optional: true - '@rollup/rollup-freebsd-x64@4.45.0': + '@rollup/rollup-freebsd-x64@4.45.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.45.0': + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.45.0': + '@rollup/rollup-linux-arm-musleabihf@4.45.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.45.0': + '@rollup/rollup-linux-arm64-gnu@4.45.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.45.0': + '@rollup/rollup-linux-arm64-musl@4.45.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.45.0': + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.45.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.45.0': + '@rollup/rollup-linux-riscv64-gnu@4.45.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.45.0': + '@rollup/rollup-linux-riscv64-musl@4.45.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.45.0': + '@rollup/rollup-linux-s390x-gnu@4.45.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.45.0': + '@rollup/rollup-linux-x64-gnu@4.45.1': optional: true - '@rollup/rollup-linux-x64-musl@4.45.0': + '@rollup/rollup-linux-x64-musl@4.45.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.45.0': + '@rollup/rollup-win32-arm64-msvc@4.45.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.45.0': + '@rollup/rollup-win32-ia32-msvc@4.45.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.45.0': + '@rollup/rollup-win32-x64-msvc@4.45.1': optional: true '@sindresorhus/is@4.6.0': {} @@ -4139,23 +4145,23 @@ snapshots: '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.7 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@types/babel__template@7.4.4': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@types/babel__traverse@7.20.7': dependencies: - '@babel/types': 7.28.0 + '@babel/types': 7.28.1 '@types/estree@1.0.8': {} @@ -4163,13 +4169,17 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: - '@types/node': 24.0.13 - form-data: 4.0.3 + '@types/node': 24.1.0 + form-data: 4.0.4 '@types/node@24.0.13': dependencies: undici-types: 7.8.0 + '@types/node@24.1.0': + dependencies: + undici-types: 7.8.0 + '@types/react-dom@19.1.6(@types/react@19.1.8)': dependencies: '@types/react': 19.1.8 @@ -4192,7 +4202,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@yao-pkg/pkg-fetch@3.5.23(encoding@0.1.13)': + '@yao-pkg/pkg-fetch@3.5.24(encoding@0.1.13)': dependencies: https-proxy-agent: 5.0.1 node-fetch: 2.7.0(encoding@0.1.13) @@ -4205,17 +4215,17 @@ snapshots: - encoding - supports-color - '@yao-pkg/pkg@6.5.1(encoding@0.1.13)': + '@yao-pkg/pkg@6.6.0(encoding@0.1.13)': dependencies: '@babel/generator': 7.28.0 '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 - '@yao-pkg/pkg-fetch': 3.5.23(encoding@0.1.13) + '@babel/types': 7.28.1 + '@yao-pkg/pkg-fetch': 3.5.24(encoding@0.1.13) into-stream: 6.0.0 minimist: 1.2.8 multistream: 4.1.0 picocolors: 1.1.1 - picomatch: 4.0.2 + picomatch: 4.0.3 prebuild-install: 7.1.3 resolve: 1.22.10 stream-meter: 1.0.4 @@ -4289,7 +4299,7 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - aproba@2.0.0: + aproba@2.1.0: optional: true are-we-there-yet@3.0.1: @@ -4382,7 +4392,7 @@ snapshots: callsites@3.1.0: {} - camoufox-js@0.6.0(encoding@0.1.13)(playwright-core@1.54.0): + camoufox-js@0.6.1(encoding@0.1.13)(playwright-core@1.54.1): dependencies: adm-zip: 0.5.16 commander: 13.1.0 @@ -4391,7 +4401,7 @@ snapshots: js-yaml: 4.1.0 language-tags: 2.1.0 maxmind: 4.3.28 - playwright-core: 1.54.0 + playwright-core: 1.54.1 progress: 2.0.3 sqlite3: 5.1.7 ua-parser-js: 2.0.4(encoding@0.1.13) @@ -4596,34 +4606,34 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - esbuild@0.25.6: + esbuild@0.25.8: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.6 - '@esbuild/android-arm': 0.25.6 - '@esbuild/android-arm64': 0.25.6 - '@esbuild/android-x64': 0.25.6 - '@esbuild/darwin-arm64': 0.25.6 - '@esbuild/darwin-x64': 0.25.6 - '@esbuild/freebsd-arm64': 0.25.6 - '@esbuild/freebsd-x64': 0.25.6 - '@esbuild/linux-arm': 0.25.6 - '@esbuild/linux-arm64': 0.25.6 - '@esbuild/linux-ia32': 0.25.6 - '@esbuild/linux-loong64': 0.25.6 - '@esbuild/linux-mips64el': 0.25.6 - '@esbuild/linux-ppc64': 0.25.6 - '@esbuild/linux-riscv64': 0.25.6 - '@esbuild/linux-s390x': 0.25.6 - '@esbuild/linux-x64': 0.25.6 - '@esbuild/netbsd-arm64': 0.25.6 - '@esbuild/netbsd-x64': 0.25.6 - '@esbuild/openbsd-arm64': 0.25.6 - '@esbuild/openbsd-x64': 0.25.6 - '@esbuild/openharmony-arm64': 0.25.6 - '@esbuild/sunos-x64': 0.25.6 - '@esbuild/win32-arm64': 0.25.6 - '@esbuild/win32-ia32': 0.25.6 - '@esbuild/win32-x64': 0.25.6 + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 escalade@3.2.0: {} @@ -4631,9 +4641,9 @@ snapshots: expand-template@2.0.3: {} - fdir@6.4.6(picomatch@4.0.2): + fdir@6.4.6(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 file-uri-to-path@1.0.0: {} @@ -4647,7 +4657,7 @@ snapshots: header-generator: 2.1.69 tslib: 2.8.1 - form-data@4.0.3: + form-data@4.0.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -4682,7 +4692,7 @@ snapshots: gauge@4.0.4: dependencies: - aproba: 2.0.0 + aproba: 2.1.0 color-support: 1.1.3 console-control-strings: 1.1.0 has-unicode: 2.0.1 @@ -5283,11 +5293,11 @@ snapshots: picomatch@2.3.1: {} - picomatch@4.0.2: {} + picomatch@4.0.3: {} pidtree@0.6.0: {} - playwright-core@1.54.0: {} + playwright-core@1.54.1: {} postcss@8.4.31: dependencies: @@ -5331,7 +5341,7 @@ snapshots: proxy-chain@2.5.9: dependencies: - socks: 2.8.5 + socks: 2.8.6 socks-proxy-agent: 8.0.5 tslib: 2.8.1 transitivePeerDependencies: @@ -5438,30 +5448,30 @@ snapshots: glob: 7.2.3 optional: true - rollup@4.45.0: + rollup@4.45.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.45.0 - '@rollup/rollup-android-arm64': 4.45.0 - '@rollup/rollup-darwin-arm64': 4.45.0 - '@rollup/rollup-darwin-x64': 4.45.0 - '@rollup/rollup-freebsd-arm64': 4.45.0 - '@rollup/rollup-freebsd-x64': 4.45.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.45.0 - '@rollup/rollup-linux-arm-musleabihf': 4.45.0 - '@rollup/rollup-linux-arm64-gnu': 4.45.0 - '@rollup/rollup-linux-arm64-musl': 4.45.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.45.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.45.0 - '@rollup/rollup-linux-riscv64-gnu': 4.45.0 - '@rollup/rollup-linux-riscv64-musl': 4.45.0 - '@rollup/rollup-linux-s390x-gnu': 4.45.0 - '@rollup/rollup-linux-x64-gnu': 4.45.0 - '@rollup/rollup-linux-x64-musl': 4.45.0 - '@rollup/rollup-win32-arm64-msvc': 4.45.0 - '@rollup/rollup-win32-ia32-msvc': 4.45.0 - '@rollup/rollup-win32-x64-msvc': 4.45.0 + '@rollup/rollup-android-arm-eabi': 4.45.1 + '@rollup/rollup-android-arm64': 4.45.1 + '@rollup/rollup-darwin-arm64': 4.45.1 + '@rollup/rollup-darwin-x64': 4.45.1 + '@rollup/rollup-freebsd-arm64': 4.45.1 + '@rollup/rollup-freebsd-x64': 4.45.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.45.1 + '@rollup/rollup-linux-arm-musleabihf': 4.45.1 + '@rollup/rollup-linux-arm64-gnu': 4.45.1 + '@rollup/rollup-linux-arm64-musl': 4.45.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.45.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-musl': 4.45.1 + '@rollup/rollup-linux-s390x-gnu': 4.45.1 + '@rollup/rollup-linux-x64-gnu': 4.45.1 + '@rollup/rollup-linux-x64-musl': 4.45.1 + '@rollup/rollup-win32-arm64-msvc': 4.45.1 + '@rollup/rollup-win32-ia32-msvc': 4.45.1 + '@rollup/rollup-win32-x64-msvc': 4.45.1 fsevents: 2.3.3 safe-buffer@5.1.2: {} @@ -5552,7 +5562,7 @@ snapshots: dependencies: agent-base: 6.0.2 debug: 4.4.1(supports-color@5.5.0) - socks: 2.8.5 + socks: 2.8.6 transitivePeerDependencies: - supports-color optional: true @@ -5561,11 +5571,11 @@ snapshots: dependencies: agent-base: 7.1.4 debug: 4.4.1(supports-color@5.5.0) - socks: 2.8.5 + socks: 2.8.6 transitivePeerDependencies: - supports-color - socks@2.8.5: + socks@2.8.6: dependencies: ip-address: 9.0.5 smart-buffer: 4.2.0 @@ -5700,8 +5710,8 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 tmp@0.2.3: {} @@ -5713,14 +5723,14 @@ snapshots: tr46@0.0.3: {} - ts-node@10.9.2(@types/node@24.0.13)(typescript@5.8.3): + ts-node@10.9.2(@types/node@24.1.0)(typescript@5.8.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 24.0.13 + '@types/node': 24.1.0 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -5819,11 +5829,11 @@ snapshots: vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0): dependencies: - esbuild: 0.25.6 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + esbuild: 0.25.8 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.45.0 + rollup: 4.45.1 tinyglobby: 0.2.14 optionalDependencies: '@types/node': 24.0.13 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a2761b4..19bfe1c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -542,8 +542,10 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -965,6 +967,7 @@ version = "0.7.2" dependencies = [ "async-trait", "base64 0.22.1", + "chrono", "core-foundation 0.10.1", "directories", "futures-util", @@ -991,6 +994,7 @@ dependencies = [ "tokio", "tower", "tower-http", + "url", "uuid", "windows", "winreg", @@ -2571,7 +2575,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.3.0", "proc-macro2", "quote", "syn 2.0.101", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ca9a7dd..4537d01 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -30,13 +30,16 @@ tauri-plugin-dialog = "2" tauri-plugin-macos-permissions = "2" directories = "6" reqwest = { version = "0.12", features = ["json", "stream"] } -tokio = { version = "1", features = ["full"] } +tokio = { version = "1", features = ["full", "sync"] } sysinfo = "0.36" lazy_static = "1.4" base64 = "0.22" async-trait = "0.1" futures-util = "0.3" + uuid = { version = "1.0", features = ["v4", "serde"] } +url = "2.5" +chrono = { version = "0.4", features = ["serde"] } [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies] tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index e13530e..2246652 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -198,8 +198,8 @@ mod linux { } BrowserType::Camoufox => { vec![ - browser_subdir.join("camoufox"), browser_subdir.join("camoufox-bin"), + browser_subdir.join("camoufox"), ] } _ => vec![], diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 47d0768..301f991 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -13,7 +13,7 @@ use crate::browser::{create_browser, BrowserType, ProxySettings}; use crate::browser_version_service::{ BrowserVersionInfo, BrowserVersionService, BrowserVersionsResult, }; -use crate::camoufox::CamoufoxConfig; +use crate::camoufox_direct::CamoufoxConfig; use crate::download::{DownloadProgress, Downloader}; use crate::downloaded_browsers::DownloadedBrowsersRegistry; use crate::extraction::Extractor; @@ -1782,7 +1782,7 @@ impl BrowserRunner { url: Option, local_proxy_settings: Option<&ProxySettings>, ) -> Result> { - // Handle camoufox profiles specially + // Handle camoufox profiles specially using only the direct launcher if profile.browser == "camoufox" { if let Some(mut camoufox_config) = profile.camoufox_config.clone() { // Handle proxy settings for camoufox @@ -1840,11 +1840,29 @@ impl BrowserRunner { } } - // Use the camoufox launcher - let camoufox_result = crate::camoufox::launch_camoufox_profile( + // Use the existing config or create a test config if none exists + let final_config = if camoufox_config.timezone.is_some() + || camoufox_config.screen_min_width.is_some() + || camoufox_config.window_width.is_some() + { + camoufox_config.clone() + } else { + // No meaningful config provided, use test config to ensure anti-fingerprinting works + println!("No Camoufox configuration provided, using test configuration"); + let mut test_config = + crate::camoufox_direct::CamoufoxDirectLauncher::create_test_config(); + // Preserve any proxy settings from the original config + test_config.proxy = camoufox_config.proxy.clone(); + test_config.headless = camoufox_config.headless; + test_config.debug = Some(true); // Enable debug for troubleshooting + test_config + }; + + // Use the direct camoufox launcher + let camoufox_result = crate::camoufox_direct::launch_camoufox_profile_direct( app_handle.clone(), profile.clone(), - camoufox_config, + final_config, url, ) .await @@ -2046,9 +2064,10 @@ impl BrowserRunner { url: &str, _internal_proxy_settings: Option<&ProxySettings>, ) -> Result<(), Box> { - // Handle camoufox profiles specially + // Handle camoufox profiles specially using only the direct launcher if profile.browser == "camoufox" { - let camoufox_launcher = crate::camoufox::CamoufoxLauncher::new(app_handle.clone()); + let camoufox_launcher = + crate::camoufox_direct::CamoufoxDirectLauncher::new(app_handle.clone()); // Get the profile path based on the UUID let profiles_dir = self.get_profiles_dir(); @@ -2066,9 +2085,9 @@ impl BrowserRunner { profile.name ); - // For Camoufox, we need to launch a new instance with the URL since nodecar doesn't support - // opening URLs in existing instances. This is a limitation of the anti-detect architecture. - return Err("Camoufox does not support opening URLs in existing instances. Please close the browser and relaunch it with the new URL.".into()); + // For Camoufox, we need to launch a new instance with the URL since it doesn't support remote commands + // This is a limitation of Camoufox's architecture + return Err("Camoufox doesn't support opening URLs in existing instances. Please close the browser and launch again with the URL.".into()); } Ok(None) => { return Err("Camoufox browser is not running".into()); @@ -2240,7 +2259,7 @@ impl BrowserRunner { } BrowserType::Camoufox => { // This should never be reached due to the early return above, but handle it just in case - Err("Camoufox does not support opening URLs in existing instances".into()) + Err("Camoufox URL opening should be handled in the early return above".into()) } } } @@ -2447,77 +2466,8 @@ impl BrowserRunner { app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result> { - // Handle camoufox profiles specially using the camoufox launcher - if profile.browser == "camoufox" { - let camoufox_launcher = crate::camoufox::CamoufoxLauncher::new(app_handle.clone()); - - // Get the profile path based on the UUID - let profiles_dir = self.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - let profile_path_str = profile_data_path.to_string_lossy(); - - println!("Checking Camoufox status for profile: {}", profile.name); - println!("Profile UUID: {}", profile.id); - println!("Profile path: {profile_path_str}"); - - match camoufox_launcher - .find_camoufox_by_profile(&profile_path_str) - .await - { - Ok(Some(camoufox_process)) => { - // Found a running camoufox process for this profile - println!( - "Found running Camoufox process for profile {}: {:?}", - profile.name, camoufox_process - ); - - // Update the profile with the current PID if it's different - if let Some(pid) = camoufox_process.pid { - if profile.process_id != Some(pid) { - let mut updated_profile = profile.clone(); - updated_profile.process_id = Some(pid); - if let Err(e) = self.save_profile(&updated_profile) { - println!("Warning: Failed to update profile PID: {e}"); - } else { - // Emit profile update event to frontend - if let Err(e) = app_handle.emit("profile-updated", &updated_profile) { - println!("Warning: Failed to emit profile update event: {e}"); - } - } - } - } - - return Ok(true); - } - Ok(None) => { - // No running camoufox process found for this profile - println!( - "No running Camoufox process found for profile: {}", - profile.name - ); - - // Clear the PID if one was stored - if profile.process_id.is_some() { - let mut updated_profile = profile.clone(); - updated_profile.process_id = None; - if let Err(e) = self.save_profile(&updated_profile) { - println!("Warning: Failed to clear profile PID: {e}"); - } else { - // Emit profile update event to frontend - if let Err(e) = app_handle.emit("profile-updated", &updated_profile) { - println!("Warning: Failed to emit profile update event: {e}"); - } - } - } - - return Ok(false); - } - Err(e) => { - println!("Error checking Camoufox status: {e}"); - return Ok(false); - } - } - } + // Handle camoufox profiles using the same fast approach as other browsers + // No special handling needed - camoufox uses the same process checking logic // For non-camoufox browsers, use the existing logic let mut inner_profile = profile.clone(); @@ -2535,12 +2485,13 @@ impl BrowserRunner { let profile_data_path_str = profile_data_path.to_string_lossy(); let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); - // For Firefox-based browsers, check for exact profile path match + // For Firefox-based browsers (including camoufox), check for exact profile path match if profile.browser == "tor-browser" || profile.browser == "firefox" || profile.browser == "firefox-developer" || profile.browser == "mullvad-browser" || profile.browser == "zen" + || profile.browser == "camoufox" { arg == profile_data_path_str || arg == format!("-profile={profile_data_path_str}") @@ -2583,6 +2534,7 @@ impl BrowserRunner { && !exe_name.contains("developer") && !exe_name.contains("tor") && !exe_name.contains("mullvad") + && !exe_name.contains("camoufox") } "firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"), "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser"), @@ -2590,6 +2542,13 @@ impl BrowserRunner { "zen" => exe_name.contains("zen"), "chromium" => exe_name.contains("chromium"), "brave" => exe_name.contains("brave"), + "camoufox" => { + exe_name.contains("camoufox") + || (exe_name.contains("firefox") + && cmd + .iter() + .any(|arg| arg.to_str().unwrap_or("").contains("camoufox"))) + } _ => false, }; @@ -2662,66 +2621,99 @@ impl BrowserRunner { Ok(is_running) } + pub fn update_camoufox_config( + &self, + profile_name: &str, + config: crate::camoufox_direct::CamoufoxConfig, + ) -> Result<(), Box> { + // Find the profile by name + let profiles = self.list_profiles()?; + let mut profile = profiles + .into_iter() + .find(|p| p.name == profile_name) + .ok_or_else(|| format!("Profile {profile_name} not found"))?; + + // Check if the browser is currently running + if profile.process_id.is_some() { + return Err( + "Cannot update Camoufox configuration while browser is running. Please stop the browser first.".into(), + ); + } + + // Update the Camoufox configuration + profile.camoufox_config = Some(config); + + // Save the updated profile + self.save_profile(&profile)?; + + println!("Camoufox configuration updated for profile '{profile_name}'."); + + Ok(()) + } + pub async fn kill_browser_process( &self, app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result<(), Box> { - // Handle camoufox profiles specially + // Handle camoufox profiles specially using only the direct launcher if profile.browser == "camoufox" { - let camoufox_launcher = crate::camoufox::CamoufoxLauncher::new(app_handle.clone()); + let camoufox_launcher = + crate::camoufox_direct::CamoufoxDirectLauncher::new(app_handle.clone()); - // Get the profile path based on the UUID - let profiles_dir = self.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - let profile_path_str = profile_data_path.to_string_lossy(); - - println!( - "Attempting to kill Camoufox process for profile: {}", - profile.name - ); - println!("Profile UUID: {}", profile.id); - println!("Profile path: {profile_path_str}"); - - match camoufox_launcher - .find_camoufox_by_profile(&profile_path_str) - .await - { - Ok(Some(camoufox_process)) => { - println!( - "Found running Camoufox process for profile {}: {:?}", - profile.name, camoufox_process - ); - - // Stop the camoufox process using the launcher - match camoufox_launcher.stop_camoufox(&camoufox_process.id).await { - Ok(stopped) => { - if stopped { - println!( - "Successfully stopped Camoufox process: {}", - camoufox_process.id - ); - } else { - println!("Failed to stop Camoufox process: {}", camoufox_process.id); - return Err("Failed to stop Camoufox process".into()); - } - } - Err(e) => { - println!("Error stopping Camoufox process: {e}"); - return Err(format!("Error stopping Camoufox process: {e}").into()); + // Try to stop by PID first (faster) + if let Some(stored_pid) = profile.process_id { + match camoufox_launcher + .stop_camoufox(&stored_pid.to_string()) + .await + { + Ok(stopped) => { + if stopped { + println!("Successfully stopped Camoufox process by PID: {stored_pid}"); + } else { + println!("Failed to stop Camoufox process by PID: {stored_pid}"); } } + Err(e) => { + println!("Error stopping Camoufox process by PID: {e}"); + } } - Ok(None) => { - println!( - "No running Camoufox process found for profile: {}", - profile.name - ); - // Process might already be stopped, just clear the PID - } - Err(e) => { - println!("Error finding Camoufox process: {e}"); - return Err(format!("Error finding Camoufox process: {e}").into()); + } else { + // Fallback: search by profile path + let profiles_dir = self.get_profiles_dir(); + let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_path_str = profile_data_path.to_string_lossy(); + + match camoufox_launcher + .find_camoufox_by_profile(&profile_path_str) + .await + { + Ok(Some(camoufox_process)) => { + match camoufox_launcher.stop_camoufox(&camoufox_process.id).await { + Ok(stopped) => { + if stopped { + println!( + "Successfully stopped Camoufox process: {}", + camoufox_process.id + ); + } else { + println!("Failed to stop Camoufox process: {}", camoufox_process.id); + } + } + Err(e) => { + println!("Error stopping Camoufox process: {e}"); + } + } + } + Ok(None) => { + println!( + "No running Camoufox process found for profile: {}", + profile.name + ); + } + Err(e) => { + println!("Error finding Camoufox process: {e}"); + } } } @@ -2766,6 +2758,7 @@ impl BrowserRunner { && !exe_name.contains("developer") && !exe_name.contains("tor") && !exe_name.contains("mullvad") + && !exe_name.contains("camoufox") } "firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"), "mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser"), @@ -2773,6 +2766,13 @@ impl BrowserRunner { "zen" => exe_name.contains("zen"), "chromium" => exe_name.contains("chromium"), "brave" => exe_name.contains("brave"), + "camoufox" => { + exe_name.contains("camoufox") + || (exe_name.contains("firefox") + && cmd + .iter() + .any(|arg| arg.to_str().unwrap_or("").contains("camoufox"))) + } _ => false, }; @@ -3180,34 +3180,6 @@ impl BrowserRunner { files_exist } - - /// Update camoufox configuration for a profile - pub fn update_camoufox_config( - &self, - profile_name: &str, - config: CamoufoxConfig, - ) -> Result<(), Box> { - let mut profiles = self.list_profiles()?; - - // Find the profile to update - let profile = profiles - .iter_mut() - .find(|p| p.name == profile_name) - .ok_or_else(|| format!("Profile '{profile_name}' not found"))?; - - // Ensure the profile is a camoufox profile - if profile.browser != "camoufox" { - return Err(format!("Profile '{profile_name}' is not a camoufox profile").into()); - } - - // Update the camoufox configuration - profile.camoufox_config = Some(config); - - // Save the updated profile - self.save_profile(profile)?; - - Ok(()) - } } impl BrowserProfile { diff --git a/src-tauri/src/camoufox.rs b/src-tauri/src/camoufox.rs deleted file mode 100644 index b36471d..0000000 --- a/src-tauri/src/camoufox.rs +++ /dev/null @@ -1,607 +0,0 @@ -use crate::browser_runner::BrowserProfile; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use tauri::AppHandle; -use tauri_plugin_shell::ShellExt; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CamoufoxConfig { - pub os: Option>, - pub block_images: Option, - pub block_webrtc: Option, - pub block_webgl: Option, - pub disable_coop: Option, - pub geoip: Option, // Can be String or bool - pub country: Option, - pub timezone: Option, - pub latitude: Option, - pub longitude: Option, - pub humanize: Option, - pub humanize_duration: Option, - pub headless: Option, - pub locale: Option>, - pub addons: Option>, - pub fonts: Option>, - pub custom_fonts_only: Option, - pub exclude_addons: Option>, - pub screen_min_width: Option, - pub screen_max_width: Option, - pub screen_min_height: Option, - pub screen_max_height: Option, - pub window_width: Option, - pub window_height: Option, - pub ff_version: Option, - pub main_world_eval: Option, - pub webgl_vendor: Option, - pub webgl_renderer: Option, - pub proxy: Option, - pub enable_cache: Option, - pub virtual_display: Option, - pub debug: Option, - pub additional_args: Option>, - pub env_vars: Option>, - pub firefox_prefs: Option>, -} - -impl Default for CamoufoxConfig { - fn default() -> Self { - Self { - os: None, - block_images: None, - block_webrtc: None, - block_webgl: None, - disable_coop: None, - geoip: None, - country: None, - timezone: None, - latitude: None, - longitude: None, - humanize: None, - humanize_duration: None, - headless: None, - locale: None, - addons: None, - fonts: None, - custom_fonts_only: None, - exclude_addons: None, - screen_min_width: None, - screen_max_width: None, - screen_min_height: None, - screen_max_height: None, - window_width: None, - window_height: None, - ff_version: None, - main_world_eval: None, - webgl_vendor: None, - webgl_renderer: None, - proxy: None, - enable_cache: Some(true), // Cache enabled by default - virtual_display: None, - debug: None, - additional_args: None, - env_vars: None, - firefox_prefs: None, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct CamoufoxLaunchResult { - pub id: String, - pub pid: Option, - #[serde(alias = "executable_path")] - pub executablePath: String, - #[serde(alias = "profile_path")] - pub profilePath: String, - pub url: Option, -} - -pub struct CamoufoxLauncher { - app_handle: AppHandle, -} - -impl CamoufoxLauncher { - pub fn new(app_handle: AppHandle) -> Self { - Self { app_handle } - } - - /// Launch Camoufox browser with the specified configuration - pub async fn launch_camoufox( - &self, - executable_path: &str, - profile_path: &str, - config: &CamoufoxConfig, - url: Option<&str>, - ) -> Result> { - println!("Launching Camoufox with executable: {executable_path}"); - println!("Profile path: {profile_path}"); - println!("URL: {url:?}"); - - // Use Tauri's sidecar to call nodecar - let mut sidecar = self - .app_handle - .shell() - .sidecar("nodecar") - .map_err(|e| format!("Failed to create nodecar sidecar: {e}"))? - .arg("camoufox") - .arg("launch") - .arg("--executable-path") - .arg(executable_path) - .arg("--profile-path") - .arg(profile_path); - - // Add URL if provided - if let Some(url) = url { - sidecar = sidecar.arg("--url").arg(url); - } - - // Add configuration options - if let Some(os_list) = &config.os { - sidecar = sidecar.arg("--os").arg(os_list.join(",")); - } - - if config.block_images.unwrap_or(false) { - sidecar = sidecar.arg("--block-images"); - } - - if config.block_webrtc.unwrap_or(false) { - sidecar = sidecar.arg("--block-webrtc"); - } - - if config.block_webgl.unwrap_or(false) { - sidecar = sidecar.arg("--block-webgl"); - } - - if config.disable_coop.unwrap_or(false) { - sidecar = sidecar.arg("--disable-coop"); - } - - if let Some(geoip) = &config.geoip { - match geoip { - serde_json::Value::String(s) => { - sidecar = sidecar.arg("--geoip").arg(s); - } - serde_json::Value::Bool(b) => { - sidecar = sidecar - .arg("--geoip") - .arg(if *b { "auto" } else { "false" }); - } - _ => { - sidecar = sidecar.arg("--geoip").arg(geoip.to_string()); - } - } - } - - if let Some(country) = &config.country { - sidecar = sidecar.arg("--country").arg(country); - } - - if let Some(timezone) = &config.timezone { - sidecar = sidecar.arg("--timezone").arg(timezone); - } - - if let Some(latitude) = config.latitude { - if let Some(longitude) = config.longitude { - sidecar = sidecar.arg("--latitude").arg(latitude.to_string()); - sidecar = sidecar.arg("--longitude").arg(longitude.to_string()); - } - } - - if let Some(humanize) = config.humanize { - if humanize { - if let Some(duration) = config.humanize_duration { - sidecar = sidecar.arg("--humanize").arg(duration.to_string()); - } else { - sidecar = sidecar.arg("--humanize"); - } - } - } - - if config.headless.unwrap_or(false) { - sidecar = sidecar.arg("--headless"); - } - - if let Some(locale_list) = &config.locale { - sidecar = sidecar.arg("--locale").arg(locale_list.join(",")); - } - - if let Some(addons_list) = &config.addons { - sidecar = sidecar.arg("--addons").arg(addons_list.join(",")); - } - - if let Some(fonts_list) = &config.fonts { - sidecar = sidecar.arg("--fonts").arg(fonts_list.join(",")); - } - - if config.custom_fonts_only.unwrap_or(false) { - sidecar = sidecar.arg("--custom-fonts-only"); - } - - if let Some(exclude_addons_list) = &config.exclude_addons { - sidecar = sidecar - .arg("--exclude-addons") - .arg(exclude_addons_list.join(",")); - } - - // Screen size configuration - if let Some(width) = config.screen_min_width { - sidecar = sidecar.arg("--screen-min-width").arg(width.to_string()); - } - - if let Some(width) = config.screen_max_width { - sidecar = sidecar.arg("--screen-max-width").arg(width.to_string()); - } - - if let Some(height) = config.screen_min_height { - sidecar = sidecar.arg("--screen-min-height").arg(height.to_string()); - } - - if let Some(height) = config.screen_max_height { - sidecar = sidecar.arg("--screen-max-height").arg(height.to_string()); - } - - if let Some(width) = config.window_width { - sidecar = sidecar.arg("--window-width").arg(width.to_string()); - } - - if let Some(height) = config.window_height { - sidecar = sidecar.arg("--window-height").arg(height.to_string()); - } - - // Advanced options - if let Some(ff_version) = config.ff_version { - sidecar = sidecar.arg("--ff-version").arg(ff_version.to_string()); - } - - if config.main_world_eval.unwrap_or(false) { - sidecar = sidecar.arg("--main-world-eval"); - } - - if let Some(vendor) = &config.webgl_vendor { - if let Some(renderer) = &config.webgl_renderer { - sidecar = sidecar.arg("--webgl-vendor").arg(vendor); - sidecar = sidecar.arg("--webgl-renderer").arg(renderer); - } - } - - if let Some(proxy) = &config.proxy { - sidecar = sidecar.arg("--proxy").arg(proxy); - } - - // Cache is enabled by default, only add flag if disabled - if !config.enable_cache.unwrap_or(true) { - sidecar = sidecar.arg("--disable-cache"); - } - - if let Some(virtual_display) = &config.virtual_display { - sidecar = sidecar.arg("--virtual-display").arg(virtual_display); - } - - if config.debug.unwrap_or(false) { - sidecar = sidecar.arg("--debug"); - } - - if let Some(args) = &config.additional_args { - sidecar = sidecar.arg("--args").arg(args.join(",")); - } - - if let Some(env_vars) = &config.env_vars { - let env_json = serde_json::to_string(env_vars) - .map_err(|e| format!("Failed to serialize environment variables: {e}"))?; - sidecar = sidecar.arg("--env").arg(env_json); - } - - if let Some(firefox_prefs) = &config.firefox_prefs { - let prefs_json = serde_json::to_string(firefox_prefs) - .map_err(|e| format!("Failed to serialize Firefox preferences: {e}"))?; - sidecar = sidecar.arg("--firefox-prefs").arg(prefs_json); - } - - // Execute the command - println!("Executing nodecar command..."); - let output = sidecar - .output() - .await - .map_err(|e| format!("Failed to execute nodecar command: {e}"))?; - - // Check the command status first - if !output.status.success() { - let error_msg = String::from_utf8_lossy(&output.stderr); - let stdout_msg = String::from_utf8_lossy(&output.stdout); - return Err( - format!( - "Failed to launch Camoufox: Command failed with status {:?}\nstderr: {}\nstdout: {}", - output.status, error_msg, stdout_msg - ) - .into(), - ); - } - - // Parse the JSON response - let stdout = String::from_utf8_lossy(&output.stdout); - println!("Nodecar stdout: {stdout}"); - - // Try to parse the JSON response - let result: CamoufoxLaunchResult = serde_json::from_str(&stdout) - .map_err(|e| format!("Failed to parse nodecar response as JSON: {e}\nResponse: {stdout}"))?; - - println!("Successfully launched Camoufox with ID: {}", result.id); - - Ok(result) - } - - /// Stop a Camoufox process by ID - pub async fn stop_camoufox( - &self, - id: &str, - ) -> Result> { - println!("Stopping Camoufox process with ID: {id}"); - - // First, we need to find the process to get its executable and profile paths - let processes = self.list_camoufox_processes().await?; - let target_process = processes.iter().find(|p| p.id == id); - - if let Some(process) = target_process { - println!( - "Found process to stop: executable={}, profile={}", - process.executablePath, process.profilePath - ); - - let sidecar = self - .app_handle - .shell() - .sidecar("nodecar") - .map_err(|e| format!("Failed to create nodecar sidecar: {e}"))? - .arg("camoufox") - .arg("stop") - .arg("--executable-path") - .arg(&process.executablePath) - .arg("--profile-path") - .arg(&process.profilePath) - .arg("--id") - .arg(id); - - let output = sidecar - .output() - .await - .map_err(|e| format!("Failed to execute nodecar stop command: {e}"))?; - - if !output.status.success() { - let error_msg = String::from_utf8_lossy(&output.stderr); - let stdout_msg = String::from_utf8_lossy(&output.stdout); - println!("Failed to stop Camoufox process - stderr: {error_msg}, stdout: {stdout_msg}"); - return Err(format!("Failed to stop Camoufox process: {error_msg}").into()); - } - - let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); - println!("Stop command result: {stdout}"); - - // Parse the JSON response which contains a "success" field - let response: serde_json::Value = serde_json::from_str(&stdout) - .map_err(|e| format!("Failed to parse stop response as JSON: {e}\nResponse: {stdout}"))?; - - let success = response - .get("success") - .and_then(|v| v.as_bool()) - .ok_or_else(|| { - format!("Invalid response format - missing or invalid 'success' field: {stdout}") - })?; - - if success { - println!("Successfully stopped Camoufox process: {id}"); - } else { - println!("Failed to stop Camoufox process: {id} (process may not exist)"); - } - - Ok(success) - } else { - println!("Camoufox process with ID {id} not found in running processes"); - // If we can't find the process, it might already be stopped - Ok(false) - } - } - - /// List all Camoufox processes - pub async fn list_camoufox_processes( - &self, - ) -> Result, Box> { - println!("Listing Camoufox processes..."); - - // For the list command, we need to provide dummy executable-path and profile-path - // even though they're not used by the list action - let sidecar = self - .app_handle - .shell() - .sidecar("nodecar") - .map_err(|e| format!("Failed to create nodecar sidecar: {e}"))? - .arg("camoufox") - .arg("list") - .arg("--executable-path") - .arg("/dummy/path") // Dummy path since list doesn't use it - .arg("--profile-path") - .arg("/dummy/profile"); // Dummy path since list doesn't use it - - let output = sidecar - .output() - .await - .map_err(|e| format!("Failed to execute nodecar list command: {e}"))?; - - if !output.status.success() { - let error_msg = String::from_utf8_lossy(&output.stderr); - return Err(format!("Failed to list Camoufox processes: {error_msg}").into()); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - println!("List command result: {stdout}"); - - // Parse the response as an array of process info - let processes: Vec = - serde_json::from_str(&stdout).map_err(|e| format!("Failed to parse list response: {e}"))?; - - // Convert to CamoufoxLaunchResult format - let mut results = Vec::new(); - for process in processes { - // Handle both camelCase and snake_case formats from nodecar - let id = process.get("id").and_then(|v| v.as_str()); - - // Try both formats for executable path - let executable_path = process - .get("executable_path") - .and_then(|v| v.as_str()) - .or_else(|| process.get("executablePath").and_then(|v| v.as_str())); - - // Try both formats for profile path - let profile_path = process - .get("profile_path") - .and_then(|v| v.as_str()) - .or_else(|| process.get("profilePath").and_then(|v| v.as_str())); - - if let (Some(id), Some(executable_path), Some(profile_path)) = - (id, executable_path, profile_path) - { - let pid = process - .get("pid") - .and_then(|v| v.as_u64()) - .map(|v| v as u32); - - let url = process - .get("url") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - results.push(CamoufoxLaunchResult { - id: id.to_string(), - pid, - executablePath: executable_path.to_string(), - profilePath: profile_path.to_string(), - url, - }); - } else { - println!("Skipping malformed process entry: {process:?}"); - } - } - - println!("Parsed {} valid Camoufox processes", results.len()); - Ok(results) - } - - /// Find Camoufox process by profile path (for integration with browser_runner) - pub async fn find_camoufox_by_profile( - &self, - profile_path: &str, - ) -> Result, Box> { - println!("Looking for Camoufox process with profile path: {profile_path}"); - - let processes = self.list_camoufox_processes().await?; - println!("Found {} running Camoufox processes", processes.len()); - - for process in &processes { - println!( - "Checking process with profile path: {}", - process.profilePath - ); - } - - // Convert both paths to canonical form for comparison - let target_path = std::path::Path::new(profile_path) - .canonicalize() - .unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf()); - - for process in &processes { - println!( - "Comparing target path: {} with process path: {}", - target_path.display(), - process.profilePath - ); - - // Try multiple comparison methods - let process_path = std::path::Path::new(&process.profilePath) - .canonicalize() - .unwrap_or_else(|_| std::path::Path::new(&process.profilePath).to_path_buf()); - - // Method 1: Canonical path comparison - if process_path == target_path { - println!("Found match using canonical path comparison"); - return Ok(Some(process.clone())); - } - - // Method 2: Direct string comparison - if process.profilePath == profile_path { - println!("Found match using direct string comparison"); - return Ok(Some(process.clone())); - } - - // Method 3: Compare as strings after canonicalization - if process_path.to_string_lossy() == target_path.to_string_lossy() { - println!("Found match using canonical string comparison"); - return Ok(Some(process.clone())); - } - - // Method 4: Compare file names if full paths don't match - if let (Some(process_file), Some(target_file)) = - (process_path.file_name(), target_path.file_name()) - { - if process_file == target_file { - // If the parent directories also match, it's likely the same profile - if let (Some(process_parent), Some(target_parent)) = - (process_path.parent(), target_path.parent()) - { - if process_parent == target_parent { - println!("Found match using parent directory and file name comparison"); - return Ok(Some(process.clone())); - } - } - } - } - - // Method 5: Check if either path contains the other (for symlinks or different representations) - let process_path_str = process_path.to_string_lossy(); - let target_path_str = target_path.to_string_lossy(); - - if process_path_str.contains(target_path_str.as_ref()) - || target_path_str.contains(process_path_str.as_ref()) - { - println!("Found match using path containment check"); - return Ok(Some(process.clone())); - } - } - - println!("No matching Camoufox process found for profile path: {profile_path}"); - Ok(None) - } -} - -pub async fn launch_camoufox_profile( - app_handle: AppHandle, - profile: BrowserProfile, - config: CamoufoxConfig, - url: Option, -) -> Result { - let launcher = CamoufoxLauncher::new(app_handle); - - // Get the executable path for Camoufox - let browser_runner = crate::browser_runner::BrowserRunner::new(); - let binaries_dir = browser_runner.get_binaries_dir(); - let browser_dir = binaries_dir.join("camoufox").join(&profile.version); - - // Get executable path - let browser = crate::browser::create_browser(crate::browser::BrowserType::Camoufox); - let executable_path = browser - .get_executable_path(&browser_dir) - .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?; - - // Get profile path - let profiles_dir = browser_runner.get_profiles_dir(); - let profile_path = profile.get_profile_data_path(&profiles_dir); - - launcher - .launch_camoufox( - &executable_path.to_string_lossy(), - &profile_path.to_string_lossy(), - &config, - url.as_deref(), - ) - .await - .map_err(|e| format!("Failed to launch Camoufox: {e}")) -} diff --git a/src-tauri/src/camoufox_direct.rs b/src-tauri/src/camoufox_direct.rs new file mode 100644 index 0000000..ac2d5d8 --- /dev/null +++ b/src-tauri/src/camoufox_direct.rs @@ -0,0 +1,1334 @@ +use crate::browser_runner::BrowserProfile; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::Path; +use std::process::{Child, Command, Stdio}; +use std::sync::Arc; +use sysinfo::{Pid, System}; +use tauri::AppHandle; +use tokio::sync::Mutex as AsyncMutex; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CamoufoxConfig { + pub os: Option>, + pub block_images: Option, + pub block_webrtc: Option, + pub block_webgl: Option, + pub disable_coop: Option, + pub geoip: Option, // Can be String or bool + pub country: Option, + pub timezone: Option, + pub latitude: Option, + pub longitude: Option, + pub humanize: Option, + pub humanize_duration: Option, + pub headless: Option, + pub locale: Option>, + pub addons: Option>, + pub fonts: Option>, + pub custom_fonts_only: Option, + pub exclude_addons: Option>, + pub screen_min_width: Option, + pub screen_max_width: Option, + pub screen_min_height: Option, + pub screen_max_height: Option, + pub window_width: Option, + pub window_height: Option, + pub ff_version: Option, + pub main_world_eval: Option, + pub webgl_vendor: Option, + pub webgl_renderer: Option, + pub proxy: Option, + pub enable_cache: Option, + pub virtual_display: Option, + pub debug: Option, + pub additional_args: Option>, + pub env_vars: Option>, + pub firefox_prefs: Option>, +} + +impl Default for CamoufoxConfig { + fn default() -> Self { + Self { + os: None, + block_images: None, + block_webrtc: None, + block_webgl: None, + disable_coop: None, + geoip: None, + country: None, + timezone: None, + latitude: None, + longitude: None, + humanize: None, + humanize_duration: None, + headless: None, + locale: None, + addons: None, + fonts: None, + custom_fonts_only: None, + exclude_addons: None, + screen_min_width: None, + screen_max_width: None, + screen_min_height: None, + screen_max_height: None, + window_width: None, + window_height: None, + ff_version: None, + main_world_eval: None, + webgl_vendor: None, + webgl_renderer: None, + proxy: None, + enable_cache: Some(true), // Cache enabled by default + virtual_display: None, + debug: None, + additional_args: None, + env_vars: None, + firefox_prefs: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct CamoufoxLaunchResult { + pub id: String, + pub pid: Option, + #[serde(alias = "executable_path")] + pub executablePath: String, + #[serde(alias = "profile_path")] + pub profilePath: String, + pub url: Option, +} + +#[derive(Debug)] +struct CamoufoxInstance { + pid: u32, + executable_path: String, + profile_path: String, + url: Option, + _child: Option, // Keep handle to prevent zombie processes +} + +struct CamoufoxDirectLauncherInner { + instances: HashMap, +} + +pub struct CamoufoxDirectLauncher { + inner: Arc>, +} + +// Global singleton instance +lazy_static::lazy_static! { + static ref GLOBAL_DIRECT_LAUNCHER: CamoufoxDirectLauncher = CamoufoxDirectLauncher::new_singleton(); +} + +impl CamoufoxDirectLauncher { + pub fn new(_app_handle: AppHandle) -> Self { + // Return a reference to the global singleton + GLOBAL_DIRECT_LAUNCHER.clone() + } + + pub fn new_singleton() -> Self { + Self { + inner: Arc::new(AsyncMutex::new(CamoufoxDirectLauncherInner { + instances: HashMap::new(), + })), + } + } + + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } + + /// Create a test configuration to verify anti-fingerprinting is working + pub fn create_test_config() -> CamoufoxConfig { + CamoufoxConfig { + // Core anti-fingerprinting settings + timezone: Some("Europe/London".to_string()), + screen_min_width: Some(1440), + screen_min_height: Some(900), + window_width: Some(1200), + window_height: Some(800), + + // Locale settings + locale: Some(vec!["en-GB".to_string(), "en-US".to_string()]), + + // WebGL spoofing + webgl_vendor: Some("Intel Inc.".to_string()), + webgl_renderer: Some("Intel Iris Pro OpenGL Engine".to_string()), + + // Geolocation spoofing (London coordinates) + latitude: Some(51.5074), + longitude: Some(-0.1278), + + // Font settings + fonts: Some(vec![ + "Arial".to_string(), + "Times New Roman".to_string(), + "Helvetica".to_string(), + "Georgia".to_string(), + ]), + custom_fonts_only: Some(true), + + // Humanization + humanize: Some(true), + humanize_duration: Some(2.0), + + // Blocking features + block_images: Some(false), // Don't block images for testing + block_webrtc: Some(true), + block_webgl: Some(false), // Don't block WebGL so we can test spoofing + + // Other settings + debug: Some(true), + enable_cache: Some(true), + headless: Some(false), // Not headless for testing + + ..Default::default() + } + } + + /// Generate Camoufox configuration using nodecar with camoufox-js-lsd + async fn generate_camoufox_config_with_nodecar( + &self, + config: &CamoufoxConfig, + ) -> Result> { + println!("Generating Camoufox configuration using nodecar with camoufox-js-lsd..."); + + // Build nodecar command arguments + let mut args = vec!["camoufox-config".to_string(), "generate".to_string()]; + + // Add configuration options + if let Some(os_list) = &config.os { + let os_str = os_list.join(","); + args.extend(["--os".to_string(), os_str]); + } + + if let Some(block_images) = config.block_images { + if block_images { + args.push("--block-images".to_string()); + } + } + + if let Some(block_webrtc) = config.block_webrtc { + if block_webrtc { + args.push("--block-webrtc".to_string()); + } + } + + if let Some(block_webgl) = config.block_webgl { + if block_webgl { + args.push("--block-webgl".to_string()); + } + } + + if let Some(disable_coop) = config.disable_coop { + if disable_coop { + args.push("--disable-coop".to_string()); + } + } + + if let Some(geoip) = &config.geoip { + match geoip { + serde_json::Value::Bool(true) => { + args.extend(["--geoip".to_string(), "auto".to_string()]); + } + serde_json::Value::String(ip) => { + args.extend(["--geoip".to_string(), ip.clone()]); + } + _ => {} + } + } + + if let Some(country) = &config.country { + args.extend(["--country".to_string(), country.clone()]); + } + + if let Some(timezone) = &config.timezone { + args.extend(["--timezone".to_string(), timezone.clone()]); + } + + if let Some(latitude) = config.latitude { + args.extend(["--latitude".to_string(), latitude.to_string()]); + } + + if let Some(longitude) = config.longitude { + args.extend(["--longitude".to_string(), longitude.to_string()]); + } + + if let Some(humanize) = config.humanize { + if humanize { + if let Some(duration) = config.humanize_duration { + args.extend(["--humanize".to_string(), duration.to_string()]); + } else { + args.push("--humanize".to_string()); + } + } + } + + if let Some(headless) = config.headless { + if headless { + args.push("--headless".to_string()); + } + } + + if let Some(locale_list) = &config.locale { + let locale_str = locale_list.join(","); + args.extend(["--locale".to_string(), locale_str]); + } + + if let Some(addons) = &config.addons { + let addons_str = addons.join(","); + args.extend(["--addons".to_string(), addons_str]); + } + + if let Some(fonts) = &config.fonts { + let fonts_str = fonts.join(","); + args.extend(["--fonts".to_string(), fonts_str]); + } + + if let Some(custom_fonts_only) = config.custom_fonts_only { + if custom_fonts_only { + args.push("--custom-fonts-only".to_string()); + } + } + + if let Some(exclude_addons) = &config.exclude_addons { + let exclude_str = exclude_addons.join(","); + args.extend(["--exclude-addons".to_string(), exclude_str]); + } + + if let Some(screen_min_width) = config.screen_min_width { + args.extend([ + "--screen-min-width".to_string(), + screen_min_width.to_string(), + ]); + } + + if let Some(screen_max_width) = config.screen_max_width { + args.extend([ + "--screen-max-width".to_string(), + screen_max_width.to_string(), + ]); + } + + if let Some(screen_min_height) = config.screen_min_height { + args.extend([ + "--screen-min-height".to_string(), + screen_min_height.to_string(), + ]); + } + + if let Some(screen_max_height) = config.screen_max_height { + args.extend([ + "--screen-max-height".to_string(), + screen_max_height.to_string(), + ]); + } + + if let Some(window_width) = config.window_width { + args.extend(["--window-width".to_string(), window_width.to_string()]); + } + + if let Some(window_height) = config.window_height { + args.extend(["--window-height".to_string(), window_height.to_string()]); + } + + if let Some(ff_version) = config.ff_version { + args.extend(["--ff-version".to_string(), ff_version.to_string()]); + } + + if let Some(main_world_eval) = config.main_world_eval { + if main_world_eval { + args.push("--main-world-eval".to_string()); + } + } + + if let Some(webgl_vendor) = &config.webgl_vendor { + args.extend(["--webgl-vendor".to_string(), webgl_vendor.clone()]); + } + + if let Some(webgl_renderer) = &config.webgl_renderer { + args.extend(["--webgl-renderer".to_string(), webgl_renderer.clone()]); + } + + if let Some(proxy) = &config.proxy { + args.extend(["--proxy".to_string(), proxy.clone()]); + } + + if let Some(enable_cache) = config.enable_cache { + if !enable_cache { + args.push("--disable-cache".to_string()); + } + } + + if let Some(virtual_display) = &config.virtual_display { + args.extend(["--virtual-display".to_string(), virtual_display.clone()]); + } + + if let Some(debug) = config.debug { + if debug { + args.push("--debug".to_string()); + } + } + + if let Some(additional_args) = &config.additional_args { + let args_str = additional_args.join(","); + args.extend(["--args".to_string(), args_str]); + } + + if let Some(env_vars) = &config.env_vars { + let env_json = serde_json::to_string(env_vars)?; + args.extend(["--env".to_string(), env_json]); + } + + if let Some(firefox_prefs) = &config.firefox_prefs { + let prefs_json = serde_json::to_string(firefox_prefs)?; + args.extend(["--firefox-prefs".to_string(), prefs_json]); + } + + // Get the nodecar binary path + let nodecar_path = self.get_nodecar_binary_path()?; + + println!( + "Executing nodecar command: {:?} with args: {:?}", + nodecar_path, args + ); + + // Execute nodecar command + let output = tokio::process::Command::new(nodecar_path) + .args(&args) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("nodecar camoufox-config failed: {stderr}").into()); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + println!("nodecar output: {}", stdout); + + // Parse the JSON output + let config_json: serde_json::Value = serde_json::from_str(&stdout) + .map_err(|e| format!("Failed to parse nodecar output as JSON: {e}"))?; + + Ok(config_json) + } + + /// Get the path to the nodecar binary + fn get_nodecar_binary_path( + &self, + ) -> Result> { + // Try to find nodecar binary in the same directory as the current executable + let current_exe = std::env::current_exe()?; + let exe_dir = current_exe + .parent() + .ok_or("Failed to get executable directory")?; + + // Check for nodecar in the same directory + let nodecar_path = exe_dir.join("nodecar"); + if nodecar_path.exists() { + return Ok(nodecar_path); + } + + // Check for nodecar with .exe extension on Windows + #[cfg(target_os = "windows")] + { + let nodecar_exe_path = exe_dir.join("nodecar.exe"); + if nodecar_exe_path.exists() { + return Ok(nodecar_exe_path); + } + } + + // Fallback to system PATH + Ok(std::path::PathBuf::from("nodecar")) + } + + /// Build the CAMOU_CONFIG JSON from CamoufoxConfig (fallback method) + fn build_camou_config(&self, config: &CamoufoxConfig) -> serde_json::Value { + let mut camou_config = serde_json::Map::new(); + + // Always set some basic anti-fingerprinting defaults to ensure the system works + camou_config.insert("debug".to_string(), serde_json::Value::Bool(true)); // Enable debug for troubleshooting + + // Set some default values that should always work to test the system + if config.timezone.is_none() { + camou_config.insert( + "timezone".to_string(), + serde_json::Value::String("America/New_York".to_string()), + ); + } + + // Set default screen size if not specified + if config.screen_min_width.is_none() { + camou_config.insert( + "screen.width".to_string(), + serde_json::Value::Number(1920.into()), + ); + camou_config.insert( + "screen.availWidth".to_string(), + serde_json::Value::Number(1920.into()), + ); + } + if config.screen_min_height.is_none() { + camou_config.insert( + "screen.height".to_string(), + serde_json::Value::Number(1080.into()), + ); + camou_config.insert( + "screen.availHeight".to_string(), + serde_json::Value::Number(1080.into()), + ); + } + + // Set default window size if not specified + if config.window_width.is_none() { + camou_config.insert( + "window.outerWidth".to_string(), + serde_json::Value::Number(1366.into()), + ); + camou_config.insert( + "window.innerWidth".to_string(), + serde_json::Value::Number(1350.into()), + ); + } + if config.window_height.is_none() { + camou_config.insert( + "window.outerHeight".to_string(), + serde_json::Value::Number(768.into()), + ); + camou_config.insert( + "window.innerHeight".to_string(), + serde_json::Value::Number(668.into()), + ); + } + + // Screen dimensions - use proper camoufox format + if let Some(width) = config.screen_min_width { + camou_config.insert( + "screen.width".to_string(), + serde_json::Value::Number(width.into()), + ); + camou_config.insert( + "screen.availWidth".to_string(), + serde_json::Value::Number(width.into()), + ); + } + if let Some(height) = config.screen_min_height { + camou_config.insert( + "screen.height".to_string(), + serde_json::Value::Number(height.into()), + ); + camou_config.insert( + "screen.availHeight".to_string(), + serde_json::Value::Number(height.into()), + ); + } + + // Window dimensions - use proper camoufox format + if let Some(width) = config.window_width { + camou_config.insert( + "window.outerWidth".to_string(), + serde_json::Value::Number(width.into()), + ); + camou_config.insert( + "window.innerWidth".to_string(), + serde_json::Value::Number((width.saturating_sub(16)).into()), // Account for scrollbar + ); + } + if let Some(height) = config.window_height { + camou_config.insert( + "window.outerHeight".to_string(), + serde_json::Value::Number(height.into()), + ); + camou_config.insert( + "window.innerHeight".to_string(), + serde_json::Value::Number((height.saturating_sub(100)).into()), // Account for browser chrome + ); + } + + // Geolocation - use proper camoufox format (colon notation) + if let Some(latitude) = config.latitude { + camou_config.insert( + "geolocation:latitude".to_string(), + serde_json::Value::Number( + serde_json::Number::from_f64(latitude).unwrap_or(serde_json::Number::from(0)), + ), + ); + } + if let Some(longitude) = config.longitude { + camou_config.insert( + "geolocation:longitude".to_string(), + serde_json::Value::Number( + serde_json::Number::from_f64(longitude).unwrap_or(serde_json::Number::from(0)), + ), + ); + } + + // Timezone - use proper camoufox format + if let Some(timezone) = &config.timezone { + camou_config.insert( + "timezone".to_string(), + serde_json::Value::String(timezone.clone()), + ); + } + + // Locale - use proper camoufox format (colon notation) + if let Some(locale_list) = &config.locale { + if let Some(first_locale) = locale_list.first() { + // Parse locale (e.g., "en-US" -> language: "en", region: "US") + let parts: Vec<&str> = first_locale.split('-').collect(); + if parts.len() >= 2 { + camou_config.insert( + "locale:language".to_string(), + serde_json::Value::String(parts[0].to_string()), + ); + camou_config.insert( + "locale:region".to_string(), + serde_json::Value::String(parts[1].to_string()), + ); + } + + // Set the full locale + camou_config.insert( + "locale:all".to_string(), + serde_json::Value::String(first_locale.clone()), + ); + + // Set navigator language properties + camou_config.insert( + "navigator.language".to_string(), + serde_json::Value::String(first_locale.clone()), + ); + + // Set Accept-Language header + camou_config.insert( + "headers.Accept-Language".to_string(), + serde_json::Value::String(first_locale.clone()), + ); + + // Convert to languages array for navigator.languages + let languages: Vec = locale_list + .iter() + .map(|l| serde_json::Value::String(l.clone())) + .collect(); + camou_config.insert( + "navigator.languages".to_string(), + serde_json::Value::Array(languages), + ); + } + } + + // WebGL - use proper camoufox format (colon notation) + if let Some(vendor) = &config.webgl_vendor { + camou_config.insert( + "webGl:vendor".to_string(), + serde_json::Value::String(vendor.clone()), + ); + } + if let Some(renderer) = &config.webgl_renderer { + camou_config.insert( + "webGl:renderer".to_string(), + serde_json::Value::String(renderer.clone()), + ); + } + + // Fonts - use proper camoufox format + if let Some(fonts) = &config.fonts { + let font_values: Vec = fonts + .iter() + .map(|f| serde_json::Value::String(f.clone())) + .collect(); + camou_config.insert("fonts".to_string(), serde_json::Value::Array(font_values)); + } + + // Custom fonts only + if let Some(custom_fonts_only) = config.custom_fonts_only { + camou_config.insert( + "customFontsOnly".to_string(), + serde_json::Value::Bool(custom_fonts_only), + ); + } + + // Humanization - use proper camoufox format (colon notation) + if let Some(humanize) = config.humanize { + camou_config.insert("humanize".to_string(), serde_json::Value::Bool(humanize)); + if let Some(duration) = config.humanize_duration { + camou_config.insert( + "humanize:maxTime".to_string(), + serde_json::Value::Number( + serde_json::Number::from_f64(duration * 1000.0).unwrap_or(serde_json::Number::from(0)), // Convert to milliseconds + ), + ); + } + } + + // Debug mode + if let Some(debug) = config.debug { + camou_config.insert("debug".to_string(), serde_json::Value::Bool(debug)); + } + + // Main world evaluation + if let Some(main_world_eval) = config.main_world_eval { + camou_config.insert( + "allowMainWorld".to_string(), + serde_json::Value::Bool(main_world_eval), + ); + } + + // Addons + if let Some(addons) = &config.addons { + let addon_values: Vec = addons + .iter() + .map(|a| serde_json::Value::String(a.clone())) + .collect(); + camou_config.insert("addons".to_string(), serde_json::Value::Array(addon_values)); + } + + // Exclude addons + if let Some(exclude_addons) = &config.exclude_addons { + let exclude_addon_values: Vec = exclude_addons + .iter() + .map(|a| serde_json::Value::String(a.clone())) + .collect(); + camou_config.insert( + "excludeAddons".to_string(), + serde_json::Value::Array(exclude_addon_values), + ); + } + + // Block features + if let Some(block_images) = config.block_images { + camou_config.insert( + "blockImages".to_string(), + serde_json::Value::Bool(block_images), + ); + } + if let Some(block_webrtc) = config.block_webrtc { + camou_config.insert( + "blockWebRTC".to_string(), + serde_json::Value::Bool(block_webrtc), + ); + } + if let Some(block_webgl) = config.block_webgl { + camou_config.insert( + "blockWebGL".to_string(), + serde_json::Value::Bool(block_webgl), + ); + } + + // COOP disable + if let Some(disable_coop) = config.disable_coop { + camou_config.insert( + "disableCOOP".to_string(), + serde_json::Value::Bool(disable_coop), + ); + } + + // GeoIP + if let Some(geoip) = &config.geoip { + camou_config.insert("geoip".to_string(), geoip.clone()); + } + + // Country + if let Some(country) = &config.country { + camou_config.insert( + "country".to_string(), + serde_json::Value::String(country.clone()), + ); + } + + // Firefox version + if let Some(ff_version) = config.ff_version { + camou_config.insert( + "ffVersion".to_string(), + serde_json::Value::Number(ff_version.into()), + ); + } + + // Enable cache + if let Some(enable_cache) = config.enable_cache { + camou_config.insert( + "enableCache".to_string(), + serde_json::Value::Bool(enable_cache), + ); + } + + // Proxy configuration + if let Some(proxy) = &config.proxy { + camou_config.insert( + "proxy".to_string(), + serde_json::Value::String(proxy.clone()), + ); + } + + // Firefox preferences + if let Some(firefox_prefs) = &config.firefox_prefs { + let mut prefs_obj = serde_json::Map::new(); + for (key, value) in firefox_prefs { + prefs_obj.insert(key.clone(), value.clone()); + } + camou_config.insert( + "firefoxPrefs".to_string(), + serde_json::Value::Object(prefs_obj), + ); + } + + camou_config.insert("showcursor".to_string(), serde_json::Value::Bool(false)); + + camou_config.insert("disableTheming".to_string(), serde_json::Value::Bool(true)); + + let final_config = serde_json::Value::Object(camou_config); + println!( + "Built CAMOU_CONFIG: {}", + serde_json::to_string_pretty(&final_config).unwrap_or_default() + ); + + // Validate that we have some basic anti-fingerprinting settings + let config_obj = final_config.as_object().unwrap(); + let has_timezone = config_obj.contains_key("timezone"); + let has_screen = + config_obj.contains_key("screen.width") || config_obj.contains_key("screen.height"); + let has_window = + config_obj.contains_key("window.outerWidth") || config_obj.contains_key("window.outerHeight"); + + println!("Anti-fingerprinting validation:"); + println!(" - Has timezone: {has_timezone}"); + println!(" - Has screen dimensions: {has_screen}"); + println!(" - Has window dimensions: {has_window}"); + + if !has_timezone && !has_screen && !has_window { + println!( + "WARNING: No anti-fingerprinting settings detected! Camoufox may not work as expected." + ); + } + + final_config + } + + /// Launch Camoufox browser with the specified configuration using direct process management + pub async fn launch_camoufox( + &self, + executable_path: &str, + profile_path: &str, + config: &CamoufoxConfig, + url: Option<&str>, + ) -> Result> { + println!("Launching Camoufox directly with executable: {executable_path}"); + println!("Profile path: {profile_path}"); + println!("URL: {url:?}"); + + // Generate unique ID for this instance + let instance_id = uuid::Uuid::new_v4().to_string(); + + // Try to generate configuration using nodecar first, fallback to manual build + let camou_config_json = match self.generate_camoufox_config_with_nodecar(config).await { + Ok(config) => { + println!("✅ Successfully generated Camoufox config using nodecar with camoufox-js-lsd"); + config + } + Err(e) => { + println!("⚠️ Failed to generate config with nodecar, falling back to manual build: {e}"); + self.build_camou_config(config) + } + }; + + // Build command arguments + let mut args = vec!["-profile".to_string(), profile_path.to_string()]; + + // Add URL if provided + if let Some(url) = url { + args.push(url.to_string()); + } + + // Add headless mode if specified + // if config.headless.unwrap_or(false) { + // args.push("-headless".to_string()); + // } + + // Add additional arguments + if let Some(additional_args) = &config.additional_args { + args.extend(additional_args.clone()); + } + + // Extract the env object from the generated config if it exists + let mut final_env_vars = std::collections::HashMap::new(); + if let Some(env_obj) = camou_config_json.get("env") { + if let Some(env_map) = env_obj.as_object() { + for (key, value) in env_map { + let value_str = match value { + serde_json::Value::String(s) => s.clone(), + serde_json::Value::Number(n) => n.to_string(), + serde_json::Value::Bool(b) => b.to_string(), + _ => value.to_string(), + }; + final_env_vars.insert(key.clone(), value_str); + } + } + } + + // Add user-specified environment variables (they override generated ones) + if let Some(user_env_vars) = &config.env_vars { + for (key, value) in user_env_vars { + final_env_vars.insert(key.clone(), value.clone()); + } + } + + // Remove the env key from the config JSON since we'll set it as actual env vars + let mut config_for_env = camou_config_json.clone(); + if let Some(config_obj) = config_for_env.as_object_mut() { + config_obj.remove("env"); + } + let camou_config_str = config_for_env.to_string(); + + // Set CAMOU_CONFIG environment variable - this is crucial for anti-fingerprinting + println!( + "Setting CAMOU_CONFIG environment variable: {}", + camou_config_str + ); + + // Build environment variables + let mut cmd = Command::new(executable_path); + cmd.args(&args); + + // Don't suppress stderr in debug mode so we can see Camoufox error messages + if config.debug.unwrap_or(false) { + println!("Debug mode enabled - keeping stderr output for troubleshooting"); + } else { + cmd.stdout(Stdio::null()); + cmd.stderr(Stdio::null()); + } + + // CRITICAL: Add cache-busting environment variables to force Camoufox config refresh + // This works around the std::call_once limitation in MaskConfig.hpp + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + + let cache_buster = format!("{}_{}", std::process::id(), timestamp); + + // Multiple cache-busting strategies to ensure config refresh + cmd.env("CAMOU_CACHE_INVALIDATE", &cache_buster); + cmd.env("CAMOU_CONFIG_REFRESH", ×tamp.to_string()); + cmd.env("CAMOU_PROCESS_ISOLATION", &cache_buster); + + // Force Camoufox to treat this as a completely new process context + cmd.env("CAMOU_FORCE_CONFIG_RELOAD", "1"); + cmd.env("CAMOU_DISABLE_CONFIG_CACHE", "1"); + + println!( + "Setting cache-busting environment variables with timestamp: {}", + timestamp + ); + + // Check if the config string is too large for a single environment variable + const MAX_ENV_SIZE: usize = 2000; + + if camou_config_str.len() > MAX_ENV_SIZE { + // Split into multiple environment variables + let chunks: Vec<&str> = camou_config_str + .as_bytes() + .chunks(MAX_ENV_SIZE) + .map(|chunk| std::str::from_utf8(chunk).unwrap_or("")) + .collect(); + + for (i, chunk) in chunks.iter().enumerate() { + let env_name = format!("CAMOU_CONFIG_{}", i + 1); + println!( + "Setting {} (chunk {} of {}): {} bytes", + env_name, + i + 1, + chunks.len(), + chunk.len() + ); + cmd.env(&env_name, chunk); + } + } else { + // Use single environment variable + cmd.env("CAMOU_CONFIG", &camou_config_str); + } + + // Set working directory to the executable's directory for better compatibility + if let Some(parent_dir) = std::path::Path::new(executable_path).parent() { + cmd.current_dir(parent_dir); + println!("Set working directory to: {:?}", parent_dir); + } + + // Set all environment variables from the generated config + for (key, value) in &final_env_vars { + println!("Setting generated environment variable: {}={}", key, value); + cmd.env(key, value); + } + + // Add user-specified environment variables (they override generated ones) + if let Some(user_env_vars) = &config.env_vars { + for (key, value) in user_env_vars { + println!("Setting user environment variable: {}={}", key, value); + cmd.env(key, value); + } + } + + // Set virtual display if specified + if let Some(virtual_display) = &config.virtual_display { + println!("Setting DISPLAY environment variable: {}", virtual_display); + cmd.env("DISPLAY", virtual_display); + } + + // Debug: Print launch information + println!("=== Camoufox Launch Debug Info ==="); + println!("Executable: {}", executable_path); + println!("Arguments: {:?}", args); + println!("CAMOU_CONFIG length: {} bytes", camou_config_str.len()); + + // Verify the JSON is valid + match serde_json::from_str::(&camou_config_str) { + Ok(parsed) => { + println!("✅ CAMOU_CONFIG JSON is valid"); + if let Some(obj) = parsed.as_object() { + println!("📊 Config contains {} keys:", obj.len()); + for key in obj.keys() { + println!(" - {}", key); + } + } + } + Err(e) => { + println!("❌ CAMOU_CONFIG JSON is invalid: {}", e); + } + } + + // Launch the process + let child = cmd + .spawn() + .map_err(|e| format!("Failed to launch Camoufox process: {e}"))?; + + let pid = child.id(); + println!("Launched Camoufox with PID: {pid}"); + + // Store the instance + let instance = CamoufoxInstance { + pid, + executable_path: executable_path.to_string(), + profile_path: profile_path.to_string(), + url: url.map(|u| u.to_string()), + _child: Some(child), + }; + + { + let mut inner = self.inner.lock().await; + inner.instances.insert(instance_id.clone(), instance); + } + + // Return launch result + Ok(CamoufoxLaunchResult { + id: instance_id, + pid: Some(pid), + executablePath: executable_path.to_string(), + profilePath: profile_path.to_string(), + url: url.map(|u| u.to_string()), + }) + } + + /// Stop a Camoufox process by ID + pub async fn stop_camoufox( + &self, + id: &str, + ) -> Result> { + println!("Stopping Camoufox process with ID: {id}"); + + let instance = { + let mut inner = self.inner.lock().await; + inner.instances.remove(id) + }; + + if let Some(mut instance) = instance { + // Try to kill the process gracefully first + let system = System::new_all(); + if let Some(process) = system.process(Pid::from(instance.pid as usize)) { + if process.kill() { + println!( + "Successfully killed Camoufox process: {id} (PID: {})", + instance.pid + ); + } else { + println!( + "Failed to kill Camoufox process: {id} (PID: {})", + instance.pid + ); + } + } + + // Also try to kill the child process if we still have a handle + if let Some(ref mut child) = instance._child { + let _ = child.kill(); + } + + Ok(true) + } else { + println!("Camoufox process with ID {id} not found"); + Ok(false) + } + } + + /// Find Camoufox process by profile path (for integration with browser_runner) + pub async fn find_camoufox_by_profile( + &self, + profile_path: &str, + ) -> Result, Box> { + println!("Looking for Camoufox process with profile path: {profile_path}"); + + let inner = self.inner.lock().await; + + // Convert paths to canonical form for comparison + let target_path = Path::new(profile_path) + .canonicalize() + .unwrap_or_else(|_| Path::new(profile_path).to_path_buf()); + + for (id, instance) in inner.instances.iter() { + let instance_path = Path::new(&instance.profile_path) + .canonicalize() + .unwrap_or_else(|_| Path::new(&instance.profile_path).to_path_buf()); + + if instance_path == target_path { + println!("Found match using canonical path comparison"); + return Ok(Some(CamoufoxLaunchResult { + id: id.clone(), + pid: Some(instance.pid), + executablePath: instance.executable_path.clone(), + profilePath: instance.profile_path.clone(), + url: instance.url.clone(), + })); + } + } + + println!("No matching Camoufox process found for profile path: {profile_path}"); + Ok(None) + } + + /// Check if processes are still alive and clean up dead instances + pub async fn cleanup_dead_instances( + &self, + ) -> Result, Box> { + let mut dead_instances = Vec::new(); + let mut instances_to_remove = Vec::new(); + + { + let inner = self.inner.lock().await; + let system = System::new_all(); + + for (id, instance) in inner.instances.iter() { + // Check if the process is still alive + if let Some(_process) = system.process(Pid::from(instance.pid as usize)) { + // Process is still alive + continue; + } else { + // Process is dead + println!( + "Detected dead Camoufox instance: {} (PID: {})", + id, instance.pid + ); + dead_instances.push(id.clone()); + instances_to_remove.push(id.clone()); + } + } + } + + // Remove dead instances + if !instances_to_remove.is_empty() { + let mut inner = self.inner.lock().await; + for id in &instances_to_remove { + inner.instances.remove(id); + } + println!( + "Cleaned up {} dead Camoufox instances", + instances_to_remove.len() + ); + } + + Ok(dead_instances) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_nodecar_config_generation() { + let launcher = CamoufoxDirectLauncher::new_singleton(); + + // Test with empty config (should generate random config) + let empty_config = CamoufoxConfig::default(); + let empty_result = launcher + .generate_camoufox_config_with_nodecar(&empty_config) + .await; + + match empty_result { + Ok(config) => { + println!("✅ Empty config test passed"); + + // Check if it has essential properties + if let Some(obj) = config.as_object() { + let has_navigator_ua = obj.contains_key("navigator.userAgent"); + let has_screen_width = obj.contains_key("screen.width"); + let has_timezone = obj.contains_key("timezone"); + + // At least one of these should be present in a valid config + assert!( + has_navigator_ua || has_screen_width || has_timezone, + "Generated config should have at least one fingerprinting property" + ); + } + } + Err(e) => { + // This is expected if nodecar is not available in test environment + println!("⚠️ Nodecar not available in test environment: {}", e); + } + } + + // Test with configured values + let test_config = CamoufoxDirectLauncher::create_test_config(); + let test_result = launcher + .generate_camoufox_config_with_nodecar(&test_config) + .await; + + match test_result { + Ok(config) => { + println!("✅ Test config generation passed"); + + // Verify the config is valid JSON + assert!( + config.is_object(), + "Generated config should be a JSON object" + ); + + // Check if user settings might be respected (this depends on nodecar being available) + if let Some(obj) = config.as_object() { + // At least verify we got a valid config structure + assert!(!obj.is_empty(), "Generated config should not be empty"); + } + } + Err(e) => { + println!("⚠️ Nodecar not available for test config: {}", e); + } + } + } + + #[test] + fn test_camoufox_config_creation() { + let test_config = CamoufoxDirectLauncher::create_test_config(); + + // Verify test config has expected values + assert_eq!(test_config.timezone, Some("Europe/London".to_string())); + assert_eq!(test_config.screen_min_width, Some(1440)); + assert_eq!(test_config.screen_min_height, Some(900)); + assert_eq!(test_config.window_width, Some(1200)); + assert_eq!(test_config.window_height, Some(800)); + assert_eq!(test_config.webgl_vendor, Some("Intel Inc.".to_string())); + assert_eq!( + test_config.webgl_renderer, + Some("Intel Iris Pro OpenGL Engine".to_string()) + ); + assert_eq!(test_config.latitude, Some(51.5074)); + assert_eq!(test_config.longitude, Some(-0.1278)); + assert_eq!(test_config.humanize, Some(true)); + assert_eq!(test_config.debug, Some(true)); + assert_eq!(test_config.enable_cache, Some(true)); + assert_eq!(test_config.headless, Some(false)); + } + + #[test] + fn test_fallback_config_generation() { + let launcher = CamoufoxDirectLauncher::new_singleton(); + + let test_config = CamoufoxDirectLauncher::create_test_config(); + let fallback_config = launcher.build_camou_config(&test_config); + + // Verify fallback config structure + assert!( + fallback_config.is_object(), + "Fallback config should be a JSON object" + ); + + let config_obj = fallback_config.as_object().unwrap(); + + // Check essential anti-fingerprinting properties + assert!(config_obj.contains_key("timezone"), "Should have timezone"); + assert!( + config_obj.contains_key("screen.width"), + "Should have screen width" + ); + assert!( + config_obj.contains_key("window.outerWidth"), + "Should have window width" + ); + assert!(config_obj.contains_key("debug"), "Should have debug flag"); + + // Verify specific values + assert_eq!( + config_obj.get("timezone").unwrap().as_str().unwrap(), + "Europe/London" + ); + assert_eq!( + config_obj.get("screen.width").unwrap().as_u64().unwrap(), + 1440 + ); + assert_eq!( + config_obj + .get("window.outerWidth") + .unwrap() + .as_u64() + .unwrap(), + 1200 + ); + } + + #[test] + fn test_default_config() { + let default_config = CamoufoxConfig::default(); + + // Verify defaults + assert_eq!(default_config.enable_cache, Some(true)); + assert_eq!(default_config.timezone, None); + assert_eq!(default_config.debug, None); + assert_eq!(default_config.headless, None); + } +} + +pub async fn launch_camoufox_profile_direct( + app_handle: AppHandle, + profile: BrowserProfile, + config: CamoufoxConfig, + url: Option, +) -> Result { + let launcher = CamoufoxDirectLauncher::new(app_handle); + + // Get the executable path for Camoufox + let browser_runner = crate::browser_runner::BrowserRunner::new(); + let binaries_dir = browser_runner.get_binaries_dir(); + let browser_dir = binaries_dir.join("camoufox").join(&profile.version); + + // Get executable path + let browser = crate::browser::create_browser(crate::browser::BrowserType::Camoufox); + let executable_path = browser + .get_executable_path(&browser_dir) + .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?; + + // Get profile path + let profiles_dir = browser_runner.get_profiles_dir(); + let profile_path = profile.get_profile_data_path(&profiles_dir); + + launcher + .launch_camoufox( + &executable_path.to_string_lossy(), + &profile_path.to_string_lossy(), + &config, + url.as_deref(), + ) + .await + .map_err(|e| format!("Failed to launch Camoufox: {e}")) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 11448be..fc6ef55 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,7 +13,7 @@ mod auto_updater; mod browser; mod browser_runner; mod browser_version_service; -mod camoufox; +mod camoufox_direct; mod default_browser; mod download; mod downloaded_browsers; @@ -216,7 +216,7 @@ async fn delete_stored_proxy(proxy_id: String) -> Result<(), String> { #[tauri::command] async fn update_camoufox_config( profile_name: String, - config: crate::camoufox::CamoufoxConfig, + config: crate::camoufox_direct::CamoufoxConfig, ) -> Result<(), String> { let browser_runner = browser_runner::BrowserRunner::new(); browser_runner @@ -400,6 +400,31 @@ pub fn run() { } }); + // Start Camoufox cleanup task + let app_handle_cleanup = app.handle().clone(); + tauri::async_runtime::spawn(async move { + let launcher = crate::camoufox_direct::CamoufoxDirectLauncher::new(app_handle_cleanup); + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); + + loop { + interval.tick().await; + + match launcher.cleanup_dead_instances().await { + Ok(dead_instances) => { + if !dead_instances.is_empty() { + println!( + "Cleaned up {} dead Camoufox instances", + dead_instances.len() + ); + } + } + Err(e) => { + eprintln!("Error during Camoufox cleanup: {e}"); + } + } + } + }); + Ok(()) }) .invoke_handler(tauri::generate_handler![ diff --git a/src/app/page.tsx b/src/app/page.tsx index 7ce2d79..8392719 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -141,9 +141,7 @@ export default function Home() { const profileList = await invoke( "list_browser_profiles", ); - setProfiles( - profileList.filter((profile) => profile.browser !== "camoufox"), - ); + setProfiles(profileList); // Check for missing binaries after loading profiles await checkMissingBinaries(); @@ -191,9 +189,7 @@ export default function Home() { const profileList = await invoke( "list_browser_profiles", ); - setProfiles( - profileList.filter((profile) => profile.browser !== "camoufox"), - ); + setProfiles(profileList); // TODO: remove after a few version bumps, needed to properly display migrated profiles setTimeout(async () => { @@ -201,9 +197,7 @@ export default function Home() { const profiles = await invoke( "list_browser_profiles", ); - setProfiles( - profiles.filter((profile) => profile.browser !== "camoufox"), - ); + setProfiles(profiles); } await sleep(500); }, 0); diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index 4ffa9a6..076e603 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -86,7 +86,7 @@ const browserOptions: BrowserOption[] = [ { value: "mullvad-browser", label: "Mullvad Browser", - description: "Privacy browser by Mullvad VPN", + description: "TOR Browser fork by Mullvad VPN", }, { value: "tor-browser", @@ -95,8 +95,6 @@ const browserOptions: BrowserOption[] = [ }, ]; -const IS_ANTI_DETECT_SUPPORTED = false; - export function CreateProfileDialog({ isOpen, onClose, @@ -388,54 +386,45 @@ export function CreateProfileDialog({ + {/* Anti-Detect Description */}

- Anti-Detect support is coming soon! + Powered by Camoufox

-
- {IS_ANTI_DETECT_SUPPORTED && ( - - {/* Anti-Detect Description */} -
-

- Powered by Camoufox -

-
-
- {/* Camoufox Download Status */} - {!isBrowserVersionAvailable("camoufox") && - camoufoxReleaseTypes.stable && ( -
-

- Camoufox version ({camoufoxReleaseTypes.stable}) - needs to be downloaded -

- handleDownload("camoufox")} - isLoading={isBrowserDownloading("camoufox")} - size="sm" - disabled={isBrowserDownloading("camoufox")} - > - Download - -
- )} - {isBrowserVersionAvailable("camoufox") && ( -
- ✓ Camoufox version ({camoufoxReleaseTypes.stable}) is - available +
+ {/* Camoufox Download Status */} + {!isBrowserVersionAvailable("camoufox") && + camoufoxReleaseTypes.stable && ( +
+

+ Camoufox version ({camoufoxReleaseTypes.stable}) needs + to be downloaded +

+ handleDownload("camoufox")} + isLoading={isBrowserDownloading("camoufox")} + size="sm" + disabled={isBrowserDownloading("camoufox")} + > + Download +
)} + {isBrowserVersionAvailable("camoufox") && ( +
+ ✓ Camoufox version ({camoufoxReleaseTypes.stable}) is + available +
+ )} - -
- - )} + +
+ {/* Proxy Selection - Common to both tabs - Compact without card */} {storedProxies.length > 0 && ( diff --git a/src/lib/toast-utils.ts b/src/lib/toast-utils.ts index ccb0d8b..1285d63 100644 --- a/src/lib/toast-utils.ts +++ b/src/lib/toast-utils.ts @@ -100,7 +100,7 @@ export function showToast(props: ToastProps & { id?: string }) { } if (props.type === "success") { - sonnerToast.success(React.createElement(UnifiedToast, props), { + sonnerToast.custom(() => React.createElement(UnifiedToast, props), { id: toastId, duration, style: { @@ -113,7 +113,7 @@ export function showToast(props: ToastProps & { id?: string }) { }, }); } else if (props.type === "error") { - sonnerToast.error(React.createElement(UnifiedToast, props), { + sonnerToast.custom(() => React.createElement(UnifiedToast, props), { id: toastId, duration, style: {