From 703ca2c50b24cb76e7d208d5906064dc8c930ce9 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Mon, 7 Jul 2025 06:19:43 +0400 Subject: [PATCH] feat: add anti-detect functionality --- .vscode/settings.json | 16 +- nodecar/package.json | 1 + nodecar/src/camoufox-launcher.ts | 503 ++++++++ nodecar/src/index.ts | 278 ++++ package.json | 1 + pnpm-lock.yaml | 1140 ++++++++++++++++- pnpm-workspace.yaml | 8 +- src-tauri/src/api_client.rs | 194 +++ src-tauri/src/auto_updater.rs | 1 + src-tauri/src/browser.rs | 104 +- src-tauri/src/browser_runner.rs | 637 ++++++--- src-tauri/src/browser_version_service.rs | 71 + src-tauri/src/camoufox.rs | 607 +++++++++ src-tauri/src/download.rs | 53 + src-tauri/src/geoip_downloader.rs | 171 +++ src-tauri/src/lib.rs | 20 + src-tauri/src/profile_importer.rs | 1 + src-tauri/src/proxy_manager.rs | 6 +- src-tauri/src/system_utils.rs | 331 +++++ src/app/page.tsx | 46 +- src/components/camoufox-config-dialog.tsx | 502 ++++++++ src/components/create-profile-dialog.tsx | 754 +++++------ src/components/profile-data-table.tsx | 17 +- .../shared-camoufox-config-form.tsx | 568 ++++++++ src/components/ui/combobox.tsx | 79 ++ src/components/ui/tabs.tsx | 55 + src/hooks/use-browser-download.ts | 75 +- src/hooks/use-version-updater.ts | 312 +++-- src/lib/browser-utils.ts | 5 +- src/types.ts | 47 + 30 files changed, 5844 insertions(+), 759 deletions(-) create mode 100644 nodecar/src/camoufox-launcher.ts create mode 100644 src-tauri/src/camoufox.rs create mode 100644 src-tauri/src/geoip_downloader.rs create mode 100644 src-tauri/src/system_utils.rs create mode 100644 src/components/camoufox-config-dialog.tsx create mode 100644 src/components/shared-camoufox-config-form.tsx create mode 100644 src/components/ui/tabs.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 2366813..088143b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "autoconfig", "autologin", "biomejs", + "camoufox", "cdylib", "CFURL", "checkin", @@ -15,6 +16,7 @@ "clippy", "cmdk", "codegen", + "CTYPE", "devedition", "doesn", "donutbrowser", @@ -26,6 +28,8 @@ "esac", "esbuild", "frontmost", + "geoip", + "gettimezone", "gifs", "gsettings", "icns", @@ -42,6 +46,8 @@ "librsvg", "libwebkit", "libxdo", + "localtime", + "mmdb", "mountpoint", "msiexec", "msvc", @@ -58,6 +64,7 @@ "osascript", "pixbuf", "plasmohq", + "prefs", "propertylist", "reqwest", "ridedott", @@ -68,6 +75,7 @@ "shadcn", "signon", "sonner", + "splitn", "sspi", "staticlib", "stefanzweifel", @@ -76,9 +84,12 @@ "swatinem", "sysinfo", "systempreferences", + "systemsetup", "taskkill", "tasklist", "tauri", + "TERX", + "timedatectl", "titlebar", "Torbrowser", "turbopack", @@ -89,9 +100,12 @@ "urlencoding", "vercel", "VERYSILENT", + "webgl", + "webrtc", "winreg", "wiremock", "xattr", - "zhom" + "zhom", + "zoneinfo" ] } diff --git a/nodecar/package.json b/nodecar/package.json index f42720d..07c5087 100644 --- a/nodecar/package.json +++ b/nodecar/package.json @@ -23,6 +23,7 @@ "dependencies": { "@types/node": "^24.0.10", "@yao-pkg/pkg": "^6.5.1", + "camoufox-js": "^0.6.0", "commander": "^14.0.0", "dotenv": "^17.0.1", "get-port": "^7.1.0", diff --git a/nodecar/src/camoufox-launcher.ts b/nodecar/src/camoufox-launcher.ts new file mode 100644 index 0000000..f287698 --- /dev/null +++ b/nodecar/src/camoufox-launcher.ts @@ -0,0 +1,503 @@ +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; +} + +export interface CamoufoxLaunchOptions { + // Operating system to use for fingerprint generation + os?: "windows" | "macos" | "linux" | string[]; + + // Blocking options + block_images?: boolean; + block_webrtc?: boolean; + block_webgl?: boolean; + + // Security options + disable_coop?: boolean; + + // Geolocation options + geoip?: string | boolean; + + // UI behavior + humanize?: boolean | number; + + // Localization + locale?: string | string[]; + + // Extensions and fonts + addons?: string[]; + fonts?: string[]; + custom_fonts_only?: boolean; + exclude_addons?: string[]; + + // Screen and window + screen?: { + minWidth?: number; + maxWidth?: number; + minHeight?: number; + maxHeight?: number; + }; + window?: [number, number]; + + // Fingerprint + fingerprint?: any; + + // Version and mode + ff_version?: number; + headless?: boolean; + main_world_eval?: boolean; + + // Custom executable path + executable_path?: string; + + // Firefox preferences + firefox_user_prefs?: Record; + + // Proxy settings + proxy?: + | string + | { + server: string; + username?: string; + password?: string; + bypass?: string; + }; + + // Cache and performance + enable_cache?: boolean; + + // Additional options + args?: string[]; + env?: Record; + debug?: boolean; + virtual_display?: string; + webgl_config?: [string, string]; + + // Custom options + timezone?: string; + country?: string; + geolocation?: { + latitude: number; + longitude: number; + accuracy?: number; + }; +} + +// Store for active Camoufox processes +const activeCamoufoxProcesses = new Map(); + +/** + * Generate a unique ID for the Camoufox instance + */ +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, + 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; + } + + try { + if (isProcessRunning(config.pid)) { + process.kill(config.pid, "SIGTERM"); + + // Wait a moment for graceful shutdown + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Force kill if still running + if (isProcessRunning(config.pid)) { + process.kill(config.pid, "SIGKILL"); + } + } + + deleteCamoufoxConfig(id); + return true; + } catch (error) { + console.error(`Failed to stop Camoufox process: ${error}`); + return false; + } +} + +/** + * 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 f6957d8..34f46ac 100644 --- a/nodecar/src/index.ts +++ b/nodecar/src/index.ts @@ -1,4 +1,9 @@ import { program } from "commander"; +import { + launchCamoufox, + listCamoufoxProcesses, + stopCamoufox, +} from "./camoufox-launcher"; import { startProxyProcess, stopAllProxyProcesses, @@ -149,4 +154,277 @@ program } }); +// Command for Camoufox anti-detect browser +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)") + + // Operating system fingerprinting + .option( + "--os ", + "OS to emulate (windows, macos, linux, or comma-separated list)", + ) + + // Blocking options + .option("--block-images", "block all images") + .option("--block-webrtc", "block WebRTC entirely") + .option("--block-webgl", "block WebGL") + + // Security options + .option("--disable-coop", "disable Cross-Origin-Opener-Policy") + + // Geolocation and IP + .option( + "--geoip ", + "IP address for geolocation spoofing (or 'auto' for automatic)", + ) + .option("--country ", "country code for geolocation") + .option("--timezone ", "timezone to spoof") + .option("--latitude ", "latitude for geolocation", parseFloat) + .option("--longitude ", "longitude for geolocation", parseFloat) + + // UI and behavior + .option( + "--humanize [duration]", + "humanize cursor movement (optional max duration in seconds)", + (val) => (val ? parseFloat(val) : true), + ) + .option("--headless", "run in headless mode") + + // Localization + .option("--locale ", "locale(s) to use (comma-separated)") + + // Extensions and fonts + .option("--addons ", "Firefox addons to load (comma-separated paths)") + .option("--fonts ", "additional fonts to load (comma-separated)") + .option("--custom-fonts-only", "use only custom fonts, exclude OS fonts") + .option( + "--exclude-addons ", + "default addons to exclude (comma-separated)", + ) + + // Screen and window + .option("--screen-min-width ", "minimum screen width", parseInt) + .option("--screen-max-width ", "maximum screen width", parseInt) + .option("--screen-min-height ", "minimum screen height", parseInt) + .option("--screen-max-height ", "maximum screen height", parseInt) + .option("--window-width ", "fixed window width", parseInt) + .option("--window-height ", "fixed window height", parseInt) + + // Advanced options + .option("--ff-version ", "Firefox version to emulate", parseInt) + .option("--main-world-eval", "enable main world script evaluation") + .option("--webgl-vendor ", "WebGL vendor string") + .option("--webgl-renderer ", "WebGL renderer string") + + // Proxy + .option( + "--proxy ", + "proxy URL (protocol://[username:password@]host:port)", + ) + + // Cache and performance + .option("--disable-cache", "disable browser cache (cache enabled by default)") + + // Environment and debugging + .option("--virtual-display ", "virtual display number (e.g., :99)") + .option("--debug", "enable debug output") + .option("--args ", "additional browser arguments (comma-separated)") + .option("--env ", "environment variables (JSON string)") + + // Firefox preferences + .option("--firefox-prefs ", "Firefox user preferences (JSON string)") + + .description("launch and manage Camoufox anti-detect browser instances") + .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; + } + + // Build Camoufox options + const camoufoxOptions: any = { + enable_cache: !options.disableCache, // Cache enabled by default as requested + }; + + // OS fingerprinting + if (options.os) { + camoufoxOptions.os = options.os.includes(",") + ? options.os.split(",") + : options.os; + } + + // Blocking options + if (options.blockImages) camoufoxOptions.block_images = true; + if (options.blockWebrtc) camoufoxOptions.block_webrtc = true; + if (options.blockWebgl) camoufoxOptions.block_webgl = true; + + // Security options + if (options.disableCoop) camoufoxOptions.disable_coop = true; + + // Geolocation + if (options.geoip) { + camoufoxOptions.geoip = + options.geoip === "auto" ? true : options.geoip; + } + if (options.latitude && options.longitude) { + camoufoxOptions.geolocation = { + latitude: options.latitude, + longitude: options.longitude, + accuracy: 100, + }; + } + if (options.country) camoufoxOptions.country = options.country; + if (options.timezone) camoufoxOptions.timezone = options.timezone; + + // UI and behavior + if (options.humanize) camoufoxOptions.humanize = options.humanize; + if (options.headless) camoufoxOptions.headless = true; + + // Localization + if (options.locale) { + camoufoxOptions.locale = options.locale.includes(",") + ? options.locale.split(",") + : options.locale; + } + + // Extensions and fonts + if (options.addons) camoufoxOptions.addons = options.addons.split(","); + if (options.fonts) camoufoxOptions.fonts = options.fonts.split(","); + if (options.customFontsOnly) camoufoxOptions.custom_fonts_only = true; + if (options.excludeAddons) + camoufoxOptions.exclude_addons = options.excludeAddons.split(","); + + // Screen and window + const screen: any = {}; + if (options.screenMinWidth) screen.minWidth = options.screenMinWidth; + if (options.screenMaxWidth) screen.maxWidth = options.screenMaxWidth; + if (options.screenMinHeight) screen.minHeight = options.screenMinHeight; + if (options.screenMaxHeight) screen.maxHeight = options.screenMaxHeight; + if (Object.keys(screen).length > 0) camoufoxOptions.screen = screen; + + if (options.windowWidth && options.windowHeight) { + camoufoxOptions.window = [options.windowWidth, options.windowHeight]; + } + + // Advanced options + if (options.ffVersion) camoufoxOptions.ff_version = options.ffVersion; + if (options.mainWorldEval) camoufoxOptions.main_world_eval = true; + if (options.webglVendor && options.webglRenderer) { + camoufoxOptions.webgl_config = [ + options.webglVendor, + options.webglRenderer, + ]; + } + + // Proxy + if (options.proxy) camoufoxOptions.proxy = options.proxy; + + // Environment and debugging + if (options.virtualDisplay) + camoufoxOptions.virtual_display = options.virtualDisplay; + if (options.debug) camoufoxOptions.debug = true; + if (options.args) camoufoxOptions.args = options.args.split(","); + if (options.env) { + try { + camoufoxOptions.env = JSON.parse(options.env); + } catch (e) { + console.error("Invalid JSON for --env option"); + process.exit(1); + return; + } + } + + // Firefox preferences + if (options.firefoxPrefs) { + try { + camoufoxOptions.firefox_user_prefs = JSON.parse( + options.firefoxPrefs, + ); + } catch (e) { + console.error("Invalid JSON for --firefox-prefs option"); + process.exit(1); + return; + } + } + + // 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, + }), + ); + + 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'", + ); + process.exit(1); + } + } catch (error: unknown) { + console.error( + `Camoufox command failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`, + ); + process.exit(1); + } + }); + program.parse(); diff --git a/package.json b/package.json index d167361..caa1e86 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-table": "^8.21.3", "@tauri-apps/api": "^2.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 523c32a..dcb0610 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-tabs': + specifier: ^1.1.12 + version: 1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-tooltip': specifier: ^1.2.7 version: 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -140,7 +143,10 @@ importers: version: 24.0.10 '@yao-pkg/pkg': specifier: ^6.5.1 - version: 6.5.1 + version: 6.5.1(encoding@0.1.13) + camoufox-js: + specifier: ^0.6.0 + version: 0.6.0(encoding@0.1.13)(playwright-core@1.53.2) commander: specifier: ^14.0.0 version: 14.0.0 @@ -509,6 +515,9 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + '@img/sharp-darwin-arm64@0.34.2': resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -710,6 +719,14 @@ packages: cpu: [x64] os: [win32] + '@npmcli/fs@1.1.1': + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + + '@npmcli/move-file@1.1.2': + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -1004,6 +1021,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-tabs@1.1.12': + resolution: {integrity: sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tooltip@1.2.7': resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==} peerDependencies: @@ -1208,6 +1238,10 @@ packages: cpu: [x64] os: [win32] + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -1399,6 +1433,10 @@ packages: '@tauri-apps/plugin-opener@2.4.0': resolution: {integrity: sha512-43VyN8JJtvKWJY72WI/KNZszTpDpzHULFxQs0CJBIYUdCRowQ6Q1feWTDb979N7nldqSuDOaBupZ6wz2nvuWwQ==} + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -1429,6 +1467,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + '@types/node@24.0.10': resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==} @@ -1458,6 +1499,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + acorn-walk@8.3.4: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} @@ -1467,6 +1511,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -1475,6 +1523,14 @@ packages: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + ahooks@3.9.0: resolution: {integrity: sha512-r20/C38aFyU3Zqp3620gkdLnxmQhnmWORB3eGGTDlM4i/fOc0GUvM+f2oleMzEu7b3+pHXyzz+FB6ojxsUdYdw==} engines: {node: '>=8.0.0'} @@ -1506,13 +1562,27 @@ 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==} + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1523,6 +1593,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1548,6 +1621,24 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} + cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camoufox-js@0.6.0: + resolution: {integrity: sha512-QoCZoDdkXFRdHV4IgthBTogFCTL2p9QKFyEHGE6q1vGm0a9P3ael5zLXGPYiEzI5ByH2nlH4SV6CjQIJKjbxaA==} + hasBin: true + peerDependencies: + playwright-core: '*' + caniuse-lite@1.0.30001726: resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} @@ -1566,6 +1657,10 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -1573,6 +1668,10 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -1607,6 +1706,10 @@ packages: color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + color@4.2.3: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} @@ -1614,6 +1717,14 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + commander@14.0.0: resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} engines: {node: '>=20'} @@ -1621,6 +1732,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1653,6 +1767,16 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + detect-europe-js@0.1.2: + resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} + detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -1664,10 +1788,18 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + dotenv@17.0.1: resolution: {integrity: sha512-GLjkduuAL7IMJg/ZnOPm9AnWKJ82mSE2tzXLaJ/6hD6DhwGfZaXG77oB8qbReyiczNxnbxQKyh0OE5mXq0bAHA==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer2@0.1.4: resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} @@ -1680,6 +1812,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} @@ -1687,10 +1822,33 @@ packages: resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} engines: {node: '>=10.13.0'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.25.5: resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} engines: {node: '>=18'} @@ -1715,10 +1873,21 @@ packages: picomatch: optional: true + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + fingerprint-generator@2.1.69: + 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==} + engines: {node: '>= 6'} + from2@2.3.0: resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} @@ -1729,6 +1898,13 @@ packages: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1737,6 +1913,14 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + generative-bayesian-network@2.1.69: + resolution: {integrity: sha512-k8GgdPT9oCRchU4+7ofh/qpsmvSaOI0znFt/edanyWBBxLjSnjoSU97C15fGqQSdJIZ9uwsCU0RO8xnpLEX95w==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1749,6 +1933,10 @@ packages: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -1757,6 +1945,10 @@ packages: resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} engines: {node: '>=16'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -1764,6 +1956,14 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1775,25 +1975,121 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + header-generator@2.1.69: + resolution: {integrity: sha512-J3BK8UtPAR1Lvvfd/qlzmAS1Qb6Q2qx4K1s4FjYVrYvSQnUc7GgOAo9uacebg6WGCdP0eWxtojh7JwEd+AD2Hw==} + engines: {node: '>=16.0.0'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} hasBin: true + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + impit-darwin-arm64@0.5.2: + resolution: {integrity: sha512-8mDmMIBi4C3mBCnpjdhKfUFAyjQ/YzJHM+4giYzqhFU5ieBOtyqWkH2JhMq4W7Ox8kh57TW/D5BIP2fvgMgYgQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + impit-darwin-x64@0.5.2: + resolution: {integrity: sha512-WXtey+RczTaFbtcsF+ihzT3QqnYzs1H+9sADOy0XVcqeIwIQfijW0g8APUsd5wyVZZLQjlkDcfPRIm+Sd+YL3A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + impit-linux-arm64-gnu@0.5.2: + resolution: {integrity: sha512-k3wWTn8lwnXGQ/eIAw6skNuUaxCaQ3bFE6Ptu3UMeD1A4TT8/D2pWDbbz8CjEeOG2/Bv+MCMm8EM+Iz8PYSa9g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + impit-linux-arm64-musl@0.5.2: + resolution: {integrity: sha512-dlLJzZOs/x8rEhKwPHMVomveAipw3ULVi9rzOq0T81Qw+g3XL8r1ezDIkairiuwWhrcXYujCYkY4Br6IuIxRUg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + impit-linux-x64-gnu@0.5.2: + resolution: {integrity: sha512-Mvbpl3JnHND2oGMEmQt3uI0pdyitlplHEL5FY6d7OJLP00GPW3/V1f0IxDFcE6CUTccZUeHVIYdr3OiYdHadnw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + impit-linux-x64-musl@0.5.2: + resolution: {integrity: sha512-KFGCGHk4T4omtGe58gPgg+FRsBlzmZuPtpyi6m5mIv6cC7Gjjt06wzUXSmGyIWjgy+O4BYHyou5sLB03BILnNQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + impit-win32-arm64-msvc@0.5.2: + resolution: {integrity: sha512-yPMLBiAqzgZ91YoTXz55UdlTxaPWC5hcyC3bdMtPmNnoBLY2PBRRTDaWaYcBLtmAggwtF5CAkMBqNmdWk1UK8w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + impit-win32-x64-msvc@0.5.2: + resolution: {integrity: sha512-K3XuwINt7WSfJkLskADSQ3LUEd/iHfyzZl8XNaczGBcg9dPNfLQfWD9RU8BlfpCyyCTyq3x3hZZjO0Cax/l/8Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + impit@0.5.2: + resolution: {integrity: sha512-gtGBhPSUU/3Fmf/vHL6yG8B6ccJkhJZ/650z2FGpIOUmhEb9w1NPPB4QNUb7PTO/xgie+BZEipkqu/dOqAdrAg==} + engines: {node: '>= 20'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1842,13 +2138,26 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-standalone-pwa@0.1.1: + resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.4.2: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true @@ -1860,6 +2169,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} @@ -1880,6 +2193,13 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@2.1.0: + resolution: {integrity: sha512-D4CgpyCt+61f6z2jHjJS1OmZPviAWM57iJ9OKdFFWSNgS7Udj9QVWqyGs/cveVNF57XpZmhSvMdVIV5mjLA7Vg==} + engines: {node: '>=22'} + lightningcss-darwin-arm64@1.30.1: resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} engines: {node: '>= 12.0.0'} @@ -1957,6 +2277,10 @@ packages: resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} engines: {node: '>=18.0.0'} + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -1967,16 +2291,40 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + maxmind@4.3.28: + resolution: {integrity: sha512-K3PcTVjhrSU6xzY7niQf/CHPmx/qzkMIpMumd3x0JXMkIDJMerL4LY9RsEhArHb4T3IK0NBxQcUlwjsxRm9rIw==} + engines: {node: '>=12', npm: '>=6'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -1991,10 +2339,42 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + minizlib@3.0.2: resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} engines: {node: '>= 18'} @@ -2002,11 +2382,20 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} hasBin: true + mmdb-lib@2.2.1: + resolution: {integrity: sha512-DXO4L9W+08T+A7h5+xdT32l7IMot8z7WOH+7C1Maol571PnktQ8un7Ni4CyPFp4H+vht/FDA5/tpjRvWMFQDMw==} + engines: {node: '>=10', npm: '>=6'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2025,6 +2414,10 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -2056,6 +2449,9 @@ packages: resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} engines: {node: '>=10'} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -2065,6 +2461,11 @@ packages: encoding: optional: true + node-gyp@8.4.1: + resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} + engines: {node: '>= 10.12.0'} + hasBin: true + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -2076,10 +2477,20 @@ packages: engines: {node: '>=10'} hasBin: true + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2087,10 +2498,22 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + ow@0.28.2: + resolution: {integrity: sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==} + engines: {node: '>=12'} + p-is-promise@3.0.0: resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==} engines: {node: '>=8'} + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -2110,6 +2533,11 @@ packages: engines: {node: '>=0.10'} hasBin: true + playwright-core@1.53.2: + resolution: {integrity: sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==} + engines: {node: '>=18'} + hasBin: true + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -2130,6 +2558,18 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + proxy-chain@2.5.9: resolution: {integrity: sha512-DZZKtRz92WuXd7fzRTKgI/oGhjmSgGMgT3FweLunCztpaG5jDVOJp1jgRPAVLQD1SG6HhkOyRkj6RTF3A214bg==} engines: {node: '>=14'} @@ -2222,9 +2662,18 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rollup@4.44.2: resolution: {integrity: sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2236,6 +2685,12 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -2252,10 +2707,16 @@ packages: engines: {node: '>=10'} hasBin: true + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + sharp@0.34.2: resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2285,6 +2746,10 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + socks-proxy-agent@8.0.5: resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} engines: {node: '>= 14'} @@ -2306,6 +2771,13 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + sqlite3@5.1.7: + resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} + + ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + stream-meter@1.0.4: resolution: {integrity: sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==} @@ -2389,6 +2861,10 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} @@ -2396,6 +2872,10 @@ packages: tauri-plugin-macos-permissions-api@2.3.0: resolution: {integrity: sha512-pZp0jmDySysBqrGueknd1a7Rr4XEO9aXpMv9TNrT2PDHP0MSH20njieOagsFYJ5MCVb8A+wcaK0cIkjUC2dOww==} + tiny-lru@11.3.3: + resolution: {integrity: sha512-/ShxBZOgHXDdZi7FxajcsH0MfcBqwP+t7i4T3PGjI//NUA5aCpC7cB9bbdAYrAeQLBUTJfg2rk191fzZGeo7DA==} + engines: {node: '>=12'} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -2452,12 +2932,25 @@ packages: engines: {node: '>=14.17'} hasBin: true + ua-is-frozen@0.1.2: + resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==} + + ua-parser-js@2.0.4: + resolution: {integrity: sha512-XiBOnM/UpUq21ZZ91q2AVDOnGROE6UQd37WrO9WBgw4u2eGvUCNOheMmZ3EfEUj7DLHr8tre+Um/436Of/Vwzg==} + hasBin: true + undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + + unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -2497,6 +2990,10 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + vali-date@1.0.0: + resolution: {integrity: sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==} + engines: {node: '>=0.10.0'} + vite@6.2.0: resolution: {integrity: sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2543,6 +3040,14 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2554,6 +3059,14 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -2561,6 +3074,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -2863,6 +3379,9 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@gar/promisify@1.1.3': + optional: true + '@img/sharp-darwin-arm64@0.34.2': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.1.0 @@ -3008,6 +3527,18 @@ snapshots: '@next/swc-win32-x64-msvc@15.3.5': optional: true + '@npmcli/fs@1.1.1': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.7.2 + optional: true + + '@npmcli/move-file@1.1.2': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + optional: true + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.2': {} @@ -3326,6 +3857,22 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -3473,6 +4020,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.44.2': optional: true + '@sindresorhus/is@4.6.0': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -3624,6 +4173,9 @@ snapshots: dependencies: '@tauri-apps/api': 2.6.0 + '@tootallnate/once@1.1.2': + optional: true + '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -3657,6 +4209,11 @@ snapshots: '@types/json5@0.0.29': {} + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 24.0.10 + form-data: 4.0.3 + '@types/node@24.0.10': dependencies: undici-types: 7.8.0 @@ -3683,10 +4240,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@yao-pkg/pkg-fetch@3.5.23': + '@yao-pkg/pkg-fetch@3.5.23(encoding@0.1.13)': dependencies: https-proxy-agent: 5.0.1 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) picocolors: 1.1.1 progress: 2.0.3 semver: 7.7.2 @@ -3696,12 +4253,12 @@ snapshots: - encoding - supports-color - '@yao-pkg/pkg@6.5.1': + '@yao-pkg/pkg@6.5.1(encoding@0.1.13)': dependencies: '@babel/generator': 7.27.5 '@babel/parser': 7.27.5 '@babel/types': 7.27.6 - '@yao-pkg/pkg-fetch': 3.5.23 + '@yao-pkg/pkg-fetch': 3.5.23(encoding@0.1.13) into-stream: 6.0.0 minimist: 1.2.8 multistream: 4.1.0 @@ -3717,12 +4274,17 @@ snapshots: - encoding - supports-color + abbrev@1.1.1: + optional: true + acorn-walk@8.3.4: dependencies: acorn: 8.15.0 acorn@8.15.0: {} + adm-zip@0.5.16: {} + agent-base@6.0.2: dependencies: debug: 4.4.1(supports-color@5.5.0) @@ -3731,6 +4293,17 @@ snapshots: agent-base@7.1.3: {} + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + optional: true + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + optional: true + ahooks@3.9.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@babel/runtime': 7.27.6 @@ -3764,18 +4337,35 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + aproba@2.0.0: + optional: true + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + optional: true + arg@4.1.3: {} + argparse@2.0.1: {} + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 + asynckit@0.4.0: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} binary-extensions@2.3.0: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -3809,6 +4399,56 @@ snapshots: dependencies: streamsearch: 1.1.0 + cacache@15.3.0: + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.1 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + optional: true + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + camoufox-js@0.6.0(encoding@0.1.13)(playwright-core@1.53.2): + dependencies: + adm-zip: 0.5.16 + commander: 13.1.0 + fingerprint-generator: 2.1.69 + impit: 0.5.2 + js-yaml: 4.1.0 + language-tags: 2.1.0 + maxmind: 4.3.28 + playwright-core: 1.53.2 + progress: 2.0.3 + sqlite3: 5.1.7 + ua-parser-js: 2.0.4(encoding@0.1.13) + xml2js: 0.6.2 + transitivePeerDependencies: + - bluebird + - encoding + - supports-color + caniuse-lite@1.0.30001726: {} chalk@4.1.2: @@ -3832,12 +4472,17 @@ snapshots: chownr@1.1.4: {} + chownr@2.0.0: {} + chownr@3.0.0: {} class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 + clean-stack@2.2.0: + optional: true + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -3881,6 +4526,9 @@ snapshots: simple-swizzle: 0.2.2 optional: true + color-support@1.1.3: + optional: true + color@4.2.3: dependencies: color-convert: 2.0.1 @@ -3889,10 +4537,19 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@13.1.0: {} + commander@14.0.0: {} concat-map@0.0.1: {} + console-control-strings@1.1.0: + optional: true + convert-source-map@2.0.0: {} core-util-is@1.0.3: {} @@ -3915,14 +4572,31 @@ snapshots: deep-extend@0.6.0: {} + delayed-stream@1.0.0: {} + + delegates@1.0.0: + optional: true + + detect-europe-js@0.1.2: {} + detect-libc@2.0.4: {} detect-node-es@1.1.0: {} diff@4.0.2: {} + dot-prop@6.0.1: + dependencies: + is-obj: 2.0.0 + dotenv@17.0.1: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer2@0.1.4: dependencies: readable-stream: 2.3.8 @@ -3933,6 +4607,11 @@ snapshots: emoji-regex@8.0.0: {} + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + end-of-stream@1.4.4: dependencies: once: 1.4.0 @@ -3942,8 +4621,29 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 + env-paths@2.2.1: + optional: true + environment@1.1.0: {} + err-code@2.0.3: + optional: true + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.25.5: optionalDependencies: '@esbuild/aix-ppc64': 0.25.5 @@ -3982,10 +4682,26 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 + fingerprint-generator@2.1.69: + dependencies: + generative-bayesian-network: 2.1.69 + header-generator: 2.1.69 + tslib: 2.8.1 + + form-data@4.0.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + from2@2.3.0: dependencies: inherits: 2.0.4 @@ -3999,37 +4715,119 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: + optional: true + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + gauge@4.0.4: + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + optional: true + + generative-bayesian-network@2.1.69: + dependencies: + adm-zip: 0.5.16 + tslib: 2.8.1 + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} get-east-asian-width@1.3.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} get-port@7.1.0: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + github-from-package@0.0.0: {} glob-parent@5.1.2: dependencies: is-glob: 4.0.3 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true + + gopd@1.2.0: {} + graceful-fs@4.2.11: {} has-flag@3.0.0: {} has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: + optional: true + hasown@2.0.2: dependencies: function-bind: 1.1.2 + header-generator@2.1.69: + dependencies: + browserslist: 4.25.1 + generative-bayesian-network: 2.1.69 + ow: 0.28.2 + tslib: 2.8.1 + + http-cache-semantics@4.2.0: + optional: true + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + optional: true + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -4037,12 +4835,72 @@ snapshots: transitivePeerDependencies: - supports-color + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + optional: true + husky@9.1.7: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + ieee754@1.2.1: {} ignore-by-default@1.0.1: {} + impit-darwin-arm64@0.5.2: + optional: true + + impit-darwin-x64@0.5.2: + optional: true + + impit-linux-arm64-gnu@0.5.2: + optional: true + + impit-linux-arm64-musl@0.5.2: + optional: true + + impit-linux-x64-gnu@0.5.2: + optional: true + + impit-linux-x64-musl@0.5.2: + optional: true + + impit-win32-arm64-msvc@0.5.2: + optional: true + + impit-win32-x64-msvc@0.5.2: + optional: true + + impit@0.5.2: + optionalDependencies: + impit-darwin-arm64: 0.5.2 + impit-darwin-x64: 0.5.2 + impit-linux-arm64-gnu: 0.5.2 + impit-linux-arm64-musl: 0.5.2 + impit-linux-x64-gnu: 0.5.2 + impit-linux-x64-musl: 0.5.2 + impit-win32-arm64-msvc: 0.5.2 + impit-win32-x64-msvc: 0.5.2 + + imurmurhash@0.1.4: + optional: true + + indent-string@4.0.0: + optional: true + + infer-owner@1.0.4: + optional: true + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + optional: true + inherits@2.0.4: {} ini@1.3.8: {} @@ -4084,16 +4942,30 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-lambda@1.0.1: + optional: true + is-number@7.0.0: {} + is-obj@2.0.0: {} + + is-standalone-pwa@0.1.1: {} + isarray@1.0.0: {} + isexe@2.0.0: + optional: true + jiti@2.4.2: {} js-cookie@3.0.5: {} js-tokens@4.0.0: {} + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + jsbn@1.1.0: {} jsesc@3.1.0: {} @@ -4110,6 +4982,12 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + language-subtag-registry@0.3.23: {} + + language-tags@2.1.0: + dependencies: + language-subtag-registry: 0.3.23 + lightningcss-darwin-arm64@1.30.1: optional: true @@ -4181,6 +5059,8 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.0 + lodash.isequal@4.5.0: {} + lodash@4.17.21: {} log-update@6.1.0: @@ -4195,17 +5075,58 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + optional: true + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 make-error@1.3.6: {} + make-fetch-happen@9.1.0: + dependencies: + agentkeepalive: 4.6.0 + cacache: 15.3.0 + http-cache-semantics: 4.2.0 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + math-intrinsics@1.1.0: {} + + maxmind@4.3.28: + dependencies: + mmdb-lib: 2.2.1 + tiny-lru: 11.3.3 + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mimic-function@5.0.1: {} mimic-response@3.1.0: {} @@ -4216,16 +5137,60 @@ snapshots: minimist@1.2.8: {} + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-fetch@1.4.1: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + optional: true + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + optional: true + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + minipass@7.1.2: {} + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + minizlib@3.0.2: dependencies: minipass: 7.1.2 mkdirp-classic@0.5.3: {} + mkdirp@1.0.4: {} + mkdirp@3.0.1: {} + mmdb-lib@2.2.1: {} + ms@2.1.3: {} multistream@4.1.0: @@ -4239,6 +5204,9 @@ snapshots: napi-build-utils@2.0.0: {} + negotiator@0.6.4: + optional: true + next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -4273,9 +5241,30 @@ snapshots: dependencies: semver: 7.7.2 - node-fetch@2.7.0: + node-addon-api@7.1.1: {} + + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-gyp@8.4.1: + dependencies: + env-paths: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 9.1.0 + nopt: 5.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.7.2 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true node-int64@0.4.0: {} @@ -4294,8 +5283,21 @@ snapshots: touch: 3.1.1 undefsafe: 2.0.5 + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + optional: true + normalize-path@3.0.0: {} + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + optional: true + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -4304,8 +5306,24 @@ snapshots: dependencies: mimic-function: 5.0.1 + ow@0.28.2: + dependencies: + '@sindresorhus/is': 4.6.0 + callsites: 3.1.0 + dot-prop: 6.0.1 + lodash.isequal: 4.5.0 + vali-date: 1.0.0 + p-is-promise@3.0.0: {} + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + optional: true + + path-is-absolute@1.0.1: + optional: true + path-parse@1.0.7: {} picocolors@1.1.1: {} @@ -4316,6 +5334,8 @@ snapshots: pidtree@0.6.0: {} + playwright-core@1.53.2: {} + postcss@8.4.31: dependencies: nanoid: 3.3.11 @@ -4347,6 +5367,15 @@ snapshots: progress@2.0.3: {} + promise-inflight@1.0.1: + optional: true + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + optional: true + proxy-chain@2.5.9: dependencies: socks: 2.8.5 @@ -4446,8 +5475,16 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: + optional: true + rfdc@1.4.1: {} + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + optional: true + rollup@4.44.2: dependencies: '@types/estree': 1.0.8 @@ -4478,6 +5515,11 @@ snapshots: safe-buffer@5.2.1: {} + safer-buffer@2.1.2: + optional: true + + sax@1.4.1: {} + scheduler@0.26.0: {} screenfull@5.2.0: {} @@ -4486,6 +5528,9 @@ snapshots: semver@7.7.2: {} + set-blocking@2.0.0: + optional: true + sharp@0.34.2: dependencies: color: 4.2.3 @@ -4515,6 +5560,9 @@ snapshots: '@img/sharp-win32-x64': 0.34.2 optional: true + signal-exit@3.0.7: + optional: true + signal-exit@4.1.0: {} simple-concat@1.0.1: {} @@ -4546,6 +5594,15 @@ snapshots: smart-buffer@4.2.0: {} + socks-proxy-agent@6.2.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.1(supports-color@5.5.0) + socks: 2.8.5 + transitivePeerDependencies: + - supports-color + optional: true + socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 @@ -4568,6 +5625,23 @@ snapshots: sprintf-js@1.1.3: {} + sqlite3@5.1.7: + dependencies: + bindings: 1.5.0 + node-addon-api: 7.1.1 + prebuild-install: 7.1.3 + tar: 6.2.1 + optionalDependencies: + node-gyp: 8.4.1 + transitivePeerDependencies: + - bluebird + - supports-color + + ssri@8.0.1: + dependencies: + minipass: 3.3.6 + optional: true + stream-meter@1.0.4: dependencies: readable-stream: 2.3.8 @@ -4646,6 +5720,15 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + tar@7.4.3: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -4659,6 +5742,8 @@ snapshots: dependencies: '@tauri-apps/api': 2.6.0 + tiny-lru@11.3.3: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.5(picomatch@4.0.2) @@ -4715,10 +5800,32 @@ snapshots: typescript@5.8.3: {} + ua-is-frozen@0.1.2: {} + + ua-parser-js@2.0.4(encoding@0.1.13): + dependencies: + '@types/node-fetch': 2.6.12 + detect-europe-js: 0.1.2 + is-standalone-pwa: 0.1.1 + node-fetch: 2.7.0(encoding@0.1.13) + ua-is-frozen: 0.1.2 + transitivePeerDependencies: + - encoding + undefsafe@2.0.5: {} undici-types@7.8.0: {} + unique-filename@1.1.1: + dependencies: + unique-slug: 2.0.2 + optional: true + + unique-slug@2.0.2: + dependencies: + imurmurhash: 0.1.4 + optional: true + universalify@2.0.1: {} unzipper@0.12.3: @@ -4754,6 +5861,8 @@ snapshots: v8-compile-cache-lib@3.0.1: {} + vali-date@1.0.0: {} + vite@6.2.0(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0): dependencies: esbuild: 0.25.5 @@ -4773,6 +5882,16 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which@2.0.2: + dependencies: + isexe: 2.0.0 + optional: true + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + optional: true + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -4787,10 +5906,19 @@ snapshots: wrappy@1.0.2: {} + xml2js@0.6.2: + dependencies: + sax: 1.4.1 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + y18n@5.0.8: {} yallist@3.1.1: {} + yallist@4.0.0: {} + yallist@5.0.0: {} yaml@2.8.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2c2c8de..a2b6b1e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,9 +1,9 @@ packages: - - "nodecar" - + - nodecar onlyBuiltDependencies: - - "@biomejs/biome" - - "@tailwindcss/oxide" + - '@biomejs/biome' + - '@tailwindcss/oxide' - esbuild - sharp + - sqlite3 - unrs-resolver diff --git a/src-tauri/src/api_client.rs b/src-tauri/src/api_client.rs index 4814e9d..27b3eb3 100644 --- a/src-tauri/src/api_client.rs +++ b/src-tauri/src/api_client.rs @@ -259,6 +259,14 @@ pub fn is_browser_version_nightly( // Chromium builds are generally stable snapshots false } + "camoufox" => { + // For Camoufox, all releases are generally stable unless marked as prerelease + if let Some(name) = release_name { + name.to_lowercase().contains("alpha") + } else { + false + } + } _ => { // Default fallback is_nightly_version(version) @@ -856,6 +864,31 @@ impl ApiClient { } /// Check if a Brave release has compatible assets for the given platform and architecture + fn has_compatible_camoufox_asset( + &self, + assets: &[crate::browser::GithubAsset], + os: &str, + arch: &str, + ) -> bool { + let (os_name, arch_name) = match (os, arch) { + ("windows", "x64") => ("win", "x86_64"), + ("windows", "arm64") => ("win", "arm64"), + ("linux", "x64") => ("lin", "x86_64"), + ("linux", "arm64") => ("lin", "arm64"), + ("macos", "x64") => ("mac", "x86_64"), + ("macos", "arm64") => ("mac", "arm64"), + _ => return false, + }; + + // Look for assets matching the pattern: camoufox-{version}-{release}-{os}.{arch}.zip + assets.iter().any(|asset| { + let name = asset.name.to_lowercase(); + name.starts_with("camoufox-") + && name.contains(&format!("-{os_name}.{arch_name}.zip")) + && name.ends_with(".zip") + }) + } + fn has_compatible_brave_asset( assets: &[crate::browser::GithubAsset], os: &str, @@ -996,6 +1029,128 @@ impl ApiClient { ) } + pub async fn fetch_camoufox_releases_with_caching( + &self, + no_caching: bool, + ) -> Result, Box> { + // Check cache first (unless bypassing) + if !no_caching { + if let Some(cached_releases) = self.load_cached_github_releases("camoufox") { + println!( + "Using cached Camoufox releases, count: {}", + cached_releases.len() + ); + return Ok(cached_releases); + } + } + + println!("Fetching Camoufox releases from GitHub API..."); + let url = format!( + "{}/repos/daijro/camoufox/releases?per_page=100", + self.github_api_base + ); + + let response = self + .client + .get(url) + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") + .send() + .await?; + + if !response.status().is_success() { + return Err(format!("GitHub API returned status: {}", response.status()).into()); + } + + // Get the response text first for better error reporting + let response_text = response.text().await?; + + // Try to parse the JSON with better error handling + let releases: Vec = match serde_json::from_str(&response_text) { + Ok(releases) => releases, + Err(e) => { + eprintln!("Failed to parse GitHub API response for Camoufox releases:"); + eprintln!("Error: {e}"); + eprintln!( + "Response text (first 500 chars): {}", + if response_text.len() > 500 { + &response_text[..500] + } else { + &response_text + } + ); + return Err(format!("Failed to parse GitHub API response: {e}").into()); + } + }; + + println!( + "Fetched {} total Camoufox releases from GitHub", + releases.len() + ); + + // Get platform info to filter appropriate releases + let (os, arch) = Self::get_platform_info(); + println!("Filtering for platform: {os}/{arch}"); + + // Filter releases that have assets compatible with the current platform + let mut compatible_releases: Vec = releases + .into_iter() + .enumerate() + .filter_map(|(i, release)| { + let has_compatible = self.has_compatible_camoufox_asset(&release.assets, &os, &arch); + if !has_compatible { + println!( + "Release {} ({}) has no compatible assets for {}/{}", + i, release.tag_name, os, arch + ); + println!( + " Available assets: {:?}", + release.assets.iter().map(|a| &a.name).collect::>() + ); + } + if has_compatible { + Some(release) + } else { + None + } + }) + .collect(); + + println!( + "After platform filtering: {} compatible releases", + compatible_releases.len() + ); + + // Sort by version (latest first) with debugging + println!( + "Before sorting: {:?}", + compatible_releases + .iter() + .map(|r| &r.tag_name) + .take(10) + .collect::>() + ); + sort_github_releases(&mut compatible_releases); + println!( + "After sorting: {:?}", + compatible_releases + .iter() + .map(|r| &r.tag_name) + .take(10) + .collect::>() + ); + + // Cache the results (unless bypassing cache) + if !no_caching { + if let Err(e) = self.save_cached_github_releases("camoufox", &compatible_releases) { + eprintln!("Failed to cache Camoufox releases: {e}"); + } else { + println!("Cached {} Camoufox releases", compatible_releases.len()); + } + } + + Ok(compatible_releases) + } + pub async fn fetch_tor_releases_with_caching( &self, no_caching: bool, @@ -1798,4 +1953,43 @@ mod tests { let result = client.fetch_zen_releases_with_caching(true).await; assert!(result.is_err()); } + + #[test] + fn test_camoufox_beta_version_parsing() { + // Test specific Camoufox beta versions that are causing issues + let v22 = VersionComponent::parse("135.0.5beta22"); + let v24 = VersionComponent::parse("135.0.5beta24"); + + println!("v22: {v22:?}"); + println!("v24: {v24:?}"); + + // v24 should be greater than v22 + assert!( + v24 > v22, + "135.0.5beta24 should be greater than 135.0.5beta22" + ); + + // Test other beta version combinations + let v1 = VersionComponent::parse("135.0.5beta1"); + let v2 = VersionComponent::parse("135.0.5beta2"); + assert!(v2 > v1, "135.0.5beta2 should be greater than 135.0.5beta1"); + + // Test sorting of multiple versions + let mut versions = vec![ + "135.0.5beta22".to_string(), + "135.0.5beta24".to_string(), + "135.0.5beta23".to_string(), + "135.0.5beta21".to_string(), + ]; + + sort_versions(&mut versions); + + println!("Sorted versions: {versions:?}"); + + // Should be sorted from newest to oldest + assert_eq!(versions[0], "135.0.5beta24"); + assert_eq!(versions[1], "135.0.5beta23"); + assert_eq!(versions[2], "135.0.5beta22"); + assert_eq!(versions[3], "135.0.5beta21"); + } } diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index 8dfccb7..3e2558a 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -522,6 +522,7 @@ mod tests { proxy_id: None, last_launch: None, release_type: "stable".to_string(), + camoufox_config: None, } } diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index 07beb4d..e13530e 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -19,6 +19,7 @@ pub enum BrowserType { Brave, Zen, TorBrowser, + Camoufox, } impl BrowserType { @@ -31,6 +32,7 @@ impl BrowserType { BrowserType::Brave => "brave", BrowserType::Zen => "zen", BrowserType::TorBrowser => "tor-browser", + BrowserType::Camoufox => "camoufox", } } @@ -43,6 +45,7 @@ impl BrowserType { "brave" => Ok(BrowserType::Brave), "zen" => Ok(BrowserType::Zen), "tor-browser" => Ok(BrowserType::TorBrowser), + "camoufox" => Ok(BrowserType::Camoufox), _ => Err(format!("Unknown browser type: {s}")), } } @@ -89,6 +92,7 @@ mod macos { || name.starts_with("mullvad") || name.starts_with("zen") || name.starts_with("tor") + || name.starts_with("camoufox") || name.contains("Browser") }) .map(|entry| entry.path()) @@ -192,6 +196,12 @@ mod linux { browser_subdir.join("firefox-bin"), ] } + BrowserType::Camoufox => { + vec![ + browser_subdir.join("camoufox"), + browser_subdir.join("camoufox-bin"), + ] + } _ => vec![], }; @@ -274,6 +284,12 @@ mod linux { browser_subdir.join("firefox"), ] } + BrowserType::Camoufox => { + vec![ + browser_subdir.join("camoufox-bin"), + browser_subdir.join("camoufox"), + ] + } _ => vec![], }; @@ -358,6 +374,7 @@ mod windows { || name.starts_with("mullvad") || name.starts_with("zen") || name.starts_with("tor") + || name.starts_with("camoufox") || name.contains("browser") { return Ok(path); @@ -436,6 +453,7 @@ mod windows { || name.starts_with("mullvad") || name.starts_with("zen") || name.starts_with("tor") + || name.starts_with("camoufox") || name.contains("browser") { return true; @@ -532,7 +550,10 @@ impl Browser for FirefoxBrowser { BrowserType::MullvadBrowser | BrowserType::TorBrowser => { args.push("-no-remote".to_string()); } - BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen => { + BrowserType::Firefox + | BrowserType::FirefoxDeveloper + | BrowserType::Zen + | BrowserType::Camoufox => { // Don't use -no-remote so we can communicate with existing instances } _ => {} @@ -693,6 +714,81 @@ impl Browser for ChromiumBrowser { } } +pub struct CamoufoxBrowser; + +impl CamoufoxBrowser { + pub fn new() -> Self { + Self + } +} + +impl Browser for CamoufoxBrowser { + fn get_executable_path(&self, install_dir: &Path) -> Result> { + #[cfg(target_os = "macos")] + return macos::get_firefox_executable_path(install_dir); + + #[cfg(target_os = "linux")] + return linux::get_firefox_executable_path(install_dir, &BrowserType::Camoufox); + + #[cfg(target_os = "windows")] + return windows::get_firefox_executable_path(install_dir); + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + Err("Unsupported platform".into()) + } + + fn create_launch_args( + &self, + profile_path: &str, + _proxy_settings: Option<&ProxySettings>, + url: Option, + ) -> Result, Box> { + // For Camoufox, we handle launching through the camoufox launcher + // This method won't be used directly, but we provide basic Firefox args as fallback + let mut args = vec![ + "-profile".to_string(), + profile_path.to_string(), + "-no-remote".to_string(), + ]; + + if let Some(url) = url { + args.push(url); + } + + Ok(args) + } + + fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool { + let install_dir = binaries_dir.join("camoufox").join(version); + + #[cfg(target_os = "macos")] + return macos::is_firefox_version_downloaded(&install_dir); + + #[cfg(target_os = "linux")] + return linux::is_firefox_version_downloaded(&install_dir, &BrowserType::Camoufox); + + #[cfg(target_os = "windows")] + return windows::is_firefox_version_downloaded(&install_dir); + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + false + } + + fn prepare_executable(&self, executable_path: &Path) -> Result<(), Box> { + #[cfg(target_os = "macos")] + return macos::prepare_executable(executable_path); + + #[cfg(target_os = "linux")] + return linux::prepare_executable(executable_path); + + #[cfg(target_os = "windows")] + return windows::prepare_executable(executable_path); + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + Err("Unsupported platform".into()) + } +} + // Factory function to create browser instances pub fn create_browser(browser_type: BrowserType) -> Box { match browser_type { @@ -702,6 +798,7 @@ pub fn create_browser(browser_type: BrowserType) -> Box { | BrowserType::Zen | BrowserType::TorBrowser => Box::new(FirefoxBrowser::new(browser_type)), BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)), + BrowserType::Camoufox => Box::new(CamoufoxBrowser::new()), } } @@ -778,6 +875,7 @@ mod tests { assert_eq!(BrowserType::Brave.as_str(), "brave"); assert_eq!(BrowserType::Zen.as_str(), "zen"); assert_eq!(BrowserType::TorBrowser.as_str(), "tor-browser"); + assert_eq!(BrowserType::Camoufox.as_str(), "camoufox"); // Test from_str assert_eq!( @@ -802,6 +900,10 @@ mod tests { BrowserType::from_str("tor-browser").unwrap(), BrowserType::TorBrowser ); + assert_eq!( + BrowserType::from_str("camoufox").unwrap(), + BrowserType::Camoufox + ); // Test invalid browser type assert!(BrowserType::from_str("invalid").is_err()); diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 8dbc777..47d0768 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -13,6 +13,7 @@ use crate::browser::{create_browser, BrowserType, ProxySettings}; use crate::browser_version_service::{ BrowserVersionInfo, BrowserVersionService, BrowserVersionsResult, }; +use crate::camoufox::CamoufoxConfig; use crate::download::{DownloadProgress, Downloader}; use crate::downloaded_browsers::DownloadedBrowsersRegistry; use crate::extraction::Extractor; @@ -31,13 +32,15 @@ pub struct BrowserProfile { pub last_launch: Option, #[serde(default = "default_release_type")] pub release_type: String, // "stable" or "nightly" + #[serde(default)] + pub camoufox_config: Option, // Camoufox configuration } fn default_release_type() -> String { "stable".to_string() } -// Global state to track currently downloading browsers +// Global state to track currently downloading browser-version pairs lazy_static::lazy_static! { static ref DOWNLOADING_BROWSERS: Arc>> = Arc::new(Mutex::new(HashSet::new())); } @@ -1347,6 +1350,7 @@ impl BrowserRunner { version: &str, release_type: &str, proxy_id: Option, + camoufox_config: Option, ) -> Result> { println!("Attempting to create profile: {name}"); @@ -1379,6 +1383,7 @@ impl BrowserRunner { process_id: None, last_launch: None, release_type: release_type.to_string(), + camoufox_config: camoufox_config.clone(), }; // Save profile info @@ -1772,10 +1777,109 @@ impl BrowserRunner { pub async fn launch_browser( &self, + app_handle: tauri::AppHandle, profile: &BrowserProfile, url: Option, local_proxy_settings: Option<&ProxySettings>, ) -> Result> { + // Handle camoufox profiles specially + if profile.browser == "camoufox" { + if let Some(mut camoufox_config) = profile.camoufox_config.clone() { + // Handle proxy settings for camoufox + if let Some(proxy_id) = &profile.proxy_id { + if let Some(stored_proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) { + println!("Starting proxy for Camoufox profile: {}", profile.name); + + // Start the proxy and get local proxy settings + let local_proxy = PROXY_MANAGER + .start_proxy( + app_handle.clone(), + &stored_proxy, + 0, // Use 0 as temporary PID, will be updated later + Some(&profile.name), + ) + .await + .map_err(|e| format!("Failed to start proxy for Camoufox: {e}"))?; + + // Format proxy URL for camoufox + let proxy_url = format!( + "{}://{}:{}", + if stored_proxy.proxy_type == "socks5" || stored_proxy.proxy_type == "socks4" { + &stored_proxy.proxy_type + } else { + "http" + }, + local_proxy.host, + local_proxy.port + ); + + // Add username and password if available + let proxy_url = if let (Some(username), Some(password)) = + (&stored_proxy.username, &stored_proxy.password) + { + format!( + "{}://{}:{}@{}:{}", + if stored_proxy.proxy_type == "socks5" || stored_proxy.proxy_type == "socks4" { + &stored_proxy.proxy_type + } else { + "http" + }, + username, + password, + local_proxy.host, + local_proxy.port + ) + } else { + proxy_url + }; + + // Set proxy in camoufox config + camoufox_config.proxy = Some(proxy_url); + + println!("Configured proxy for Camoufox: {:?}", camoufox_config.proxy); + } + } + + // Use the camoufox launcher + let camoufox_result = crate::camoufox::launch_camoufox_profile( + app_handle.clone(), + profile.clone(), + camoufox_config, + url, + ) + .await + .map_err(|e| -> Box { + format!("Failed to launch camoufox: {e}").into() + })?; + + // Update proxy with actual PID if proxy was started + if let Some(pid) = camoufox_result.pid { + if profile.proxy_id.is_some() { + if let Err(e) = PROXY_MANAGER.update_proxy_pid(0, pid) { + println!("Warning: Failed to update proxy PID: {e}"); + } + } + } + + // Update profile with the process info from camoufox result + let mut updated_profile = profile.clone(); + updated_profile.process_id = camoufox_result.pid; + updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); + + // Save the updated profile + self.save_process_info(&updated_profile)?; + + // 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(updated_profile); + } else { + return Err("Camoufox profile missing configuration".into()); + } + } + // Create browser instance let browser_type = BrowserType::from_str(&profile.browser) .map_err(|_| format!("Invalid browser type: {}", profile.browser))?; @@ -1853,91 +1957,85 @@ impl BrowserRunner { // For TOR and Mullvad browsers, we need to find the actual browser process // because they use launcher scripts that spawn the real browser process - let actual_pid = if matches!( + let mut actual_pid = launcher_pid; + + if matches!( browser_type, BrowserType::TorBrowser | BrowserType::MullvadBrowser ) { - println!("Waiting for TOR/Mullvad browser to fully start..."); - - // Wait a bit for the browser to fully start + // Wait a moment for the actual browser process to start tokio::time::sleep(tokio::time::Duration::from_millis(3000)).await; - // Search for the actual browser process + // Find the actual browser process let system = System::new_all(); - let mut found_pid: Option = None; + for (pid, process) in system.processes() { + let process_name = process.name().to_str().unwrap_or(""); + let process_cmd = process.cmd(); + let pid_u32 = pid.as_u32(); - // Try multiple times to find the process as it might take time to start - for attempt in 1..=5 { - println!("Attempt {attempt} to find actual browser process..."); - - for (pid, process) in system.processes() { - let cmd = process.cmd(); - if cmd.len() >= 2 { - // Check if this is the right browser executable - let exe_name = process.name().to_string_lossy().to_lowercase(); - let is_correct_browser = match profile.browser.as_str() { - "mullvad-browser" => { - self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser") - } - "tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"), - _ => false, - }; - - if !is_correct_browser { - continue; - } - - // Check for profile path match - let profile_path_match = cmd.iter().any(|s| { - let arg = s.to_str().unwrap_or(""); - arg == profile_data_path.to_string_lossy() - || arg == format!("-profile={}", profile_data_path.to_string_lossy()) - || (arg == "-profile" - && cmd - .iter() - .any(|s2| s2.to_str().unwrap_or("") == profile_data_path.to_string_lossy())) - }); - - if profile_path_match { - found_pid = Some(pid.as_u32()); - println!( - "Found actual browser process with PID: {} for profile: {}", - pid.as_u32(), - profile.name - ); - break; - } - } + // Skip if this is the launcher process itself + if pid_u32 == launcher_pid { + continue; } - if found_pid.is_some() { + if self.is_tor_or_mullvad_browser(process_name, process_cmd, &profile.browser) { + println!( + "Found actual {} browser process: PID {} ({})", + profile.browser, pid_u32, process_name + ); + actual_pid = pid_u32; break; } - - // Wait before next attempt - tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; } + } - found_pid.unwrap_or(launcher_pid) - } else { - // For other browsers, the launcher PID is usually the actual browser PID - launcher_pid - }; - - // Update profile with process info + // Update profile with process info and save let mut updated_profile = profile.clone(); updated_profile.process_id = Some(actual_pid); updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); - // Save the updated profile - self - .save_process_info(&updated_profile) - .expect("Failed to save process info"); + self.save_process_info(&updated_profile)?; + + // Apply proxy settings if needed (for Firefox-based browsers) + if profile.proxy_id.is_some() + && matches!( + browser_type, + BrowserType::Firefox + | BrowserType::FirefoxDeveloper + | BrowserType::Zen + | BrowserType::TorBrowser + | BrowserType::MullvadBrowser + ) + { + // Proxy settings for Firefox-based browsers are applied via user.js file + // which is already handled in the profile creation process + } + + // Start proxy if configured and needed (for Chromium-based browsers) + if let Some(proxy_id) = &profile.proxy_id { + if let Some(stored_proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) { + println!("Starting proxy for profile: {}", profile.name); + + match PROXY_MANAGER + .start_proxy( + app_handle.clone(), + &stored_proxy, + actual_pid, + Some(&profile.name), + ) + .await + { + Ok(_) => println!("Proxy started successfully for profile: {}", profile.name), + Err(e) => println!("Warning: Failed to start proxy: {e}"), + } + } + } + + // 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}"); + } - println!( - "Browser launched successfully with PID: {} for profile: {}", - actual_pid, profile.name - ); Ok(updated_profile) } @@ -1948,8 +2046,43 @@ impl BrowserRunner { url: &str, _internal_proxy_settings: Option<&ProxySettings>, ) -> Result<(), Box> { - // Use the comprehensive browser status check - let is_running = self.check_browser_status(app_handle, profile).await?; + // Handle camoufox profiles specially + 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(); + + // Check if the process is running + match camoufox_launcher + .find_camoufox_by_profile(&profile_path_str) + .await + { + Ok(Some(_camoufox_process)) => { + println!( + "Opening URL in existing Camoufox process for profile: {}", + 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()); + } + Ok(None) => { + return Err("Camoufox browser is not running".into()); + } + Err(e) => { + return Err(format!("Error checking Camoufox process: {e}").into()); + } + } + } + + // Use the comprehensive browser status check for non-camoufox browsers + let is_running = self + .check_browser_status(app_handle.clone(), profile) + .await?; if !is_running { return Err("Browser is not running".into()); @@ -2105,6 +2238,10 @@ impl BrowserRunner { #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] return Err("Unsupported platform".into()); } + 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()) + } } } @@ -2159,7 +2296,7 @@ impl BrowserRunner { } match self .open_url_in_existing_browser( - app_handle, + app_handle.clone(), &final_profile, url_ref, internal_proxy_settings, @@ -2188,7 +2325,7 @@ impl BrowserRunner { final_profile.browser ); // Fallback to launching a new instance for other browsers - self.launch_browser(&final_profile, url, internal_proxy_settings).await + self.launch_browser(app_handle.clone(), &final_profile, url, internal_proxy_settings).await } } } @@ -2197,7 +2334,12 @@ impl BrowserRunner { // This case shouldn't happen since we checked is_some() above, but handle it gracefully println!("URL was unexpectedly None, launching new browser instance"); self - .launch_browser(&final_profile, url, internal_proxy_settings) + .launch_browser( + app_handle.clone(), + &final_profile, + url, + internal_proxy_settings, + ) .await } } else { @@ -2208,7 +2350,12 @@ impl BrowserRunner { println!("Launching new browser instance - no URL provided"); } self - .launch_browser(&final_profile, url, internal_proxy_settings) + .launch_browser( + app_handle.clone(), + &final_profile, + url, + internal_proxy_settings, + ) .await } } @@ -2242,9 +2389,15 @@ impl BrowserRunner { Ok(profile) } - fn save_process_info(&self, profile: &BrowserProfile) -> Result<(), Box> { + fn save_process_info( + &self, + profile: &BrowserProfile, + ) -> Result<(), Box> { // Use the regular save_profile method which handles the UUID structure - self.save_profile(profile) + self.save_profile(profile).map_err(|e| { + let error_string = e.to_string(); + Box::new(std::io::Error::other(error_string)) as Box + }) } pub fn delete_profile(&self, profile_name: &str) -> Result<(), Box> { @@ -2294,6 +2447,79 @@ 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); + } + } + } + + // For non-camoufox browsers, use the existing logic let mut inner_profile = profile.clone(); let system = System::new_all(); let mut is_running = false; @@ -2416,67 +2642,21 @@ impl BrowserRunner { if let Some(pid) = found_pid { if inner_profile.process_id != Some(pid) { inner_profile.process_id = Some(pid); - if let Err(e) = self.save_process_info(&inner_profile) { - println!("Warning: Failed to update process info: {e}"); - } else { - println!( - "Updated process ID for profile '{}' to: {}", - inner_profile.name, pid - ); + if let Err(e) = self.save_profile(&inner_profile) { + println!("Warning: Failed to update profile with new PID: {e}"); } } - } else if is_running { - println!("Browser is running but no PID found - this shouldn't happen"); - } else { - // Browser is not running, clear the PID if it was set - if inner_profile.process_id.is_some() { - inner_profile.process_id = None; - if let Err(e) = self.save_process_info(&inner_profile) { - println!("Warning: Failed to clear process info: {e}"); - } else { - println!("Cleared process ID for profile '{}'", inner_profile.name); - } + } else if inner_profile.process_id.is_some() { + // Clear the PID if no process found + inner_profile.process_id = None; + if let Err(e) = self.save_profile(&inner_profile) { + println!("Warning: Failed to clear profile PID: {e}"); } } - // Handle proxy management based on browser status - if let Some(proxy_id) = &inner_profile.proxy_id { - if let Some(proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) { - if is_running { - // Browser is running, check if proxy is active - let proxy_active = PROXY_MANAGER - .get_proxy_settings(inner_profile.process_id.unwrap_or(0)) - .is_some(); - - if !proxy_active { - // Browser is running but proxy is not - restart the proxy - match PROXY_MANAGER - .start_proxy( - app_handle, - &proxy, - inner_profile.process_id.unwrap(), - Some(&inner_profile.name), - ) - .await - { - Ok(_) => { - println!("Restarted proxy for profile {}", inner_profile.name); - } - Err(e) => { - eprintln!( - "Failed to restart proxy for profile {}: {}", - inner_profile.name, e - ); - } - } - } - } else { - // Browser is not running, stop the proxy if it exists - if let Some(pid) = profile.process_id { - let _ = PROXY_MANAGER.stop_proxy(app_handle, pid).await; - } - } - } + // Emit profile update event to frontend + if let Err(e) = app_handle.emit("profile-updated", &inner_profile) { + println!("Warning: Failed to emit profile update event: {e}"); } Ok(is_running) @@ -2487,7 +2667,87 @@ impl BrowserRunner { app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result<(), Box> { - // Get the current process ID + // Handle camoufox profiles specially + 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!( + "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()); + } + } + } + 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()); + } + } + + // Stop proxy if one was running for this profile + if let Some(pid) = profile.process_id { + if let Err(e) = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await { + println!("Warning: Failed to stop proxy for Camoufox profile: {e}"); + } + } + + // Clear the process ID from the profile + let mut updated_profile = profile.clone(); + updated_profile.process_id = None; + self + .save_process_info(&updated_profile) + .map_err(|e| format!("Failed to update profile: {e}"))?; + + // 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(()); + } + + // For non-camoufox browsers, use the existing logic let pid = if let Some(pid) = profile.process_id { pid } else { @@ -2554,7 +2814,7 @@ impl BrowserRunner { println!("Attempting to kill browser process with PID: {pid}"); // Stop any associated proxy first - if let Err(e) = PROXY_MANAGER.stop_proxy(app_handle, pid).await { + if let Err(e) = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await { println!("Warning: Failed to stop proxy for PID {pid}: {e}"); } @@ -2667,16 +2927,17 @@ impl BrowserRunner { browser_str: String, version: String, ) -> Result> { - // Check if this browser type is already being downloaded + // Check if this browser-version pair is already being downloaded + let download_key = format!("{browser_str}-{version}"); { let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); - if downloading.contains(&browser_str) { + if downloading.contains(&download_key) { return Err(format!( - "Browser '{browser_str}' is already being downloaded. Please wait for the current download to complete." + "Browser '{browser_str}' version '{version}' is already being downloaded. Please wait for the current download to complete." ).into()); } - // Mark this browser as being downloaded - downloading.insert(browser_str.clone()); + // Mark this browser-version pair as being downloaded + downloading.insert(download_key.clone()); } let browser_type = @@ -2762,10 +3023,10 @@ impl BrowserRunner { // Clean up failed download let _ = registry.cleanup_failed_download(&browser_str, &version); let _ = registry.save(); - // Remove browser from downloading set on error + // Remove browser-version pair from downloading set on error { let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); - downloading.remove(&browser_str); + downloading.remove(&download_key); } return Err(format!("Failed to download browser: {e}").into()); } @@ -2794,10 +3055,10 @@ impl BrowserRunner { // Clean up failed download let _ = registry.cleanup_failed_download(&browser_str, &version); let _ = registry.save(); - // Remove browser from downloading set on error + // Remove browser-version pair from downloading set on error { let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); - downloading.remove(&browser_str); + downloading.remove(&download_key); } return Err(format!("Failed to extract browser: {e}").into()); } @@ -2828,10 +3089,10 @@ impl BrowserRunner { if !browser.is_version_downloaded(&version, &binaries_dir) { let _ = registry.cleanup_failed_download(&browser_str, &version); let _ = registry.save(); - // Remove browser from downloading set on verification failure + // Remove browser-version pair from downloading set on verification failure { let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); - downloading.remove(&browser_str); + downloading.remove(&download_key); } return Err("Browser download completed but verification failed".into()); } @@ -2850,6 +3111,26 @@ impl BrowserRunner { .save() .map_err(|e| format!("Failed to save registry: {e}"))?; + // If this is Camoufox, automatically download GeoIP database + if browser_str == "camoufox" { + use crate::geoip_downloader::GeoIPDownloader; + + // Check if GeoIP database is already available + if !GeoIPDownloader::is_geoip_database_available() { + println!("Downloading GeoIP database for Camoufox..."); + + let geoip_downloader = GeoIPDownloader::new(); + if let Err(e) = geoip_downloader.download_geoip_database(&app_handle).await { + eprintln!("Warning: Failed to download GeoIP database: {e}"); + // Don't fail the browser download if GeoIP download fails + } else { + println!("GeoIP database downloaded successfully"); + } + } else { + println!("GeoIP database already available"); + } + } + // Emit completion let progress = DownloadProgress { browser: browser_str.clone(), @@ -2863,10 +3144,10 @@ impl BrowserRunner { }; let _ = app_handle.emit("download-progress", &progress); - // Remove browser from downloading set + // Remove browser-version pair from downloading set { let mut downloading = DOWNLOADING_BROWSERS.lock().unwrap(); - downloading.remove(&browser_str); + downloading.remove(&download_key); } Ok(version) @@ -2899,6 +3180,34 @@ 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 { @@ -2915,10 +3224,18 @@ pub fn create_browser_profile( version: String, release_type: String, proxy_id: Option, + camoufox_config: Option, ) -> Result { let browser_runner = BrowserRunner::new(); browser_runner - .create_profile(&name, &browser, &version, &release_type, proxy_id) + .create_profile( + &name, + &browser, + &version, + &release_type, + proxy_id, + camoufox_config, + ) .map_err(|e| format!("Failed to create profile: {e}")) } @@ -3207,6 +3524,7 @@ pub fn create_browser_profile_new( version: String, release_type: String, proxy_id: Option, + camoufox_config: Option, ) -> Result { let browser_type = BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; @@ -3216,6 +3534,7 @@ pub fn create_browser_profile_new( version, release_type, proxy_id, + camoufox_config, ) } @@ -3313,7 +3632,7 @@ mod tests { let (runner, _temp_dir) = create_test_browser_runner(); let profile = runner - .create_profile("Test Profile", "firefox", "139.0", "stable", None) + .create_profile("Test Profile", "firefox", "139.0", "stable", None, None) .unwrap(); assert_eq!(profile.name, "Test Profile"); @@ -3342,6 +3661,7 @@ mod tests { "139.0", "stable", None, // Tests now use separate proxy storage system + None, // No camoufox config for this test ) .unwrap(); @@ -3355,7 +3675,7 @@ mod tests { let (runner, _temp_dir) = create_test_browser_runner(); let profile = runner - .create_profile("Test Save Load", "firefox", "139.0", "stable", None) + .create_profile("Test Save Load", "firefox", "139.0", "stable", None, None) .unwrap(); // Save the profile @@ -3375,7 +3695,7 @@ mod tests { // Create profile let _ = runner - .create_profile("Original Name", "firefox", "139.0", "stable", None) + .create_profile("Original Name", "firefox", "139.0", "stable", None, None) .unwrap(); // Rename profile @@ -3395,7 +3715,7 @@ mod tests { // Create profile let _ = runner - .create_profile("To Delete", "firefox", "139.0", "stable", None) + .create_profile("To Delete", "firefox", "139.0", "stable", None, None) .unwrap(); // Verify profile exists @@ -3422,6 +3742,7 @@ mod tests { "139.0", "stable", None, + None, ) .unwrap(); @@ -3444,13 +3765,13 @@ mod tests { // Create multiple profiles let _ = runner - .create_profile("Profile 1", "firefox", "139.0", "stable", None) + .create_profile("Profile 1", "firefox", "139.0", "stable", None, None) .unwrap(); let _ = runner - .create_profile("Profile 2", "chromium", "1465660", "stable", None) + .create_profile("Profile 2", "chromium", "1465660", "stable", None, None) .unwrap(); let _ = runner - .create_profile("Profile 3", "brave", "v1.81.9", "stable", None) + .create_profile("Profile 3", "brave", "v1.81.9", "stable", None, None) .unwrap(); // List profiles @@ -3469,10 +3790,10 @@ mod tests { // Test that we can't rename to an existing profile name let _ = runner - .create_profile("Profile 1", "firefox", "139.0", "stable", None) + .create_profile("Profile 1", "firefox", "139.0", "stable", None, None) .unwrap(); let _ = runner - .create_profile("Profile 2", "firefox", "139.0", "stable", None) + .create_profile("Profile 2", "firefox", "139.0", "stable", None, None) .unwrap(); // Try to rename profile2 to profile1's name (should fail) @@ -3493,6 +3814,7 @@ mod tests { "139.0", "stable", None, + None, ) .unwrap(); @@ -3526,6 +3848,7 @@ mod tests { "139.0", "stable", None, // Tests now use separate proxy storage system + None, // No camoufox config for this test ) .unwrap(); diff --git a/src-tauri/src/browser_version_service.rs b/src-tauri/src/browser_version_service.rs index 3118cd3..c7f0648 100644 --- a/src-tauri/src/browser_version_service.rs +++ b/src-tauri/src/browser_version_service.rs @@ -87,6 +87,10 @@ impl BrowserVersionService { Ok(true) } } + "camoufox" => { + // Camoufox supports all platforms and architectures according to the JS code + Ok(true) + } _ => Err(format!("Unknown browser: {browser}").into()), } } @@ -101,6 +105,7 @@ impl BrowserVersionService { "brave", "chromium", "tor-browser", + "camoufox", ]; all_browsers @@ -237,6 +242,7 @@ impl BrowserVersionService { "brave" => self.fetch_brave_versions(true).await?, "chromium" => self.fetch_chromium_versions(true).await?, "tor-browser" => self.fetch_tor_versions(true).await?, + "camoufox" => self.fetch_camoufox_versions(true).await?, _ => return Err(format!("Unsupported browser: {browser}").into()), }; @@ -454,6 +460,27 @@ impl BrowserVersionService { }) .collect() } + "camoufox" => { + let releases = self.fetch_camoufox_releases_detailed(true).await?; + merged_versions + .into_iter() + .map(|version| { + if let Some(release) = releases.iter().find(|r| r.tag_name == version) { + BrowserVersionInfo { + version: release.tag_name.clone(), + is_prerelease: release.is_nightly, + date: release.published_at.clone(), + } + } else { + BrowserVersionInfo { + version: version.clone(), + is_prerelease: false, // Camoufox usually stable releases + date: "".to_string(), + } + } + }) + .collect() + } _ => { return Err(format!("Unsupported browser: {browser}").into()); } @@ -727,6 +754,32 @@ impl BrowserVersionService { is_archive, }) } + "camoufox" => { + // Camoufox downloads from GitHub releases with pattern: camoufox-{version}-{release}-{os}.{arch}.zip + let (os_name, arch_name) = match (&os[..], &arch[..]) { + ("windows", "x64") => ("win", "x86_64"), + ("windows", "arm64") => ("win", "arm64"), + ("linux", "x64") => ("lin", "x86_64"), + ("linux", "arm64") => ("lin", "arm64"), + ("macos", "x64") => ("mac", "x86_64"), + ("macos", "arm64") => ("mac", "arm64"), + _ => { + return Err( + format!("Unsupported platform/architecture for Camoufox: {os}/{arch}").into(), + ) + } + }; + + // Note: We provide a placeholder URL here since Camoufox requires dynamic resolution + // The actual URL will be resolved in download.rs resolve_download_url + Ok(DownloadInfo { + url: format!( + "https://github.com/daijro/camoufox/releases/download/{version}/camoufox-{{version}}-{{release}}-{os_name}.{arch_name}.zip" + ), + filename: format!("camoufox-{version}-{os_name}.{arch_name}.zip"), + is_archive: true, + }) + } _ => Err(format!("Unsupported browser: {browser}").into()), } } @@ -889,6 +942,24 @@ impl BrowserVersionService { .fetch_tor_releases_with_caching(no_caching) .await } + + async fn fetch_camoufox_versions( + &self, + no_caching: bool, + ) -> Result, Box> { + let releases = self.fetch_camoufox_releases_detailed(no_caching).await?; + Ok(releases.into_iter().map(|r| r.tag_name).collect()) + } + + async fn fetch_camoufox_releases_detailed( + &self, + no_caching: bool, + ) -> Result, Box> { + self + .api_client + .fetch_camoufox_releases_with_caching(no_caching) + .await + } } #[cfg(test)] diff --git a/src-tauri/src/camoufox.rs b/src-tauri/src/camoufox.rs new file mode 100644 index 0000000..b36471d --- /dev/null +++ b/src-tauri/src/camoufox.rs @@ -0,0 +1,607 @@ +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/download.rs b/src-tauri/src/download.rs index 6bdca10..61e5bb4 100644 --- a/src-tauri/src/download.rs +++ b/src-tauri/src/download.rs @@ -147,6 +147,30 @@ impl Downloader { Ok(asset_url) } + BrowserType::Camoufox => { + // For Camoufox, verify the asset exists and find the correct download URL + let releases = self + .api_client + .fetch_camoufox_releases_with_caching(true) + .await?; + + let release = releases + .iter() + .find(|r| r.tag_name == version) + .ok_or(format!("Camoufox version {version} not found"))?; + + // Get platform and architecture info + let (os, arch) = Self::get_platform_info(); + + // Find the appropriate asset + let asset_url = self + .find_camoufox_asset(&release.assets, &os, &arch) + .ok_or(format!( + "No compatible asset found for Camoufox version {version} on {os}/{arch}" + ))?; + + Ok(asset_url) + } _ => { // For other browsers, use the provided URL Ok(download_info.url.clone()) @@ -321,6 +345,35 @@ impl Downloader { asset.map(|a| a.browser_download_url.clone()) } + /// Find the appropriate Camoufox asset for the current platform and architecture + fn find_camoufox_asset( + &self, + assets: &[crate::browser::GithubAsset], + os: &str, + arch: &str, + ) -> Option { + // Camoufox asset naming pattern: camoufox-{version}-{release}-{os}.{arch}.zip + let (os_name, arch_name) = match (os, arch) { + ("windows", "x64") => ("win", "x86_64"), + ("windows", "arm64") => ("win", "arm64"), + ("linux", "x64") => ("lin", "x86_64"), + ("linux", "arm64") => ("lin", "arm64"), + ("macos", "x64") => ("mac", "x86_64"), + ("macos", "arm64") => ("mac", "arm64"), + _ => return None, + }; + + // Look for assets matching the pattern + let asset = assets.iter().find(|asset| { + let name = asset.name.to_lowercase(); + name.starts_with("camoufox-") + && name.contains(&format!("-{os_name}.{arch_name}.zip")) + && name.ends_with(".zip") + }); + + asset.map(|a| a.browser_download_url.clone()) + } + pub async fn download_browser( &self, app_handle: &tauri::AppHandle, diff --git a/src-tauri/src/geoip_downloader.rs b/src-tauri/src/geoip_downloader.rs new file mode 100644 index 0000000..876be50 --- /dev/null +++ b/src-tauri/src/geoip_downloader.rs @@ -0,0 +1,171 @@ +use crate::browser::GithubRelease; +use directories::BaseDirs; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use tauri::Emitter; +use tokio::fs; +use tokio::io::AsyncWriteExt; + +const MMDB_REPO: &str = "P3TERX/GeoLite.mmdb"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GeoIPDownloadProgress { + pub stage: String, // "downloading", "extracting", "completed" + pub percentage: f64, + pub message: String, +} + +pub struct GeoIPDownloader { + client: Client, +} + +impl GeoIPDownloader { + pub fn new() -> Self { + Self { + client: Client::new(), + } + } + + fn get_cache_dir() -> Result> { + let base_dirs = BaseDirs::new().ok_or("Failed to determine base directories")?; + + #[cfg(target_os = "windows")] + let cache_dir = base_dirs + .data_local_dir() + .join("camoufox") + .join("camoufox") + .join("Cache"); + + #[cfg(target_os = "macos")] + let cache_dir = base_dirs.cache_dir().join("camoufox"); + + #[cfg(target_os = "linux")] + let cache_dir = base_dirs.cache_dir().join("camoufox"); + + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + let cache_dir = base_dirs.cache_dir().join("camoufox"); + + Ok(cache_dir) + } + + fn get_mmdb_file_path() -> Result> { + Ok(Self::get_cache_dir()?.join("GeoLite2-City.mmdb")) + } + + pub fn is_geoip_database_available() -> bool { + if let Ok(mmdb_path) = Self::get_mmdb_file_path() { + mmdb_path.exists() + } else { + false + } + } + + fn find_city_mmdb_asset(&self, release: &GithubRelease) -> Option { + for asset in &release.assets { + if asset.name.ends_with("-City.mmdb") { + return Some(asset.browser_download_url.clone()); + } + } + None + } + + pub async fn download_geoip_database( + &self, + app_handle: &tauri::AppHandle, + ) -> Result<(), Box> { + // Emit initial progress + let _ = app_handle.emit( + "geoip-download-progress", + GeoIPDownloadProgress { + stage: "downloading".to_string(), + percentage: 0.0, + message: "Starting GeoIP database download".to_string(), + }, + ); + + // Fetch latest release from GitHub + let releases = self.fetch_geoip_releases().await?; + let latest_release = releases.first().ok_or("No GeoIP database releases found")?; + + let download_url = self + .find_city_mmdb_asset(latest_release) + .ok_or("No compatible GeoIP database asset found")?; + + // Create cache directory + let cache_dir = Self::get_cache_dir()?; + fs::create_dir_all(&cache_dir).await?; + + let mmdb_path = Self::get_mmdb_file_path()?; + + // Download the file + let response = self.client.get(&download_url).send().await?; + + if !response.status().is_success() { + return Err( + format!( + "Failed to download GeoIP database: HTTP {}", + response.status() + ) + .into(), + ); + } + + let total_size = response.content_length().unwrap_or(0); + let mut downloaded = 0; + let mut file = fs::File::create(&mmdb_path).await?; + let mut stream = response.bytes_stream(); + + use futures_util::StreamExt; + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + downloaded += chunk.len() as u64; + file.write_all(&chunk).await?; + + if total_size > 0 { + let percentage = (downloaded as f64 / total_size as f64) * 100.0; + let _ = app_handle.emit( + "geoip-download-progress", + GeoIPDownloadProgress { + stage: "downloading".to_string(), + percentage, + message: format!("Downloaded {downloaded} / {total_size} bytes"), + }, + ); + } + } + + file.flush().await?; + + // Emit completion + let _ = app_handle.emit( + "geoip-download-progress", + GeoIPDownloadProgress { + stage: "completed".to_string(), + percentage: 100.0, + message: "GeoIP database download completed".to_string(), + }, + ); + + Ok(()) + } + + async fn fetch_geoip_releases( + &self, + ) -> Result, Box> { + let url = format!("https://api.github.com/repos/{MMDB_REPO}/releases"); + let response = self + .client + .get(&url) + .header("User-Agent", "Mozilla/5.0 (compatible; donutbrowser)") + .send() + .await?; + + if !response.status().is_success() { + return Err(format!("Failed to fetch releases: HTTP {}", response.status()).into()); + } + + let releases: Vec = response.json().await?; + Ok(releases) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ed91fcc..11448be 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,13 +13,17 @@ mod auto_updater; mod browser; mod browser_runner; mod browser_version_service; +mod camoufox; mod default_browser; mod download; mod downloaded_browsers; mod extraction; +mod geoip_downloader; + mod profile_importer; mod proxy_manager; mod settings_manager; +mod system_utils; mod theme_detector; mod version_updater; @@ -60,6 +64,8 @@ use profile_importer::{detect_existing_profiles, import_browser_profile}; use theme_detector::get_system_theme; +use system_utils::{get_system_locale, get_system_timezone}; + // Trait to extend WebviewWindow with transparent titlebar functionality pub trait WindowExt { #[cfg(target_os = "macos")] @@ -207,6 +213,17 @@ async fn delete_stored_proxy(proxy_id: String) -> Result<(), String> { .map_err(|e| format!("Failed to delete stored proxy: {e}")) } +#[tauri::command] +async fn update_camoufox_config( + profile_name: String, + config: crate::camoufox::CamoufoxConfig, +) -> Result<(), String> { + let browser_runner = browser_runner::BrowserRunner::new(); + browser_runner + .update_camoufox_config(&profile_name, config) + .map_err(|e| format!("Failed to update Camoufox config: {e}")) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let args: Vec = env::args().collect(); @@ -433,6 +450,9 @@ pub fn run() { get_stored_proxies, update_stored_proxy, delete_stored_proxy, + update_camoufox_config, + get_system_locale, + get_system_timezone, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 44b4f91..430d9a9 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -689,6 +689,7 @@ impl ProfileImporter { process_id: None, last_launch: None, release_type: "stable".to_string(), + camoufox_config: None, }; // Save the profile metadata diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index 0a48725..03f8e75 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -173,11 +173,6 @@ impl ProxyManager { } // Get a stored proxy by ID - #[allow(dead_code)] - pub fn get_stored_proxy(&self, proxy_id: &str) -> Option { - let stored_proxies = self.stored_proxies.lock().unwrap(); - stored_proxies.get(proxy_id).cloned() - } // Update a stored proxy pub fn update_stored_proxy( @@ -418,6 +413,7 @@ impl ProxyManager { } // Get proxy settings for a browser process ID + #[allow(dead_code)] pub fn get_proxy_settings(&self, browser_pid: u32) -> Option { let proxies = self.active_proxies.lock().unwrap(); proxies.get(&browser_pid).map(|proxy| ProxySettings { diff --git a/src-tauri/src/system_utils.rs b/src-tauri/src/system_utils.rs new file mode 100644 index 0000000..7b0fbcb --- /dev/null +++ b/src-tauri/src/system_utils.rs @@ -0,0 +1,331 @@ +use serde::{Deserialize, Serialize}; +use std::process::Command; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SystemLocale { + pub locale: String, + pub language: String, + pub country: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SystemTimezone { + pub timezone: String, + pub offset: String, +} + +pub struct SystemUtils; + +impl SystemUtils { + pub fn new() -> Self { + Self + } + + /// Detect the system's locale settings + pub fn detect_system_locale(&self) -> SystemLocale { + #[cfg(target_os = "macos")] + return macos::detect_system_locale(); + + #[cfg(target_os = "linux")] + return linux::detect_system_locale(); + + #[cfg(target_os = "windows")] + return windows::detect_system_locale(); + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + return SystemLocale { + locale: "en-US".to_string(), + language: "en".to_string(), + country: "US".to_string(), + }; + } + + /// Detect the system's timezone settings + pub fn detect_system_timezone(&self) -> SystemTimezone { + #[cfg(target_os = "macos")] + return macos::detect_system_timezone(); + + #[cfg(target_os = "linux")] + return linux::detect_system_timezone(); + + #[cfg(target_os = "windows")] + return windows::detect_system_timezone(); + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + return SystemTimezone { + timezone: "UTC".to_string(), + offset: "+00:00".to_string(), + }; + } +} + +#[cfg(target_os = "macos")] +mod macos { + use super::*; + + pub fn detect_system_locale() -> SystemLocale { + // Try to get the system locale from macOS + if let Ok(output) = Command::new("defaults") + .args(["read", "-g", "AppleLocale"]) + .output() + { + if output.status.success() { + let locale_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + return parse_locale(&locale_str); + } + } + + // Fallback to environment variables + detect_locale_from_env() + } + + pub fn detect_system_timezone() -> SystemTimezone { + // Try to get timezone from macOS system + if let Ok(output) = Command::new("date").arg("+%Z").output() { + if output.status.success() { + let tz_abbr = String::from_utf8_lossy(&output.stdout).trim().to_string(); + + // Get the full timezone name + if let Ok(tz_output) = Command::new("systemsetup").args(["-gettimezone"]).output() { + if tz_output.status.success() { + let tz_full = String::from_utf8_lossy(&tz_output.stdout); + if let Some(tz_name) = tz_full.strip_prefix("Time Zone: ") { + let tz_clean = tz_name.trim().to_string(); + if !tz_clean.is_empty() { + return SystemTimezone { + timezone: tz_clean, + offset: tz_abbr, + }; + } + } + } + } + } + } + + // Fallback to reading /etc/localtime link + detect_timezone_from_files() + } +} + +#[cfg(target_os = "linux")] +mod linux { + use super::*; + + pub fn detect_system_locale() -> SystemLocale { + // Try to get locale from locale command + if let Ok(output) = Command::new("locale").output() { + if output.status.success() { + let output_str = String::from_utf8_lossy(&output.stdout); + for line in output_str.lines() { + if line.starts_with("LANG=") { + let locale_value = line.strip_prefix("LANG=").unwrap_or(""); + let locale_clean = locale_value.trim_matches('"'); + return parse_locale(locale_clean); + } + } + } + } + + // Fallback to environment variables + detect_locale_from_env() + } + + pub fn detect_system_timezone() -> SystemTimezone { + // Try to read /etc/timezone first (Debian/Ubuntu) + if let Ok(tz_content) = std::fs::read_to_string("/etc/timezone") { + let tz_name = tz_content.trim().to_string(); + if !tz_name.is_empty() { + return SystemTimezone { + timezone: tz_name, + offset: get_timezone_offset(), + }; + } + } + + // Try timedatectl (systemd systems) + if let Ok(output) = Command::new("timedatectl") + .args(["show", "--property=Timezone", "--value"]) + .output() + { + if output.status.success() { + let tz_name = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !tz_name.is_empty() { + return SystemTimezone { + timezone: tz_name, + offset: get_timezone_offset(), + }; + } + } + } + + // Fallback to reading /etc/localtime symlink + detect_timezone_from_files() + } + + fn get_timezone_offset() -> String { + if let Ok(output) = Command::new("date").arg("+%z").output() { + if output.status.success() { + return String::from_utf8_lossy(&output.stdout).trim().to_string(); + } + } + "+00:00".to_string() + } +} + +#[cfg(target_os = "windows")] +mod windows { + use super::*; + + pub fn detect_system_locale() -> SystemLocale { + // Try to get locale from Windows registry/powershell + if let Ok(output) = Command::new("powershell") + .args([ + "-Command", + "Get-Culture | Select-Object -ExpandProperty Name", + ]) + .output() + { + if output.status.success() { + let locale_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + return parse_locale(&locale_str); + } + } + + // Fallback to environment variables + detect_locale_from_env() + } + + pub fn detect_system_timezone() -> SystemTimezone { + // Try to get timezone from Windows + if let Ok(output) = Command::new("powershell") + .args([ + "-Command", + "Get-TimeZone | Select-Object -ExpandProperty Id", + ]) + .output() + { + if output.status.success() { + let tz_id = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !tz_id.is_empty() { + return SystemTimezone { + timezone: tz_id, + offset: get_windows_timezone_offset(), + }; + } + } + } + + // Fallback + SystemTimezone { + timezone: "UTC".to_string(), + offset: "+00:00".to_string(), + } + } + + fn get_windows_timezone_offset() -> String { + if let Ok(output) = Command::new("powershell") + .args([ + "-Command", + "Get-TimeZone | Select-Object -ExpandProperty BaseUtcOffset", + ]) + .output() + { + if output.status.success() { + let offset_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + // Convert Windows offset format to standard format + if let Some(colon_pos) = offset_str.find(':') { + let hours = &offset_str[..colon_pos]; + let minutes = &offset_str[colon_pos + 1..]; + if let (Ok(h), Ok(m)) = (hours.parse::(), minutes.parse::()) { + return format!("{:+03d}:{:02d}", h, m); + } + } + } + } + "+00:00".to_string() + } +} + +// Helper functions used across platforms +fn parse_locale(locale_str: &str) -> SystemLocale { + // Remove encoding suffix if present (e.g., "en_US.UTF-8" -> "en_US") + let locale_base = locale_str.split('.').next().unwrap_or(locale_str); + + // Split language and country (e.g., "en_US" -> ["en", "US"]) + let parts: Vec<&str> = locale_base.split(&['_', '-']).collect(); + + let language = parts.first().unwrap_or(&"en").to_string(); + let country = parts.get(1).unwrap_or(&"US").to_string(); + + // Convert to standard format (e.g., "en-US") + let standard_locale = if parts.len() >= 2 { + format!("{}-{}", language, country.to_uppercase()) + } else { + format!("{language}-US") + }; + + SystemLocale { + locale: standard_locale, + language, + country: country.to_uppercase(), + } +} + +fn detect_locale_from_env() -> SystemLocale { + // Check environment variables in order of preference + let env_vars = ["LANG", "LC_ALL", "LC_CTYPE", "LANGUAGE"]; + + for var in &env_vars { + if let Ok(value) = std::env::var(var) { + if !value.is_empty() { + return parse_locale(&value); + } + } + } + + // Default fallback + SystemLocale { + locale: "en-US".to_string(), + language: "en".to_string(), + country: "US".to_string(), + } +} + +fn detect_timezone_from_files() -> SystemTimezone { + // Try to read timezone from /etc/localtime symlink + if let Ok(link_target) = std::fs::read_link("/etc/localtime") { + if let Some(tz_path) = link_target.to_str() { + // Extract timezone name from path like /usr/share/zoneinfo/America/New_York + if let Some(zoneinfo_pos) = tz_path.find("zoneinfo/") { + let tz_name = &tz_path[zoneinfo_pos + 9..]; + if !tz_name.is_empty() { + return SystemTimezone { + timezone: tz_name.to_string(), + offset: "+00:00".to_string(), // Could be improved with actual offset calculation + }; + } + } + } + } + + // Default fallback + SystemTimezone { + timezone: "UTC".to_string(), + offset: "+00:00".to_string(), + } +} + +/// Tauri command to get system locale +#[tauri::command] +pub async fn get_system_locale() -> Result { + let utils = SystemUtils::new(); + Ok(utils.detect_system_locale()) +} + +/// Tauri command to get system timezone +#[tauri::command] +pub async fn get_system_timezone() -> Result { + let utils = SystemUtils::new(); + Ok(utils.detect_system_timezone()) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index c99fddf..8392719 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { FaDownload } from "react-icons/fa"; import { FiWifi } from "react-icons/fi"; import { GoGear, GoKebabHorizontal, GoPlus } from "react-icons/go"; +import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog"; import { ChangeVersionDialog } from "@/components/change-version-dialog"; import { CreateProfileDialog } from "@/components/create-profile-dialog"; import { ImportProfileDialog } from "@/components/import-profile-dialog"; @@ -36,7 +37,7 @@ import { useUpdateNotifications } from "@/hooks/use-update-notifications"; import { useVersionUpdater } from "@/hooks/use-version-updater"; import { showErrorToast } from "@/lib/toast-utils"; import { sleep } from "@/lib/utils"; -import type { BrowserProfile } from "@/types"; +import type { BrowserProfile, CamoufoxConfig } from "@/types"; type BrowserTypeString = | "mullvad-browser" @@ -45,7 +46,8 @@ type BrowserTypeString = | "chromium" | "brave" | "zen" - | "tor-browser"; + | "tor-browser" + | "camoufox"; interface PendingUrl { id: string; @@ -62,11 +64,15 @@ export default function Home() { const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false); const [proxyManagementDialogOpen, setProxyManagementDialogOpen] = useState(false); + const [camoufoxConfigDialogOpen, setCamoufoxConfigDialogOpen] = + useState(false); const [pendingUrls, setPendingUrls] = useState([]); const [currentProfileForProxy, setCurrentProfileForProxy] = useState(null); const [currentProfileForVersionChange, setCurrentProfileForVersionChange] = useState(null); + const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] = + useState(null); const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false); const [permissionDialogOpen, setPermissionDialogOpen] = useState(false); const [currentPermissionType, setCurrentPermissionType] = @@ -326,6 +332,30 @@ export default function Home() { setChangeVersionDialogOpen(true); }, []); + const handleConfigureCamoufox = useCallback((profile: BrowserProfile) => { + setCurrentProfileForCamoufoxConfig(profile); + setCamoufoxConfigDialogOpen(true); + }, []); + + const handleSaveCamoufoxConfig = useCallback( + async (profile: BrowserProfile, config: CamoufoxConfig) => { + setError(null); + try { + await invoke("update_camoufox_config", { + profileName: profile.name, + config, + }); + await loadProfiles(); + setCamoufoxConfigDialogOpen(false); + } catch (err: unknown) { + console.error("Failed to update camoufox config:", err); + setError(`Failed to update camoufox config: ${JSON.stringify(err)}`); + throw err; + } + }, + [loadProfiles], + ); + const handleSaveProxy = useCallback( async (proxyId: string | null) => { setProxyDialogOpen(false); @@ -356,6 +386,7 @@ export default function Home() { version: string; releaseType: string; proxyId?: string; + camoufoxConfig?: CamoufoxConfig; }) => { setError(null); @@ -368,6 +399,7 @@ export default function Home() { version: profileData.version, releaseType: profileData.releaseType, proxyId: profileData.proxyId, + camoufoxConfig: profileData.camoufoxConfig, }, ); @@ -658,6 +690,7 @@ export default function Home() { onDeleteProfile={handleDeleteProfile} onRenameProfile={handleRenameProfile} onChangeVersion={openChangeVersionDialog} + onConfigureCamoufox={handleConfigureCamoufox} runningProfiles={runningProfiles} isUpdating={isUpdating} onReloadProxyData={ @@ -739,6 +772,15 @@ export default function Home() { permissionType={currentPermissionType} onPermissionGranted={checkNextPermission} /> + + { + setCamoufoxConfigDialogOpen(false); + }} + profile={currentProfileForCamoufoxConfig} + onSave={handleSaveCamoufoxConfig} + /> ); } diff --git a/src/components/camoufox-config-dialog.tsx b/src/components/camoufox-config-dialog.tsx new file mode 100644 index 0000000..bf2a365 --- /dev/null +++ b/src/components/camoufox-config-dialog.tsx @@ -0,0 +1,502 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { BrowserProfile, CamoufoxConfig } from "@/types"; + +interface CamoufoxConfigDialogProps { + isOpen: boolean; + onClose: () => void; + profile: BrowserProfile | null; + onSave: (profile: BrowserProfile, config: CamoufoxConfig) => Promise; +} + +const osOptions = [ + { value: "windows", label: "Windows" }, + { value: "macos", label: "macOS" }, + { value: "linux", label: "Linux" }, +]; + +const timezoneOptions = [ + { value: "America/New_York", label: "America/New_York" }, + { value: "America/Los_Angeles", label: "America/Los_Angeles" }, + { value: "Europe/London", label: "Europe/London" }, + { value: "Europe/Paris", label: "Europe/Paris" }, + { value: "Asia/Tokyo", label: "Asia/Tokyo" }, + { value: "Asia/Shanghai", label: "Asia/Shanghai" }, + { value: "Australia/Sydney", label: "Australia/Sydney" }, +]; + +const localeOptions = [ + { value: "en-US", label: "English (US)" }, + { value: "en-GB", label: "English (UK)" }, + { value: "fr-FR", label: "French" }, + { value: "de-DE", label: "German" }, + { value: "es-ES", label: "Spanish" }, + { value: "it-IT", label: "Italian" }, + { value: "ja-JP", label: "Japanese" }, + { value: "zh-CN", label: "Chinese (Simplified)" }, +]; + +const getCurrentOS = () => { + if (typeof window !== "undefined") { + const userAgent = window.navigator.userAgent; + if (userAgent.includes("Win")) return "windows"; + if (userAgent.includes("Mac")) return "macos"; + if (userAgent.includes("Linux")) return "linux"; + } + return "unknown"; +}; + +export function CamoufoxConfigDialog({ + isOpen, + onClose, + profile, + onSave, +}: CamoufoxConfigDialogProps) { + const [config, setConfig] = useState({ + enable_cache: true, + os: [getCurrentOS()], + }); + const [isSaving, setIsSaving] = useState(false); + + // Initialize config when profile changes + useEffect(() => { + if (profile && profile.browser === "camoufox") { + setConfig( + profile.camoufox_config || { + enable_cache: true, + os: [getCurrentOS()], + }, + ); + } + }, [profile]); + + const updateConfig = (key: keyof CamoufoxConfig, value: unknown) => { + setConfig((prev) => ({ ...prev, [key]: value })); + }; + + const handleSave = async () => { + if (!profile) return; + + setIsSaving(true); + try { + await onSave(profile, config); + onClose(); + } catch (error) { + console.error("Failed to save camoufox config:", error); + } finally { + setIsSaving(false); + } + }; + + const handleClose = () => { + // Reset config to original when closing without saving + if (profile && profile.browser === "camoufox") { + setConfig( + profile.camoufox_config || { + enable_cache: true, + os: [getCurrentOS()], + }, + ); + } + onClose(); + }; + + if (!profile || profile.browser !== "camoufox") { + return null; + } + + // Get the selected OS for warning + const selectedOS = config.os?.[0]; + const currentOS = getCurrentOS(); + const showOSWarning = + selectedOS && selectedOS !== currentOS && currentOS !== "unknown"; + + return ( + + + + + Configure Camoufox Settings - {profile.name} + + + + +
+ {/* Operating System */} +
+ + + {showOSWarning && ( +
+

+ ⚠️ Warning: Spoofing OS features is detectable by advanced + anti-bot systems. Some platform-specific APIs and behaviors + cannot be fully replicated. +

+
+ )} +
+ + {/* Blocking Options */} +
+ +
+
+ + updateConfig("block_images", checked) + } + /> + +
+
+ + updateConfig("block_webrtc", checked) + } + /> + +
+
+ + updateConfig("block_webgl", checked) + } + /> + +
+
+
+ + {/* Geolocation */} +
+ +
+
+ + + updateConfig("country", e.target.value || undefined) + } + placeholder="e.g., US, GB, DE" + /> +
+
+ + +
+
+
+
+ + + updateConfig( + "latitude", + e.target.value ? parseFloat(e.target.value) : undefined, + ) + } + placeholder="e.g., 40.7128" + /> +
+
+ + + updateConfig( + "longitude", + e.target.value ? parseFloat(e.target.value) : undefined, + ) + } + placeholder="e.g., -74.0060" + /> +
+
+
+ + {/* Localization */} +
+ + +
+ + {/* Screen Resolution */} +
+ +
+
+ + + updateConfig( + "screen_min_width", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 1024" + /> +
+
+ + + updateConfig( + "screen_max_width", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 1920" + /> +
+
+
+
+ + + updateConfig( + "screen_min_height", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 768" + /> +
+
+ + + updateConfig( + "screen_max_height", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 1080" + /> +
+
+
+ + {/* Window Size */} +
+ +
+
+ + + updateConfig( + "window_width", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 1366" + /> +
+
+ + + updateConfig( + "window_height", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 768" + /> +
+
+
+ + {/* Advanced Options */} +
+ +
+
+ + updateConfig("enable_cache", checked) + } + /> + +
+
+ + updateConfig("main_world_eval", checked) + } + /> + +
+
+
+ + {/* WebGL Settings */} +
+ +
+
+ + + updateConfig("webgl_vendor", e.target.value || undefined) + } + placeholder="e.g., Intel Inc." + /> +
+
+ + + updateConfig( + "webgl_renderer", + e.target.value || undefined, + ) + } + placeholder="e.g., Intel Iris OpenGL Engine" + /> +
+
+
+ + {/* Debug Options */} +
+ +
+ updateConfig("debug", checked)} + /> + +
+
+
+
+ + + + + +
+
+ ); +} diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index ecf5fa4..8cabb2a 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -2,12 +2,10 @@ import { invoke } from "@tauri-apps/api/core"; import { useCallback, useEffect, useState } from "react"; -import { FiPlus } from "react-icons/fi"; -import { toast } from "sonner"; import { LoadingButton } from "@/components/loading-button"; -import { ProxyFormDialog } from "@/components/proxy-form-dialog"; -import { ReleaseTypeSelector } from "@/components/release-type-selector"; +import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form"; import { Button } from "@/components/ui/button"; +import { Combobox } from "@/components/ui/combobox"; import { Dialog, DialogContent, @@ -17,6 +15,7 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, @@ -24,16 +23,10 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useBrowserDownload } from "@/hooks/use-browser-download"; -import { useBrowserSupport } from "@/hooks/use-browser-support"; -import { getBrowserDisplayName } from "@/lib/browser-utils"; -import type { BrowserProfile, BrowserReleaseTypes, StoredProxy } from "@/types"; -import { Alert, AlertDescription } from "./ui/alert"; +import { getBrowserIcon } from "@/lib/browser-utils"; +import type { BrowserReleaseTypes, CamoufoxConfig, StoredProxy } from "@/types"; type BrowserTypeString = | "mullvad-browser" @@ -42,7 +35,8 @@ type BrowserTypeString = | "chromium" | "brave" | "zen" - | "tor-browser"; + | "tor-browser" + | "camoufox"; interface CreateProfileDialogProps { isOpen: boolean; @@ -53,503 +47,431 @@ interface CreateProfileDialogProps { version: string; releaseType: string; proxyId?: string; + camoufoxConfig?: CamoufoxConfig; }) => Promise; } +interface BrowserOption { + value: BrowserTypeString; + label: string; + description: string; +} + +const browserOptions: BrowserOption[] = [ + { + value: "firefox", + label: "Firefox", + description: "Mozilla's main web browser", + }, + { + value: "firefox-developer", + label: "Firefox Developer Edition", + description: "Browser for developers with cutting-edge features", + }, + { + value: "chromium", + label: "Chromium", + description: "Open-source version of Chrome", + }, + { + value: "brave", + label: "Brave", + description: "Privacy-focused browser with ad blocking", + }, + { + value: "zen", + label: "Zen Browser", + description: "Beautiful, customizable Firefox-based browser", + }, + { + value: "mullvad-browser", + label: "Mullvad Browser", + description: "Privacy browser by Mullvad VPN", + }, + { + value: "tor-browser", + label: "Tor Browser", + description: "Browse anonymously through the Tor network", + }, +]; + +const getCurrentOS = () => { + if (typeof window !== "undefined") { + const userAgent = window.navigator.userAgent; + if (userAgent.includes("Win")) return "windows"; + if (userAgent.includes("Mac")) return "macos"; + if (userAgent.includes("Linux")) return "linux"; + } + return "unknown"; +}; + export function CreateProfileDialog({ isOpen, onClose, onCreateProfile, }: CreateProfileDialogProps) { const [profileName, setProfileName] = useState(""); - const [selectedBrowser, setSelectedBrowser] = - useState("mullvad-browser"); - const [selectedReleaseType, setSelectedReleaseType] = useState< - "stable" | "nightly" | null - >(null); - const [releaseTypes, setReleaseTypes] = useState({ - stable: undefined, - nightly: undefined, + const [activeTab, setActiveTab] = useState("regular"); + + // Regular browser states + const [selectedBrowser, setSelectedBrowser] = useState(); + const [selectedProxyId, setSelectedProxyId] = useState(); + + // Camoufox anti-detect states + const [camoufoxConfig, setCamoufoxConfig] = useState({ + enable_cache: true, // Cache enabled by default + os: [getCurrentOS()], // Default to current OS }); - const [isCreating, setIsCreating] = useState(false); - const [existingProfiles, setExistingProfiles] = useState( - [], - ); - const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false); - // Proxy settings - now using stored proxy selection - const [selectedProxyId, setSelectedProxyId] = useState(null); + // Common states + const [availableReleaseTypes, setAvailableReleaseTypes] = + useState({}); + const [camoufoxReleaseTypes, setCamoufoxReleaseTypes] = + useState({}); + const [supportedBrowsers, setSupportedBrowsers] = useState([]); const [storedProxies, setStoredProxies] = useState([]); - const [isLoadingProxies, setIsLoadingProxies] = useState(false); - const [showProxyForm, setShowProxyForm] = useState(false); + const [isCreating, setIsCreating] = useState(false); + // Use the browser download hook const { + isBrowserDownloading, downloadBrowser, - isDownloading, - downloadedVersions, loadDownloadedVersions, isVersionDownloaded, } = useBrowserDownload(); - const { - supportedBrowsers, - isLoading: isLoadingSupport, - isBrowserSupported, - } = useBrowserSupport(); - - useEffect(() => { - if (supportedBrowsers.length > 0) { - // Set default browser to first supported browser - if (supportedBrowsers.includes("mullvad-browser")) { - setSelectedBrowser("mullvad-browser"); - } else if (supportedBrowsers.length > 0) { - setSelectedBrowser(supportedBrowsers[0] as BrowserTypeString); - } - } - }, [supportedBrowsers]); - - // Set default release type when release types are loaded - useEffect(() => { - if (!selectedReleaseType && Object.keys(releaseTypes).length > 0) { - // First try to set stable if it exists - if (releaseTypes.stable) { - setSelectedReleaseType("stable"); - } - // If stable doesn't exist but nightly does, set nightly as default - else if (releaseTypes.nightly && selectedBrowser !== "chromium") { - setSelectedReleaseType("nightly"); - } - } - }, [releaseTypes, selectedReleaseType, selectedBrowser]); - - const loadExistingProfiles = useCallback(async () => { + const loadSupportedBrowsers = useCallback(async () => { try { - const profiles = await invoke("list_browser_profiles"); - setExistingProfiles(profiles); + const browsers = await invoke("get_supported_browsers"); + setSupportedBrowsers(browsers); } catch (error) { - console.error("Failed to load existing profiles:", error); + console.error("Failed to load supported browsers:", error); } }, []); const loadStoredProxies = useCallback(async () => { try { - setIsLoadingProxies(true); const proxies = await invoke("get_stored_proxies"); setStoredProxies(proxies); } catch (error) { console.error("Failed to load stored proxies:", error); - toast.error("Failed to load available proxies"); - } finally { - setIsLoadingProxies(false); } }, []); - const loadReleaseTypes = useCallback(async (browser: string) => { - try { - setIsLoadingReleaseTypes(true); - const types = await invoke( - "get_browser_release_types", - { - browserStr: browser, - }, - ); - setReleaseTypes(types); - } catch (error) { - console.error("Failed to load release types:", error); - toast.error("Failed to load available versions"); - } finally { - setIsLoadingReleaseTypes(false); - } - }, []); + const loadReleaseTypes = useCallback( + async (browser: string) => { + try { + const releaseTypes = await invoke( + "get_browser_release_types", + { browserStr: browser }, + ); - const handleDownload = useCallback(async () => { - if (!selectedBrowser || !selectedReleaseType) return; + if (browser === "camoufox") { + setCamoufoxReleaseTypes(releaseTypes); + } else { + setAvailableReleaseTypes(releaseTypes); + } - const version = - selectedReleaseType === "stable" - ? releaseTypes.stable - : releaseTypes.nightly; - if (!version) return; - - await downloadBrowser(selectedBrowser, version); - }, [selectedBrowser, selectedReleaseType, downloadBrowser, releaseTypes]); - - const validateProfileName = useCallback( - (name: string): string | null => { - const trimmedName = name.trim(); - - if (!trimmedName) { - return "Profile name cannot be empty"; + // Load downloaded versions for this browser + await loadDownloadedVersions(browser); + } catch (error) { + console.error(`Failed to load release types for ${browser}:`, error); } - - // Check for duplicate names (case insensitive) - const isDuplicate = existingProfiles.some( - (profile) => profile.name.toLowerCase() === trimmedName.toLowerCase(), - ); - - if (isDuplicate) { - return "A profile with this name already exists"; - } - - return null; }, - [existingProfiles], + [loadDownloadedVersions], ); - // Helper to determine if proxy should be disabled for the selected browser - const isProxyDisabled = selectedBrowser === "tor-browser"; - - // Update proxy selection when browser changes to tor-browser + // Load data when dialog opens useEffect(() => { - if (selectedBrowser === "tor-browser" && selectedProxyId) { - setSelectedProxyId(null); + if (isOpen) { + void loadSupportedBrowsers(); + void loadStoredProxies(); + // Load camoufox release types when dialog opens + void loadReleaseTypes("camoufox"); } - }, [selectedBrowser, selectedProxyId]); + }, [isOpen, loadSupportedBrowsers, loadStoredProxies, loadReleaseTypes]); - const handleCreateProxy = useCallback(() => { - setShowProxyForm(true); - }, []); + // Load release types when browser selection changes + useEffect(() => { + if (selectedBrowser) { + void loadReleaseTypes(selectedBrowser); + } + }, [selectedBrowser, loadReleaseTypes]); - const handleProxySaved = useCallback((savedProxy: StoredProxy) => { - setStoredProxies((prev) => { - const existingIndex = prev.findIndex((p) => p.id === savedProxy.id); - if (existingIndex >= 0) { - // Update existing proxy - const updated = [...prev]; - updated[existingIndex] = savedProxy; - return updated; - } else { - // Add new proxy - return [...prev, savedProxy]; - } - }); - setSelectedProxyId(savedProxy.id); - setShowProxyForm(false); - }, []); + const handleDownload = async (browserStr: string) => { + const releaseTypes = + browserStr === "camoufox" ? camoufoxReleaseTypes : availableReleaseTypes; + const latestStableVersion = releaseTypes.stable; - const handleProxyFormClose = useCallback(() => { - setShowProxyForm(false); - }, []); - - const handleCreate = useCallback(async () => { - if (!profileName.trim() || !selectedBrowser || !selectedReleaseType) return; - - // Validate profile name - const nameError = validateProfileName(profileName); - if (nameError) { - toast.error(nameError); + if (!latestStableVersion) { + console.error("No stable version available for download"); return; } - const version = - selectedReleaseType === "stable" - ? releaseTypes.stable - : releaseTypes.nightly; - if (!version) { - toast.error("Selected release type is not available"); - return; + try { + await downloadBrowser(browserStr, latestStableVersion); + } catch (error) { + console.error("Failed to download browser:", error); } + }; + + const handleCreate = async () => { + if (!profileName.trim()) return; setIsCreating(true); try { - await onCreateProfile({ - name: profileName.trim(), - browserStr: selectedBrowser, - version, - releaseType: selectedReleaseType, - proxyId: isProxyDisabled ? undefined : (selectedProxyId ?? undefined), - }); + if (activeTab === "regular") { + if (!selectedBrowser) { + console.error("Missing required browser selection"); + return; + } - // Reset form - setProfileName(""); - setSelectedReleaseType(null); - setSelectedProxyId(null); - onClose(); + // Use the latest stable version by default + const latestStableVersion = availableReleaseTypes.stable; + if (!latestStableVersion) { + console.error("No stable version available"); + return; + } + + await onCreateProfile({ + name: profileName.trim(), + browserStr: selectedBrowser, + version: latestStableVersion, + releaseType: "stable", + proxyId: selectedProxyId, + }); + } else { + // Anti-detect tab - always use Camoufox with latest version + const latestCamoufoxVersion = camoufoxReleaseTypes.stable; + if (!latestCamoufoxVersion) { + console.error("No Camoufox version available"); + return; + } + + await onCreateProfile({ + name: profileName.trim(), + browserStr: "camoufox" as BrowserTypeString, + version: latestCamoufoxVersion, + releaseType: "stable", + proxyId: selectedProxyId, + camoufoxConfig, + }); + } + + handleClose(); } catch (error) { console.error("Failed to create profile:", error); } finally { setIsCreating(false); } - }, [ - profileName, - selectedBrowser, - selectedReleaseType, - onCreateProfile, - isProxyDisabled, - selectedProxyId, - onClose, - releaseTypes.nightly, - releaseTypes.stable, - validateProfileName, - ]); + }; - const nameError = profileName.trim() - ? validateProfileName(profileName) - : null; + const handleClose = () => { + // Reset all states + setProfileName(""); + setSelectedBrowser(undefined); + setSelectedProxyId(undefined); + setCamoufoxConfig({ + enable_cache: true, + os: [getCurrentOS()], // Reset to current OS + }); + setActiveTab("regular"); + onClose(); + }; - const selectedVersion = - selectedReleaseType === "stable" - ? releaseTypes.stable - : releaseTypes.nightly; + const isCreateDisabled = () => { + if (!profileName.trim()) return true; - const canCreate = - profileName.trim() && - selectedBrowser && - selectedReleaseType && - selectedVersion && - isVersionDownloaded(selectedVersion) && - !nameError; - - useEffect(() => { - if (isOpen) { - void loadExistingProfiles(); - void loadStoredProxies(); + if (activeTab === "regular") { + return !selectedBrowser || !availableReleaseTypes.stable; + } else { + // For anti-detect, we need camoufox to be available + return !camoufoxReleaseTypes.stable; } - }, [isOpen, loadExistingProfiles, loadStoredProxies]); + }; - useEffect(() => { - if (isOpen && selectedBrowser) { - // Reset selected release type when browser changes - setSelectedReleaseType(null); - void loadReleaseTypes(selectedBrowser); - void loadDownloadedVersions(selectedBrowser); - } - }, [isOpen, selectedBrowser, loadDownloadedVersions, loadReleaseTypes]); + const updateCamoufoxConfig = (key: keyof CamoufoxConfig, value: unknown) => { + setCamoufoxConfig((prev) => ({ ...prev, [key]: value })); + }; + + // Check if browser version is downloaded and available + const isBrowserVersionAvailable = (browserStr: string) => { + const releaseTypes = + browserStr === "camoufox" ? camoufoxReleaseTypes : availableReleaseTypes; + const latestStableVersion = releaseTypes.stable; + return latestStableVersion && isVersionDownloaded(latestStableVersion); + }; + + // Get the selected OS for warning + const selectedOS = camoufoxConfig.os?.[0]; + const currentOS = getCurrentOS(); + const _showOSWarning = + selectedOS && selectedOS !== currentOS && currentOS !== "unknown"; return ( - <> - - - - Create New Profile - + + + + Create New Profile + -
- {/* Profile Name */} -
- - { - setProfileName(e.target.value); - }} - placeholder="Enter profile name" - className={nameError ? "border-red-500" : ""} - /> - {nameError &&

{nameError}

} -
+ + + Regular Browsers + Anti-Detect + - {/* Browser Selection */} -
- - setProfileName(e.target.value)} + placeholder="Enter profile name" + /> +
- if (!isSupported) { - return ( - - - - {displayName} (Not supported) - - - -

- {displayName} is not supported on your current - platform or architecture. -

-
-
- ); - } - - return ( - - {displayName} - - ); - })} - - -
- - {selectedBrowser ? ( -
- - {isLoadingReleaseTypes ? ( -
- Loading release types... -
- ) : Object.keys(releaseTypes).length === 0 ? ( - - - No releases are available for{" "} - {getBrowserDisplayName(selectedBrowser)}. - - - ) : ( -
- {(!releaseTypes.stable || !releaseTypes.nightly) && ( - - - Only {(releaseTypes.stable && "Stable") ?? "Nightly"}{" "} - releases are available for{" "} - {getBrowserDisplayName(selectedBrowser)}. - - - )} - - { - void handleDownload(); - }} - placeholder="Select release type..." - downloadedVersions={downloadedVersions} + +
+
+ + + supportedBrowsers.includes(browser.value), + ) + .map((browser) => { + const IconComponent = getBrowserIcon(browser.value); + return { + value: browser.value, + label: browser.label, + icon: IconComponent, + }; + })} + value={selectedBrowser || ""} + onValueChange={(value) => + setSelectedBrowser(value as BrowserTypeString) + } + placeholder="Select a browser..." + searchPlaceholder="Search browsers..." />
- )} -
- ) : null} - {/* Proxy Settings */} -
-
-
- - {!isProxyDisabled && ( - - - - - -

Create a new proxy configuration

-
-
+ {selectedBrowser && ( +
+ {!isBrowserVersionAvailable(selectedBrowser) && + availableReleaseTypes.stable && ( +
+

+ Latest stable version ( + {availableReleaseTypes.stable}) needs to be + downloaded +

+ handleDownload(selectedBrowser)} + isLoading={isBrowserDownloading(selectedBrowser)} + size="sm" + disabled={isBrowserDownloading(selectedBrowser)} + > + Download + +
+ )} + {isBrowserVersionAvailable(selectedBrowser) && ( +
+ ✓ Latest stable version ( + {availableReleaseTypes.stable}) is available +
+ )} +
)}
+ - {isProxyDisabled ? ( - - -
-

- Tor Browser has its own built-in proxy system and - doesn't support additional proxy configuration. + +

+ {/* 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 +
- - -

- Tor Browser manages its own proxy routing automatically -

-
- - ) : ( + )} + {isBrowserVersionAvailable("camoufox") && ( +
+ ✓ Camoufox version ({camoufoxReleaseTypes.stable}) is + available +
+ )} + + +
+ + + {/* Proxy Selection - Common to both tabs - Compact without card */} + {storedProxies.length > 0 && ( +
+ - )} - - {!isProxyDisabled && - storedProxies.length === 0 && - !isLoadingProxies && ( -

- No saved proxies available. Use the "Create Proxy" button - above to create proxy configurations. -

- )} -
+
+ )}
-
+ - - void handleCreate()} - disabled={!canCreate} + disabled={isCreateDisabled()} > Create Profile - -
- - - + +
+
); } diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 977ac64..61a87b9 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -58,6 +58,7 @@ interface ProfilesDataTableProps { onDeleteProfile: (profile: BrowserProfile) => void | Promise; onRenameProfile: (oldName: string, newName: string) => Promise; onChangeVersion: (profile: BrowserProfile) => void; + onConfigureCamoufox?: (profile: BrowserProfile) => void; runningProfiles: Set; isUpdating?: (browser: string) => boolean; onReloadProxyData?: () => void | Promise; @@ -71,6 +72,7 @@ export function ProfilesDataTable({ onDeleteProfile, onRenameProfile, onChangeVersion, + onConfigureCamoufox, runningProfiles, isUpdating = () => false, onReloadProxyData, @@ -447,7 +449,19 @@ export function ProfilesDataTable({ > Configure Proxy - {!["chromium", "zen"].includes(profile.browser) && ( + {profile.browser === "camoufox" && onConfigureCamoufox && ( + { + onConfigureCamoufox(profile); + }} + disabled={!isClient || isBrowserUpdating} + > + Configure Camoufox + + )} + {!["chromium", "zen", "camoufox"].includes( + profile.browser, + ) && ( { onChangeVersion(profile); @@ -492,6 +506,7 @@ export function ProfilesDataTable({ onKillProfile, onProxySettings, onChangeVersion, + onConfigureCamoufox, getProxyInfo, hasProxy, getProxyDisplayName, diff --git a/src/components/shared-camoufox-config-form.tsx b/src/components/shared-camoufox-config-form.tsx new file mode 100644 index 0000000..206eed8 --- /dev/null +++ b/src/components/shared-camoufox-config-form.tsx @@ -0,0 +1,568 @@ +"use client"; + +import { invoke } from "@tauri-apps/api/core"; +import { useEffect, useState } from "react"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { CamoufoxConfig } from "@/types"; + +const osOptions = [ + { value: "windows", label: "Windows" }, + { value: "macos", label: "macOS" }, + { value: "linux", label: "Linux" }, +]; + +const timezoneOptions = [ + { value: "America/New_York", label: "America/New_York" }, + { value: "America/Los_Angeles", label: "America/Los_Angeles" }, + { value: "America/Chicago", label: "America/Chicago" }, + { value: "America/Denver", label: "America/Denver" }, + { value: "America/Phoenix", label: "America/Phoenix" }, + { value: "America/Toronto", label: "America/Toronto" }, + { value: "America/Vancouver", label: "America/Vancouver" }, + { value: "Europe/London", label: "Europe/London" }, + { value: "Europe/Paris", label: "Europe/Paris" }, + { value: "Europe/Berlin", label: "Europe/Berlin" }, + { value: "Europe/Rome", label: "Europe/Rome" }, + { value: "Europe/Madrid", label: "Europe/Madrid" }, + { value: "Europe/Amsterdam", label: "Europe/Amsterdam" }, + { value: "Europe/Zurich", label: "Europe/Zurich" }, + { value: "Europe/Vienna", label: "Europe/Vienna" }, + { value: "Europe/Warsaw", label: "Europe/Warsaw" }, + { value: "Europe/Prague", label: "Europe/Prague" }, + { value: "Europe/Stockholm", label: "Europe/Stockholm" }, + { value: "Europe/Copenhagen", label: "Europe/Copenhagen" }, + { value: "Europe/Helsinki", label: "Europe/Helsinki" }, + { value: "Europe/Oslo", label: "Europe/Oslo" }, + { value: "Europe/Brussels", label: "Europe/Brussels" }, + { value: "Europe/Dublin", label: "Europe/Dublin" }, + { value: "Europe/Lisbon", label: "Europe/Lisbon" }, + { value: "Europe/Athens", label: "Europe/Athens" }, + { value: "Europe/Budapest", label: "Europe/Budapest" }, + { value: "Europe/Bucharest", label: "Europe/Bucharest" }, + { value: "Europe/Sofia", label: "Europe/Sofia" }, + { value: "Europe/Kiev", label: "Europe/Kiev" }, + { value: "Europe/Moscow", label: "Europe/Moscow" }, + { value: "Asia/Tokyo", label: "Asia/Tokyo" }, + { value: "Asia/Seoul", label: "Asia/Seoul" }, + { value: "Asia/Shanghai", label: "Asia/Shanghai" }, + { value: "Asia/Hong_Kong", label: "Asia/Hong_Kong" }, + { value: "Asia/Singapore", label: "Asia/Singapore" }, + { value: "Asia/Bangkok", label: "Asia/Bangkok" }, + { value: "Asia/Jakarta", label: "Asia/Jakarta" }, + { value: "Asia/Manila", label: "Asia/Manila" }, + { value: "Asia/Kolkata", label: "Asia/Kolkata" }, + { value: "Asia/Dubai", label: "Asia/Dubai" }, + { value: "Asia/Riyadh", label: "Asia/Riyadh" }, + { value: "Asia/Tehran", label: "Asia/Tehran" }, + { value: "Asia/Jerusalem", label: "Asia/Jerusalem" }, + { value: "Asia/Istanbul", label: "Asia/Istanbul" }, + { value: "Australia/Sydney", label: "Australia/Sydney" }, + { value: "Australia/Melbourne", label: "Australia/Melbourne" }, + { value: "Australia/Brisbane", label: "Australia/Brisbane" }, + { value: "Australia/Perth", label: "Australia/Perth" }, + { value: "Australia/Adelaide", label: "Australia/Adelaide" }, + { value: "Pacific/Auckland", label: "Pacific/Auckland" }, + { value: "Pacific/Honolulu", label: "Pacific/Honolulu" }, + { value: "Africa/Cairo", label: "Africa/Cairo" }, + { value: "Africa/Johannesburg", label: "Africa/Johannesburg" }, + { value: "Africa/Lagos", label: "Africa/Lagos" }, + { value: "Africa/Nairobi", label: "Africa/Nairobi" }, + { value: "America/Sao_Paulo", label: "America/Sao_Paulo" }, + { value: "America/Buenos_Aires", label: "America/Buenos_Aires" }, + { value: "America/Lima", label: "America/Lima" }, + { value: "America/Bogota", label: "America/Bogota" }, + { value: "America/Santiago", label: "America/Santiago" }, + { value: "America/Caracas", label: "America/Caracas" }, + { value: "America/Mexico_City", label: "America/Mexico_City" }, +]; + +const localeOptions = [ + { value: "en-US", label: "English (US)" }, + { value: "en-GB", label: "English (UK)" }, + { value: "en-CA", label: "English (Canada)" }, + { value: "en-AU", label: "English (Australia)" }, + { value: "fr-FR", label: "French (France)" }, + { value: "fr-CA", label: "French (Canada)" }, + { value: "de-DE", label: "German (Germany)" }, + { value: "de-AT", label: "German (Austria)" }, + { value: "de-CH", label: "German (Switzerland)" }, + { value: "es-ES", label: "Spanish (Spain)" }, + { value: "es-MX", label: "Spanish (Mexico)" }, + { value: "es-AR", label: "Spanish (Argentina)" }, + { value: "it-IT", label: "Italian (Italy)" }, + { value: "it-CH", label: "Italian (Switzerland)" }, + { value: "pt-BR", label: "Portuguese (Brazil)" }, + { value: "pt-PT", label: "Portuguese (Portugal)" }, + { value: "ru-RU", label: "Russian (Russia)" }, + { value: "zh-CN", label: "Chinese (Simplified)" }, + { value: "zh-TW", label: "Chinese (Traditional)" }, + { value: "ja-JP", label: "Japanese (Japan)" }, + { value: "ko-KR", label: "Korean (Korea)" }, + { value: "ar-SA", label: "Arabic (Saudi Arabia)" }, + { value: "ar-EG", label: "Arabic (Egypt)" }, + { value: "hi-IN", label: "Hindi (India)" }, + { value: "tr-TR", label: "Turkish (Turkey)" }, + { value: "pl-PL", label: "Polish (Poland)" }, + { value: "nl-NL", label: "Dutch (Netherlands)" }, + { value: "nl-BE", label: "Dutch (Belgium)" }, + { value: "sv-SE", label: "Swedish (Sweden)" }, + { value: "da-DK", label: "Danish (Denmark)" }, + { value: "no-NO", label: "Norwegian (Norway)" }, + { value: "fi-FI", label: "Finnish (Finland)" }, + { value: "he-IL", label: "Hebrew (Israel)" }, + { value: "th-TH", label: "Thai (Thailand)" }, + { value: "vi-VN", label: "Vietnamese (Vietnam)" }, + { value: "id-ID", label: "Indonesian (Indonesia)" }, + { value: "ms-MY", label: "Malay (Malaysia)" }, + { value: "uk-UA", label: "Ukrainian (Ukraine)" }, + { value: "cs-CZ", label: "Czech (Czech Republic)" }, + { value: "sk-SK", label: "Slovak (Slovakia)" }, + { value: "hu-HU", label: "Hungarian (Hungary)" }, + { value: "ro-RO", label: "Romanian (Romania)" }, + { value: "bg-BG", label: "Bulgarian (Bulgaria)" }, + { value: "hr-HR", label: "Croatian (Croatia)" }, + { value: "sr-RS", label: "Serbian (Serbia)" }, + { value: "sl-SI", label: "Slovenian (Slovenia)" }, + { value: "lt-LT", label: "Lithuanian (Lithuania)" }, + { value: "lv-LV", label: "Latvian (Latvia)" }, + { value: "et-EE", label: "Estonian (Estonia)" }, + { value: "el-GR", label: "Greek (Greece)" }, + { value: "ca-ES", label: "Catalan (Spain)" }, + { value: "eu-ES", label: "Basque (Spain)" }, + { value: "gl-ES", label: "Galician (Spain)" }, + { value: "is-IS", label: "Icelandic (Iceland)" }, + { value: "mt-MT", label: "Maltese (Malta)" }, +]; + +const getCurrentOS = () => { + if (typeof window !== "undefined") { + const userAgent = window.navigator.userAgent; + if (userAgent.includes("Win")) return "windows"; + if (userAgent.includes("Mac")) return "macos"; + if (userAgent.includes("Linux")) return "linux"; + } + return "unknown"; +}; + +interface SystemLocale { + locale: string; + language: string; + country: string; +} + +interface SystemTimezone { + timezone: string; + offset: string; +} + +interface SharedCamoufoxConfigFormProps { + config: CamoufoxConfig; + onConfigChange: (key: keyof CamoufoxConfig, value: unknown) => void; + className?: string; +} + +export function SharedCamoufoxConfigForm({ + config, + onConfigChange, + className = "", +}: SharedCamoufoxConfigFormProps) { + const [systemLocale, setSystemLocale] = useState(null); + const [systemTimezone, setSystemTimezone] = useState( + null, + ); + const [isLoadingSystemDefaults, setIsLoadingSystemDefaults] = useState(true); + + // Load system defaults on component mount + useEffect(() => { + const loadSystemDefaults = async () => { + try { + const [locale, timezone] = await Promise.all([ + invoke("get_system_locale"), + invoke("get_system_timezone"), + ]); + setSystemLocale(locale); + setSystemTimezone(timezone); + } catch (error) { + console.error("Failed to load system defaults:", error); + // Set fallback defaults + setSystemLocale({ + locale: "en-US", + language: "en", + country: "US", + }); + setSystemTimezone({ + timezone: "America/New_York", + offset: "-05:00", + }); + } finally { + setIsLoadingSystemDefaults(false); + } + }; + + loadSystemDefaults(); + }, []); + + // Get the selected OS for warning + const selectedOS = config.os?.[0]; + const currentOS = getCurrentOS(); + const showOSWarning = + selectedOS && selectedOS !== currentOS && currentOS !== "unknown"; + + return ( +
+ {/* OS Selection */} +
+ + + {showOSWarning && ( +

+ ⚠️ Selected OS ({selectedOS}) differs from your current OS ( + {currentOS}). This may affect fingerprinting effectiveness. +

+ )} +
+ + {/* Privacy & Blocking */} +
+ +
+
+ + onConfigChange("block_images", checked) + } + /> + +
+
+ + onConfigChange("block_webrtc", checked) + } + /> + +
+
+ + onConfigChange("block_webgl", checked) + } + /> + +
+
+
+ + {/* Geolocation */} +
+ +
+
+ + + onConfigChange("country", e.target.value || undefined) + } + placeholder={ + systemLocale + ? `e.g., ${systemLocale.country}` + : "e.g., US, GB, DE" + } + /> +
+
+ + +
+
+
+
+ + + onConfigChange( + "latitude", + e.target.value ? parseFloat(e.target.value) : undefined, + ) + } + placeholder="e.g., 40.7128" + /> +
+
+ + + onConfigChange( + "longitude", + e.target.value ? parseFloat(e.target.value) : undefined, + ) + } + placeholder="e.g., -74.0060" + /> +
+
+
+ + {/* Localization */} +
+ + +
+ + {/* Screen Resolution */} +
+ +
+
+ + + onConfigChange( + "screen_min_width", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 1024" + /> +
+
+ + + onConfigChange( + "screen_max_width", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 1920" + /> +
+
+
+
+ + + onConfigChange( + "screen_min_height", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 768" + /> +
+
+ + + onConfigChange( + "screen_max_height", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 1080" + /> +
+
+
+ + {/* Window Size */} +
+ +
+
+ + + onConfigChange( + "window_width", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 1366" + /> +
+
+ + + onConfigChange( + "window_height", + e.target.value ? parseInt(e.target.value) : undefined, + ) + } + placeholder="e.g., 768" + /> +
+
+
+ + {/* Advanced Options */} +
+ +
+
+ + onConfigChange("enable_cache", checked) + } + /> + +
+
+ + onConfigChange("main_world_eval", checked) + } + /> + +
+
+
+ + {/* WebGL Settings */} +
+ +
+
+ + + onConfigChange("webgl_vendor", e.target.value || undefined) + } + placeholder="e.g., Intel Inc." + /> +
+
+ + + onConfigChange("webgl_renderer", e.target.value || undefined) + } + placeholder="e.g., Intel HD Graphics" + /> +
+
+
+
+ ); +} diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx index 5ea9004..14e57a1 100644 --- a/src/components/ui/combobox.tsx +++ b/src/components/ui/combobox.tsx @@ -19,6 +19,85 @@ import { } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; +interface ComboboxOption { + value: string; + label: string; + description?: string; +} + +interface ComboboxProps { + options: ComboboxOption[]; + value: string; + onValueChange: (value: string) => void; + placeholder?: string; + searchPlaceholder?: string; + className?: string; +} + +export function Combobox({ + options, + value, + onValueChange, + placeholder = "Select option...", + searchPlaceholder = "Search...", + className, +}: ComboboxProps) { + const [open, setOpen] = React.useState(false); + + return ( + + + + + + + + + No option found. + + {options.map((option) => ( + { + onValueChange(currentValue === value ? "" : currentValue); + setOpen(false); + }} + > + +
+ {option.label} + {option.description && ( + + {option.description} + + )} +
+
+ ))} +
+
+
+
+
+ ); +} + const frameworks = [ { value: "next.js", diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..37a28d3 --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client"; + +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/src/hooks/use-browser-download.ts b/src/hooks/use-browser-download.ts index 240ca77..fc3452f 100644 --- a/src/hooks/use-browser-download.ts +++ b/src/hooks/use-browser-download.ts @@ -247,41 +247,58 @@ export function useBrowserDownload() { // Listen for download progress events useEffect(() => { - const unlisten = listen("download-progress", (event) => { - const progress = event.payload; - setDownloadProgress(progress); + let unlistenFn: (() => void) | null = null; - const browserName = getBrowserDisplayName(progress.browser); + const setupListener = async () => { + try { + unlistenFn = await listen( + "download-progress", + (event) => { + const progress = event.payload; + setDownloadProgress(progress); - // Show toast with progress - if (progress.stage === "downloading") { - const speedMBps = ( - progress.speed_bytes_per_sec / - (1024 * 1024) - ).toFixed(1); - const etaText = progress.eta_seconds - ? formatTime(progress.eta_seconds) - : "calculating..."; + const browserName = getBrowserDisplayName(progress.browser); - showDownloadToast(browserName, progress.version, "downloading", { - percentage: progress.percentage, - speed: speedMBps, - eta: etaText, - }); - } else if (progress.stage === "extracting") { - showDownloadToast(browserName, progress.version, "extracting"); - } else if (progress.stage === "verifying") { - showDownloadToast(browserName, progress.version, "verifying"); - } else if (progress.stage === "completed") { - showDownloadToast(browserName, progress.version, "completed"); - setDownloadProgress(null); + // Show toast with progress + if (progress.stage === "downloading") { + const speedMBps = ( + progress.speed_bytes_per_sec / + (1024 * 1024) + ).toFixed(1); + const etaText = progress.eta_seconds + ? formatTime(progress.eta_seconds) + : "calculating..."; + + showDownloadToast(browserName, progress.version, "downloading", { + percentage: progress.percentage, + speed: speedMBps, + eta: etaText, + }); + } else if (progress.stage === "extracting") { + showDownloadToast(browserName, progress.version, "extracting"); + } else if (progress.stage === "verifying") { + showDownloadToast(browserName, progress.version, "verifying"); + } else if (progress.stage === "completed") { + showDownloadToast(browserName, progress.version, "completed"); + setDownloadProgress(null); + } + }, + ); + } catch (error) { + console.error("Failed to setup download progress listener:", error); } - }); + }; + + setupListener(); return () => { - void unlisten.then((fn) => { - fn(); - }); + if (unlistenFn) { + try { + unlistenFn(); + } catch (error) { + console.error("Failed to cleanup download progress listener:", error); + } + } }; }, [formatTime]); diff --git a/src/hooks/use-version-updater.ts b/src/hooks/use-version-updater.ts index cc4cb30..aba9a3c 100644 --- a/src/hooks/use-version-updater.ts +++ b/src/hooks/use-version-updater.ts @@ -61,167 +61,207 @@ export function useVersionUpdater() { // Listen for version update progress events useEffect(() => { - const unlisten = listen( - "version-update-progress", - (event) => { - const progress = event.payload; - setUpdateProgress(progress); + let unlistenFn: (() => void) | null = null; - if (progress.status === "updating") { - setIsUpdating(true); + const setupListener = async () => { + try { + unlistenFn = await listen( + "version-update-progress", + (event) => { + const progress = event.payload; + setUpdateProgress(progress); - // Show unified progress toast - const currentBrowserName = progress.current_browser - ? getBrowserDisplayName(progress.current_browser) - : undefined; + if (progress.status === "updating") { + setIsUpdating(true); - showUnifiedVersionUpdateToast("Checking for browser updates...", { - description: currentBrowserName - ? `Fetching ${currentBrowserName} release information...` - : "Initializing version check...", - progress: { - current: progress.completed_browsers, - total: progress.total_browsers, - found: progress.new_versions_found, - current_browser: currentBrowserName, - }, - }); - } else if (progress.status === "completed") { - setIsUpdating(false); - setUpdateProgress(null); - dismissToast("unified-version-update"); + // Show unified progress toast + const currentBrowserName = progress.current_browser + ? getBrowserDisplayName(progress.current_browser) + : undefined; - if (progress.new_versions_found > 0) { - showSuccessToast("Browser versions updated successfully", { - duration: 5000, - description: - "Auto-downloads will start shortly for available updates.", - }); - } else { - showSuccessToast("No new browser versions found", { - duration: 3000, - description: "All browser versions are up to date", - }); - } + showUnifiedVersionUpdateToast("Checking for browser updates...", { + description: currentBrowserName + ? `Fetching ${currentBrowserName} release information...` + : "Initializing version check...", + progress: { + current: progress.completed_browsers, + total: progress.total_browsers, + found: progress.new_versions_found, + current_browser: currentBrowserName, + }, + }); + } else if (progress.status === "completed") { + setIsUpdating(false); + setUpdateProgress(null); + dismissToast("unified-version-update"); - // Refresh status - void loadUpdateStatus(); - } else if (progress.status === "error") { - setIsUpdating(false); - setUpdateProgress(null); - dismissToast("unified-version-update"); + if (progress.new_versions_found > 0) { + showSuccessToast("Browser versions updated successfully", { + duration: 5000, + description: + "Auto-downloads will start shortly for available updates.", + }); + } else { + showSuccessToast("No new browser versions found", { + duration: 3000, + description: "All browser versions are up to date", + }); + } - showErrorToast("Failed to update browser versions", { - duration: 6000, - description: "Check your internet connection and try again", - }); - } - }, - ); + // Refresh status + void loadUpdateStatus(); + } else if (progress.status === "error") { + setIsUpdating(false); + setUpdateProgress(null); + dismissToast("unified-version-update"); + + showErrorToast("Failed to update browser versions", { + duration: 6000, + description: "Check your internet connection and try again", + }); + } + }, + ); + } catch (error) { + console.error( + "Failed to setup version update progress listener:", + error, + ); + } + }; + + setupListener(); return () => { - void unlisten.then((fn) => { - fn(); - }); + if (unlistenFn) { + try { + unlistenFn(); + } catch (error) { + console.error( + "Failed to cleanup version update progress listener:", + error, + ); + } + } }; }, [loadUpdateStatus]); // Listen for browser auto-update events useEffect(() => { - const unlisten = listen( - "browser-auto-update-available", - (event) => { - const handleAutoUpdate = async () => { - const { browser, new_version, notification_id } = event.payload; - console.log("Browser auto-update event received:", event.payload); + let unlistenFn: (() => void) | null = null; - const browserDisplayName = getBrowserDisplayName(browser); + const setupListener = async () => { + try { + unlistenFn = await listen( + "browser-auto-update-available", + (event) => { + const handleAutoUpdate = async () => { + const { browser, new_version, notification_id } = event.payload; + console.log("Browser auto-update event received:", event.payload); - try { - // Show auto-update start notification - showAutoUpdateToast(browserDisplayName, new_version, { - description: `Downloading ${browserDisplayName} ${new_version} automatically. Progress will be shown below.`, - }); + const browserDisplayName = getBrowserDisplayName(browser); - // Dismiss the update notification in the backend - await invoke("dismiss_update_notification", { - notificationId: notification_id, - }); + try { + // Show auto-update start notification + showAutoUpdateToast(browserDisplayName, new_version, { + description: `Downloading ${browserDisplayName} ${new_version} automatically. Progress will be shown below.`, + }); - // Check if browser already exists before downloading - const isDownloaded = await invoke("check_browser_exists", { - browserStr: browser, - version: new_version, - }); + // Dismiss the update notification in the backend + await invoke("dismiss_update_notification", { + notificationId: notification_id, + }); - if (isDownloaded) { - // Browser already exists, skip download and go straight to profile update - console.log( - `${browserDisplayName} ${new_version} already exists, skipping download`, - ); + // Check if browser already exists before downloading + const isDownloaded = await invoke( + "check_browser_exists", + { + browserStr: browser, + version: new_version, + }, + ); - showSuccessToast( - `${browserDisplayName} ${new_version} already available`, - { - description: "Updating profile configurations...", - duration: 3000, - }, - ); - } else { - // Download the browser - this will trigger download progress events automatically - await invoke("download_browser", { - browserStr: browser, - version: new_version, - }); - } + if (isDownloaded) { + // Browser already exists, skip download and go straight to profile update + console.log( + `${browserDisplayName} ${new_version} already exists, skipping download`, + ); - // Complete the update with auto-update of profile versions - const updatedProfiles = await invoke( - "complete_browser_update_with_auto_update", - { - browser, - newVersion: new_version, - }, - ); + showSuccessToast( + `${browserDisplayName} ${new_version} already available`, + { + description: "Updating profile configurations...", + duration: 3000, + }, + ); + } else { + // Download the browser - this will trigger download progress events automatically + await invoke("download_browser", { + browserStr: browser, + version: new_version, + }); + } - // Show success message based on whether profiles were updated - if (updatedProfiles.length > 0) { - const profileText = - updatedProfiles.length === 1 - ? `Profile "${updatedProfiles[0]}" has been updated` - : `${updatedProfiles.length} profiles have been updated`; + // Complete the update with auto-update of profile versions + const updatedProfiles = await invoke( + "complete_browser_update_with_auto_update", + { + browser, + newVersion: new_version, + }, + ); - showSuccessToast(`${browserDisplayName} update completed`, { - description: `${profileText} to version ${new_version}. You can now launch your browsers with the latest version.`, - duration: 6000, - }); - } else { - showSuccessToast(`${browserDisplayName} update completed`, { - description: `Version ${new_version} is now available. Running profiles will use the new version when restarted.`, - duration: 6000, - }); - } - } catch (error) { - console.error("Failed to handle browser auto-update:", error); - showErrorToast(`Failed to auto-update ${browserDisplayName}`, { - description: - error instanceof Error - ? error.message - : "Unknown error occurred", - duration: 8000, - }); - } - }; + // Show success message based on whether profiles were updated + if (updatedProfiles.length > 0) { + const profileText = + updatedProfiles.length === 1 + ? `Profile "${updatedProfiles[0]}" has been updated` + : `${updatedProfiles.length} profiles have been updated`; - // Call the async handler - void handleAutoUpdate(); - }, - ); + showSuccessToast(`${browserDisplayName} update completed`, { + description: `${profileText} to version ${new_version}. You can now launch your browsers with the latest version.`, + duration: 6000, + }); + } else { + showSuccessToast(`${browserDisplayName} update completed`, { + description: `Version ${new_version} is now available. Running profiles will use the new version when restarted.`, + duration: 6000, + }); + } + } catch (error) { + console.error("Failed to handle browser auto-update:", error); + showErrorToast(`Failed to auto-update ${browserDisplayName}`, { + description: + error instanceof Error + ? error.message + : "Unknown error occurred", + duration: 8000, + }); + } + }; + + // Call the async handler + void handleAutoUpdate(); + }, + ); + } catch (error) { + console.error("Failed to setup browser auto-update listener:", error); + } + }; + + setupListener(); return () => { - void unlisten.then((fn) => { - fn(); - }); + if (unlistenFn) { + try { + unlistenFn(); + } catch (error) { + console.error( + "Failed to cleanup browser auto-update listener:", + error, + ); + } + } }; }, []); diff --git a/src/lib/browser-utils.ts b/src/lib/browser-utils.ts index ace6989..0fee1c6 100644 --- a/src/lib/browser-utils.ts +++ b/src/lib/browser-utils.ts @@ -3,7 +3,7 @@ * Centralized helpers for browser name mapping, icons, etc. */ -import { FaChrome, FaFirefox } from "react-icons/fa"; +import { FaChrome, FaFirefox, FaShieldAlt } from "react-icons/fa"; import { SiBrave, SiMullvad, SiTorbrowser } from "react-icons/si"; import { ZenBrowser } from "@/components/icons/zen-browser"; @@ -19,6 +19,7 @@ export function getBrowserDisplayName(browserType: string): string { brave: "Brave", chromium: "Chromium", "tor-browser": "Tor Browser", + camoufox: "Anti-Detect", }; return browserNames[browserType] || browserType; @@ -42,6 +43,8 @@ export function getBrowserIcon(browserType: string) { return ZenBrowser; case "tor-browser": return SiTorbrowser; + case "camoufox": + return FaShieldAlt; default: return null; } diff --git a/src/types.ts b/src/types.ts index 723285e..955add0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,6 +20,7 @@ export interface BrowserProfile { process_id?: number; last_launch?: number; release_type: string; // "stable" or "nightly" + camoufox_config?: CamoufoxConfig; // Camoufox configuration } export interface StoredProxy { @@ -56,3 +57,49 @@ export interface AppUpdateProgress { eta?: string; // estimated time remaining message: string; } + +export interface CamoufoxConfig { + os?: string[]; + block_images?: boolean; + block_webrtc?: boolean; + block_webgl?: boolean; + disable_coop?: boolean; + geoip?: string | boolean; + country?: string; + timezone?: string; + latitude?: number; + longitude?: number; + humanize?: boolean; + humanize_duration?: number; + headless?: boolean; + locale?: string[]; + addons?: string[]; + fonts?: string[]; + custom_fonts_only?: boolean; + exclude_addons?: string[]; + screen_min_width?: number; + screen_max_width?: number; + screen_min_height?: number; + screen_max_height?: number; + window_width?: number; + window_height?: number; + ff_version?: number; + main_world_eval?: boolean; + webgl_vendor?: string; + webgl_renderer?: string; + proxy?: string; + enable_cache?: boolean; + virtual_display?: string; + debug?: boolean; + additional_args?: string[]; + env_vars?: Record; + firefox_prefs?: Record; +} + +export interface CamoufoxLaunchResult { + id: string; + pid?: number; + executable_path: string; + profile_path: string; + url?: string; +}