From af2aa36ac6745dd775cd293923e87e79d202450b Mon Sep 17 00:00:00 2001
From: zhom <2717306+zhom@users.noreply.github.com>
Date: Mon, 16 Feb 2026 22:18:11 +0400
Subject: [PATCH] feat: block launching profiles for incompatible systems
---
src-tauri/src/auto_updater.rs | 1 +
src-tauri/src/profile/manager.rs | 14 ++--
src-tauri/src/profile/types.rs | 21 +++++
src-tauri/src/profile_importer.rs | 1 +
src-tauri/src/sync/engine.rs | 107 +++++++++++++++++++++++++
src/components/profile-data-table.tsx | 111 ++++++++++++++++++++++++--
src/hooks/use-browser-state.ts | 13 ++-
src/i18n/locales/en.json | 5 ++
src/i18n/locales/es.json | 5 ++
src/i18n/locales/fr.json | 5 ++
src/i18n/locales/ja.json | 5 ++
src/i18n/locales/pt.json | 5 ++
src/i18n/locales/ru.json | 5 ++
src/i18n/locales/zh.json | 5 ++
src/lib/browser-utils.ts | 18 +++++
src/types.ts | 1 +
16 files changed, 309 insertions(+), 13 deletions(-)
diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs
index fac2f33..37f10d1 100644
--- a/src-tauri/src/auto_updater.rs
+++ b/src-tauri/src/auto_updater.rs
@@ -520,6 +520,7 @@ mod tests {
note: None,
sync_enabled: false,
last_sync: None,
+ host_os: None,
}
}
diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs
index dedc415..cb2ba68 100644
--- a/src-tauri/src/profile/manager.rs
+++ b/src-tauri/src/profile/manager.rs
@@ -3,7 +3,7 @@ use crate::browser::{create_browser, BrowserType, ProxySettings};
use crate::camoufox_manager::CamoufoxConfig;
use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry;
use crate::events;
-use crate::profile::types::BrowserProfile;
+use crate::profile::types::{get_host_os, BrowserProfile};
use crate::proxy_manager::PROXY_MANAGER;
use crate::wayfern_manager::WayfernConfig;
use directories::BaseDirs;
@@ -173,6 +173,7 @@ impl ProfileManager {
note: None,
sync_enabled: false,
last_sync: None,
+ host_os: None,
};
match self
@@ -286,6 +287,7 @@ impl ProfileManager {
note: None,
sync_enabled: false,
last_sync: None,
+ host_os: None,
};
match self
@@ -331,6 +333,7 @@ impl ProfileManager {
note: None,
sync_enabled: false,
last_sync: None,
+ host_os: Some(get_host_os()),
};
// Save profile info
@@ -466,8 +469,8 @@ impl ProfileManager {
.find(|p| p.id == profile_uuid)
.ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))?;
- // Check if browser is running
- if profile.process_id.is_some() {
+ // Check if browser is running (cross-OS profiles can't be running locally)
+ if profile.process_id.is_some() && !profile.is_cross_os() {
return Err(
"Cannot delete profile while browser is running. Please stop the browser first.".into(),
);
@@ -733,8 +736,8 @@ impl ProfileManager {
.find(|p| p.id == profile_uuid)
.ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))?;
- // Check if browser is running
- if profile.process_id.is_some() {
+ // Check if browser is running (cross-OS profiles can't be running locally)
+ if profile.process_id.is_some() && !profile.is_cross_os() {
return Err(
format!(
"Cannot delete profile '{}' while browser is running. Please stop the browser first.",
@@ -847,6 +850,7 @@ impl ProfileManager {
note: source.note,
sync_enabled: false,
last_sync: None,
+ host_os: Some(get_host_os()),
};
self.save_profile(&new_profile)?;
diff --git a/src-tauri/src/profile/types.rs b/src-tauri/src/profile/types.rs
index 0bde2b4..9f89f02 100644
--- a/src-tauri/src/profile/types.rs
+++ b/src-tauri/src/profile/types.rs
@@ -41,15 +41,36 @@ pub struct BrowserProfile {
pub sync_enabled: bool, // Whether sync is enabled for this profile
#[serde(default)]
pub last_sync: Option, // Timestamp of last successful sync (epoch seconds)
+ #[serde(default)]
+ pub host_os: Option, // OS where profile was created ("macos", "windows", "linux")
}
pub fn default_release_type() -> String {
"stable".to_string()
}
+pub fn get_host_os() -> String {
+ if cfg!(target_os = "macos") {
+ "macos".to_string()
+ } else if cfg!(target_os = "windows") {
+ "windows".to_string()
+ } else {
+ "linux".to_string()
+ }
+}
+
impl BrowserProfile {
/// Get the path to the profile data directory (profiles/{uuid}/profile)
pub fn get_profile_data_path(&self, profiles_dir: &Path) -> PathBuf {
profiles_dir.join(self.id.to_string()).join("profile")
}
+
+ /// Returns true when the profile was created on a different OS than the current host.
+ /// Profiles without an `os` field (backward compat) are treated as native.
+ pub fn is_cross_os(&self) -> bool {
+ match &self.host_os {
+ Some(host_os) => host_os != &get_host_os(),
+ None => false,
+ }
+ }
}
diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs
index e0ae683..4c3d06b 100644
--- a/src-tauri/src/profile_importer.rs
+++ b/src-tauri/src/profile_importer.rs
@@ -555,6 +555,7 @@ impl ProfileImporter {
note: None,
sync_enabled: false,
last_sync: None,
+ host_os: Some(crate::profile::types::get_host_os()),
};
// Save the profile metadata
diff --git a/src-tauri/src/sync/engine.rs b/src-tauri/src/sync/engine.rs
index 1098f0b..08fbebd 100644
--- a/src-tauri/src/sync/engine.rs
+++ b/src-tauri/src/sync/engine.rs
@@ -59,6 +59,15 @@ impl SyncEngine {
app_handle: &tauri::AppHandle,
profile: &BrowserProfile,
) -> SyncResult<()> {
+ if profile.is_cross_os() {
+ log::info!(
+ "Skipping file sync for cross-OS profile: {} ({})",
+ profile.name,
+ profile.id
+ );
+ return Ok(());
+ }
+
let profile_manager = ProfileManager::instance();
let profiles_dir = profile_manager.get_profiles_dir();
let profile_dir = profiles_dir.join(profile.id.to_string());
@@ -832,6 +841,49 @@ impl SyncEngine {
let mut profile: BrowserProfile = serde_json::from_slice(&metadata_data)
.map_err(|e| SyncError::SerializationError(format!("Failed to parse metadata: {e}")))?;
+ // Cross-OS profile: save metadata only, skip manifest + file downloads
+ if profile.is_cross_os() {
+ log::info!(
+ "Profile {} is cross-OS (host_os={:?}), downloading metadata only",
+ profile_id,
+ profile.host_os
+ );
+
+ fs::create_dir_all(&profile_dir).map_err(|e| {
+ SyncError::IoError(format!(
+ "Failed to create profile directory {}: {e}",
+ profile_dir.display()
+ ))
+ })?;
+
+ profile.sync_enabled = true;
+ profile.last_sync = Some(
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ );
+
+ profile_manager
+ .save_profile(&profile)
+ .map_err(|e| SyncError::IoError(format!("Failed to save cross-OS profile: {e}")))?;
+
+ let _ = events::emit("profiles-changed", ());
+ let _ = events::emit(
+ "profile-sync-status",
+ serde_json::json!({
+ "profile_id": profile_id,
+ "status": "synced"
+ }),
+ );
+
+ log::info!(
+ "Cross-OS profile {} metadata downloaded successfully",
+ profile_id
+ );
+ return Ok(true);
+ }
+
// Download manifest
let manifest = self.download_manifest(&manifest_key).await?;
let Some(manifest) = manifest else {
@@ -940,6 +992,57 @@ impl SyncEngine {
log::info!("No missing profiles found");
}
+ // Refresh metadata for local cross-OS profiles (propagate renames, tags, notes from originating device)
+ let profile_manager = ProfileManager::instance();
+ // Collect cross-OS profiles before async operations to avoid holding non-Send Result across await
+ let cross_os_profiles: Vec<(String, bool)> = profile_manager
+ .list_profiles()
+ .unwrap_or_default()
+ .iter()
+ .filter(|p| p.is_cross_os() && p.sync_enabled)
+ .map(|p| (p.id.to_string(), p.sync_enabled))
+ .collect();
+
+ if !cross_os_profiles.is_empty() {
+ for (pid, sync_enabled) in &cross_os_profiles {
+ let metadata_key = format!("profiles/{}/metadata.json", pid);
+ match self.client.stat(&metadata_key).await {
+ Ok(stat) if stat.exists => match self.client.presign_download(&metadata_key).await {
+ Ok(presign) => match self.client.download_bytes(&presign.url).await {
+ Ok(data) => {
+ if let Ok(mut remote_profile) = serde_json::from_slice::(&data) {
+ remote_profile.sync_enabled = *sync_enabled;
+ remote_profile.last_sync = Some(
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ );
+ if let Err(e) = profile_manager.save_profile(&remote_profile) {
+ log::warn!("Failed to refresh cross-OS profile {} metadata: {}", pid, e);
+ } else {
+ log::debug!("Refreshed cross-OS profile {} metadata", pid);
+ }
+ }
+ }
+ Err(e) => {
+ log::warn!(
+ "Failed to download cross-OS profile {} metadata: {}",
+ pid,
+ e
+ );
+ }
+ },
+ Err(e) => {
+ log::warn!("Failed to presign cross-OS profile {} metadata: {}", pid, e);
+ }
+ },
+ _ => {}
+ }
+ }
+ let _ = events::emit("profiles-changed", ());
+ }
+
Ok(downloaded)
}
}
@@ -1048,6 +1151,10 @@ pub async fn set_profile_sync_enabled(
.find(|p| p.id == profile_uuid)
.ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))?;
+ if profile.is_cross_os() {
+ return Err("Cannot modify sync settings for a cross-OS profile".to_string());
+ }
+
// If enabling, first check that sync settings are configured
if enabled {
// Cloud auth provides sync settings dynamically — skip local checks
diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx
index b6d43e2..79b9604 100644
--- a/src/components/profile-data-table.tsx
+++ b/src/components/profile-data-table.tsx
@@ -13,6 +13,7 @@ import { invoke } from "@tauri-apps/api/core";
import { emit, listen } from "@tauri-apps/api/event";
import type { Dispatch, SetStateAction } from "react";
import * as React from "react";
+import { FaApple, FaLinux, FaWindows } from "react-icons/fa";
import { FiWifi } from "react-icons/fi";
import { IoEllipsisHorizontal } from "react-icons/io5";
import {
@@ -68,6 +69,8 @@ import {
getBrowserDisplayName,
getBrowserIcon,
getCurrentOS,
+ getOSDisplayName,
+ isCrossOsProfile,
} from "@/lib/browser-utils";
import { formatRelativeTime } from "@/lib/flag-utils";
import { trimName } from "@/lib/name-utils";
@@ -1464,6 +1467,7 @@ export function ProfilesDataTable({
const profile = row.original;
const browser = profile.browser;
const IconComponent = getBrowserIcon(browser);
+ const isCrossOs = isCrossOsProfile(profile);
const isSelected = meta.isProfileSelected(profile.id);
const isRunning =
@@ -1474,6 +1478,66 @@ export function ProfilesDataTable({
const isDisabled =
isRunning || isLaunching || isStopping || isBrowserUpdating;
+ // Cross-OS profiles: show OS icon when checkboxes aren't visible, show checkbox when they are
+ if (isCrossOs && !meta.showCheckboxes && !isSelected) {
+ const osName = profile.host_os
+ ? getOSDisplayName(profile.host_os)
+ : "another OS";
+ const OsIcon =
+ profile.host_os === "macos"
+ ? FaApple
+ : profile.host_os === "windows"
+ ? FaWindows
+ : FaLinux;
+ return (
+
+
+
+
+
+
+
+ Created on {osName} - view only
+
+
+ );
+ }
+
+ // Cross-OS profiles with checkboxes visible: show checkbox (selectable for bulk delete)
+ if (isCrossOs && (meta.showCheckboxes || isSelected)) {
+ const osName = profile.host_os
+ ? getOSDisplayName(profile.host_os)
+ : "another OS";
+ return (
+ Created on {osName} - view only
}
+ sideOffset={4}
+ horizontalOffset={8}
+ >
+
+
+ meta.handleCheckboxChange(profile.id, !!value)
+ }
+ aria-label="Select row"
+ className="w-4 h-4"
+ />
+
+
+ );
+ }
+
if (isDisabled) {
const tooltipMessage = isRunning
? "Can't modify running profile"
@@ -1718,13 +1782,18 @@ export function ProfilesDataTable({
);
+ const isCrossOs = isCrossOsProfile(profile);
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isBrowserUpdating = meta.isUpdating(profile.browser);
const isDisabled =
- isRunning || isLaunching || isStopping || isBrowserUpdating;
+ isRunning ||
+ isLaunching ||
+ isStopping ||
+ isBrowserUpdating ||
+ isCrossOs;
return (