use anyhow::{Context, Result};
#[cfg(windows)]
use sha1::{Digest, Sha1};
use tsafe_cli::cli::BrowserNativeHostAction;
pub fn cmd_browser_native_host(action: BrowserNativeHostAction) -> Result<()> {
#[cfg(windows)]
{
match action {
BrowserNativeHostAction::Register { extension_id } => register_impl(&extension_id),
BrowserNativeHostAction::Unregister => unregister_impl(),
BrowserNativeHostAction::Detect => detect_impl_windows(),
}
}
#[cfg(target_os = "macos")]
{
match action {
BrowserNativeHostAction::Register { extension_id } => {
unix_register_impl(&extension_id, MACOS_PATHS)
}
BrowserNativeHostAction::Unregister => unix_unregister_impl(MACOS_PATHS),
BrowserNativeHostAction::Detect => unix_detect_impl(MACOS_PATHS),
}
}
#[cfg(target_os = "linux")]
{
match action {
BrowserNativeHostAction::Register { extension_id } => {
unix_register_impl(&extension_id, LINUX_PATHS)
}
BrowserNativeHostAction::Unregister => unix_unregister_impl(LINUX_PATHS),
BrowserNativeHostAction::Detect => unix_detect_impl(LINUX_PATHS),
}
}
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
{
let _ = action;
anyhow::bail!("browser-native-host is only supported on Windows, macOS, and Linux");
}
}
#[cfg(windows)]
fn chromium_manifest_dir() -> Result<std::path::PathBuf> {
let base = std::env::var_os("LOCALAPPDATA").context(
"LOCALAPPDATA is unset; expected per-user manifest under %LOCALAPPDATA%\\tsafe\\",
)?;
Ok(std::path::Path::new(&base).join("tsafe"))
}
#[cfg(windows)]
fn firefox_manifest_dir() -> Result<std::path::PathBuf> {
let base = std::env::var_os("APPDATA").context(
"APPDATA is unset; expected Firefox manifest under %APPDATA%\\Mozilla\\NativeMessagingHosts\\",
)?;
Ok(std::path::Path::new(&base)
.join("Mozilla")
.join("NativeMessagingHosts"))
}
#[cfg(windows)]
fn sibling_host_exe_path() -> Result<std::path::PathBuf> {
let tsafe = std::env::current_exe().context("could not resolve path to tsafe.exe")?;
let dir = tsafe
.parent()
.context("tsafe.exe has no parent directory (unexpected)")?;
Ok(dir.join("tsafe-nativehost.exe"))
}
#[cfg(windows)]
fn host_exe_path() -> Result<std::path::PathBuf> {
let host = sibling_host_exe_path()?;
if !host.is_file() {
anyhow::bail!(
"tsafe-nativehost.exe not found next to tsafe at {}.\n\
Use the MSI or place tsafe-nativehost.exe beside tsafe.exe, then retry.",
host.display()
);
}
Ok(host)
}
#[cfg(windows)]
#[derive(serde::Serialize, serde::Deserialize)]
struct WindowsManifest {
name: String,
description: String,
path: String,
#[serde(rename = "type")]
ty: String,
allowed_origins: Vec<String>,
}
#[cfg(windows)]
#[derive(serde::Serialize, serde::Deserialize)]
struct WindowsFirefoxManifest {
name: String,
description: String,
path: String,
#[serde(rename = "type")]
ty: String,
allowed_extensions: Vec<String>,
}
#[cfg(windows)]
fn manifest_path_for_host(host: &std::path::Path) -> Result<std::path::PathBuf> {
let dir = chromium_manifest_dir()?;
let mut hasher = Sha1::new();
hasher.update(host.to_string_lossy().as_bytes());
let digest = hasher.finalize();
let suffix: String = digest[..6].iter().map(|b| format!("{b:02x}")).collect();
Ok(dir.join(format!("com.tsafe.host.{suffix}.json")))
}
#[cfg(windows)]
fn is_firefox_extension_id(extension_id: &str) -> bool {
if extension_id.starts_with('{') && extension_id.ends_with('}') {
return true;
}
if let Some(at_pos) = extension_id.find('@') {
let domain = &extension_id[at_pos + 1..];
return domain.contains('.');
}
false
}
#[cfg(windows)]
fn is_chromium_extension_id(extension_id: &str) -> bool {
extension_id.len() == 32 && extension_id.chars().all(|ch| matches!(ch, 'a'..='p'))
}
#[cfg(windows)]
fn register_impl(extension_id: &str) -> Result<()> {
use std::fs;
let host = host_exe_path()?;
let path_str = host
.to_str()
.ok_or_else(|| anyhow::anyhow!("native host path is not valid UTF-8"))?
.to_string();
if is_firefox_extension_id(extension_id) {
let dir = firefox_manifest_dir()?;
fs::create_dir_all(&dir)
.with_context(|| format!("create Firefox manifest directory {}", dir.display()))?;
let manifest_path = dir.join("com.tsafe.host.json");
let manifest = WindowsFirefoxManifest {
name: "com.tsafe.host".to_string(),
description: "tsafe vault native messaging host".to_string(),
path: path_str,
ty: "stdio".to_string(),
allowed_extensions: vec![extension_id.to_string()],
};
let json = serde_json::to_string_pretty(&manifest)?;
fs::write(&manifest_path, json)
.with_context(|| format!("write Firefox manifest {}", manifest_path.display()))?;
eprintln!(
"Registered native messaging host com.tsafe.host (Firefox) → {}",
manifest_path.display()
);
return Ok(());
}
if !is_chromium_extension_id(extension_id) {
anyhow::bail!(
"Unrecognised extension ID format: {extension_id:?}\n\
Chromium-family IDs are 32 characters from a–p (find at chrome://extensions).\n\
Firefox IDs are email-style (e.g. tsafe@tsafe.dev) or UUID in curly braces."
);
}
let dir = chromium_manifest_dir()?;
fs::create_dir_all(&dir).with_context(|| format!("create directory {}", dir.display()))?;
let manifest_path = manifest_path_for_host(&host)?;
let requested_origin = windows_allowed_origin(extension_id);
let existing_origins = read_existing_windows_allowed_origins(&manifest_path)?;
let merged_origins = merge_windows_allowed_origins(existing_origins, &requested_origin);
let manifest = WindowsManifest {
name: "com.tsafe.host".to_string(),
description: "tsafe vault native messaging host".to_string(),
path: path_str,
ty: "stdio".to_string(),
allowed_origins: merged_origins.clone(),
};
let json = serde_json::to_string_pretty(&manifest)?;
fs::write(&manifest_path, json)
.with_context(|| format!("write manifest {}", manifest_path.display()))?;
let manifest_str = manifest_path
.to_str()
.ok_or_else(|| anyhow::anyhow!("manifest path is not valid UTF-8"))?;
write_chromium_registry(manifest_str)?;
eprintln!(
"Registered native messaging host com.tsafe.host → {} ({})",
manifest_path.display(),
merged_origins.join(", ")
);
Ok(())
}
#[cfg(all(test, windows))]
mod windows_tests {
use super::{
is_chromium_extension_id, is_firefox_extension_id, merge_windows_allowed_origins,
windows_allowed_origin,
};
#[test]
fn chromium_extension_ids_are_accepted() {
assert!(is_chromium_extension_id("abcdefghijklmnopabcdefghijklmnop"));
}
#[test]
fn firefox_email_style_id_is_detected() {
assert!(is_firefox_extension_id("tsafe@tsafe.dev"));
assert!(is_firefox_extension_id("tsafe@example.com"));
}
#[test]
fn firefox_uuid_style_id_is_detected() {
assert!(is_firefox_extension_id(
"{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}"
));
}
#[test]
fn chromium_id_is_not_firefox() {
assert!(!is_firefox_extension_id("abcdefghijklmnopabcdefghijklmnop"));
}
#[test]
fn merge_preserves_existing_allowed_origins() {
let existing = vec![windows_allowed_origin("abcdefghijklmnopabcdefghijklmnop")];
let merged = merge_windows_allowed_origins(
existing,
&windows_allowed_origin("ponmlkjihgfedcbaponmlkjihgfedcba"),
);
assert_eq!(
merged,
vec![
windows_allowed_origin("abcdefghijklmnopabcdefghijklmnop"),
windows_allowed_origin("ponmlkjihgfedcbaponmlkjihgfedcba"),
]
);
}
#[test]
fn merge_dedupes_allowed_origins() {
let requested = windows_allowed_origin("abcdefghijklmnopabcdefghijklmnop");
let merged = merge_windows_allowed_origins(vec![requested.clone()], &requested);
assert_eq!(merged, vec![requested]);
}
}
#[cfg(windows)]
fn windows_allowed_origin(extension_id: &str) -> String {
format!("chrome-extension://{extension_id}/")
}
#[cfg(windows)]
fn read_existing_windows_allowed_origins(manifest_path: &std::path::Path) -> Result<Vec<String>> {
use std::fs;
if manifest_path.is_file() {
let raw = fs::read_to_string(manifest_path)
.with_context(|| format!("read existing manifest {}", manifest_path.display()))?;
let manifest: WindowsManifest = serde_json::from_str(&raw)
.with_context(|| format!("parse existing manifest {}", manifest_path.display()))?;
return Ok(manifest.allowed_origins);
}
let legacy_path = chromium_manifest_dir()?.join("com.tsafe.host.json");
if !legacy_path.is_file() {
return Ok(Vec::new());
}
let raw = fs::read_to_string(&legacy_path)
.with_context(|| format!("read legacy manifest {}", legacy_path.display()))?;
let manifest: WindowsManifest = serde_json::from_str(&raw)
.with_context(|| format!("parse legacy manifest {}", legacy_path.display()))?;
Ok(manifest.allowed_origins)
}
#[cfg(windows)]
fn read_windows_manifest(manifest_path: &std::path::Path) -> Result<WindowsManifest> {
let raw = std::fs::read_to_string(manifest_path)
.with_context(|| format!("read manifest {}", manifest_path.display()))?;
serde_json::from_str(&raw)
.with_context(|| format!("parse manifest {}", manifest_path.display()))
}
#[cfg(windows)]
fn merge_windows_allowed_origins(mut existing: Vec<String>, requested_origin: &str) -> Vec<String> {
if !existing.iter().any(|origin| origin == requested_origin) {
existing.push(requested_origin.to_string());
}
existing.sort();
existing.dedup();
existing
}
#[cfg(windows)]
fn write_chromium_registry(manifest_path: &str) -> Result<()> {
use winreg::enums::*;
use winreg::RegKey;
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
for rel in [
r"Software\Google\Chrome\NativeMessagingHosts\com.tsafe.host",
r"Software\BraveSoftware\Brave-Browser\NativeMessagingHosts\com.tsafe.host",
r"Software\Chromium\NativeMessagingHosts\com.tsafe.host",
r"Software\Microsoft\Edge\NativeMessagingHosts\com.tsafe.host",
] {
let (key, _) = hkcu
.create_subkey(rel)
.with_context(|| format!("create registry key HKCU\\{rel}"))?;
key.set_value("", &manifest_path)
.with_context(|| format!("set default value for HKCU\\{rel}"))?;
}
Ok(())
}
#[cfg(windows)]
fn unregister_impl() -> Result<()> {
use std::fs;
use winreg::enums::*;
use winreg::RegKey;
let host = sibling_host_exe_path()?;
let host_path = host
.to_str()
.ok_or_else(|| anyhow::anyhow!("native host path is not valid UTF-8"))?
.to_string();
let primary_manifest_path = manifest_path_for_host(&host)?;
let legacy_manifest_path = chromium_manifest_dir()?.join("com.tsafe.host.json");
let mut owned_manifest_paths: Vec<std::path::PathBuf> = vec![primary_manifest_path.clone()];
if !legacy_manifest_path.is_file() {
owned_manifest_paths.push(legacy_manifest_path.clone());
} else if let Ok(manifest) = read_windows_manifest(&legacy_manifest_path) {
if manifest.path == host_path {
owned_manifest_paths.push(legacy_manifest_path.clone());
}
}
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
for rel in [
r"Software\Google\Chrome\NativeMessagingHosts\com.tsafe.host",
r"Software\BraveSoftware\Brave-Browser\NativeMessagingHosts\com.tsafe.host",
r"Software\Chromium\NativeMessagingHosts\com.tsafe.host",
r"Software\Microsoft\Edge\NativeMessagingHosts\com.tsafe.host",
] {
match hkcu.open_subkey(rel) {
Ok(key) => {
let current_path = key.get_value::<String, _>("").ok();
let owned_match = current_path.as_ref().is_some_and(|path| {
owned_manifest_paths
.iter()
.filter_map(|candidate| candidate.to_str())
.any(|candidate| candidate == path)
});
if owned_match {
if let Err(e) = hkcu.delete_subkey_all(rel) {
if e.kind() != std::io::ErrorKind::NotFound {
eprintln!("warning: could not remove HKCU\\{rel}: {e}");
}
}
} else if let Some(path) = current_path {
eprintln!(
" - leaving HKCU\\{rel} in place because it points at another tsafe manifest: {path}"
);
}
}
Err(e) => {
if e.kind() != std::io::ErrorKind::NotFound {
eprintln!("warning: could not inspect HKCU\\{rel}: {e}");
}
}
}
}
for manifest_path in owned_manifest_paths {
if !manifest_path.is_file() {
continue;
}
if let Err(e) = fs::remove_file(&manifest_path) {
eprintln!(
"warning: could not remove manifest {}: {e}",
manifest_path.display()
);
}
}
if let Ok(ff_dir) = firefox_manifest_dir() {
let ff_manifest = ff_dir.join("com.tsafe.host.json");
if ff_manifest.is_file() {
let owned = std::fs::read_to_string(&ff_manifest)
.ok()
.and_then(|raw| serde_json::from_str::<WindowsFirefoxManifest>(&raw).ok())
.is_some_and(|m| m.path == host_path);
if owned {
if let Err(e) = fs::remove_file(&ff_manifest) {
eprintln!(
"warning: could not remove Firefox manifest {}: {e}",
ff_manifest.display()
);
} else {
eprintln!(" ✓ removed Firefox manifest {}", ff_manifest.display());
}
}
}
}
eprintln!("Unregistered native messaging host com.tsafe.host");
Ok(())
}
#[cfg(windows)]
fn detect_impl_windows() -> Result<()> {
let tsafe = std::env::current_exe().context("could not resolve path to tsafe.exe")?;
let dir = tsafe
.parent()
.context("tsafe.exe has no parent directory")?;
let host = dir.join("tsafe-nativehost.exe");
let host_found = host.is_file();
eprintln!("=== tsafe browser-native-host detect ===");
eprintln!();
if host_found {
eprintln!(" native-host binary : FOUND");
eprintln!(" {}", host.display());
} else {
eprintln!(" native-host binary : NOT FOUND");
eprintln!(" Expected: {}", host.display());
eprintln!(" Install via MSI, or place tsafe-nativehost.exe beside tsafe.exe.");
}
eprintln!();
let chromium_dir_result = chromium_manifest_dir();
eprintln!(" Chromium manifest directory:");
match &chromium_dir_result {
Ok(d) => eprintln!(" {}", d.display()),
Err(e) => eprintln!(" (could not resolve: {e})"),
}
eprintln!();
eprintln!(" HKCU registry keys that `register` (Chromium ID) would write:");
for rel in [
r"Software\Google\Chrome\NativeMessagingHosts\com.tsafe.host",
r"Software\BraveSoftware\Brave-Browser\NativeMessagingHosts\com.tsafe.host",
r"Software\Chromium\NativeMessagingHosts\com.tsafe.host",
r"Software\Microsoft\Edge\NativeMessagingHosts\com.tsafe.host",
] {
eprintln!(" HKCU\\{rel}");
}
eprintln!();
let firefox_dir_result = firefox_manifest_dir();
eprintln!(" Firefox manifest directory:");
match &firefox_dir_result {
Ok(d) => {
let ff_manifest = d.join("com.tsafe.host.json");
let status = if ff_manifest.is_file() {
" [already registered]"
} else {
""
};
eprintln!(" {}{status}", ff_manifest.display());
}
Err(e) => eprintln!(" (could not resolve: {e})"),
}
eprintln!();
if host_found {
eprintln!("Next step (Chromium-family browsers):");
eprintln!(" 1. Load the extension in Chrome/Edge/Brave.");
eprintln!(" 2. Open chrome://extensions (or edge://extensions), enable Developer mode.");
eprintln!(" 3. Copy the 32-character extension ID shown beneath the extension name.");
eprintln!(" 4. Run:");
eprintln!(" tsafe browser-native-host register --extension-id <32-char-id>");
eprintln!();
eprintln!("Next step (Firefox):");
eprintln!(" 1. Load the extension in Firefox (about:debugging → Load Temporary Add-on).");
eprintln!(" 2. Run:");
eprintln!(" tsafe browser-native-host register --extension-id tsafe@tsafe.dev");
} else {
eprintln!("Fix the missing binary first, then re-run detect, then register.");
}
Ok(())
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
struct UnixBrowserPath {
name: &'static str,
app_marker: &'static str,
manifest_dir_under_home: &'static str,
is_firefox: bool,
}
#[cfg(target_os = "macos")]
const MACOS_PATHS: &[UnixBrowserPath] = &[
UnixBrowserPath {
name: "Google Chrome",
app_marker: "/Applications/Google Chrome.app",
manifest_dir_under_home: "Library/Application Support/Google/Chrome/NativeMessagingHosts",
is_firefox: false,
},
UnixBrowserPath {
name: "Brave Browser",
app_marker: "/Applications/Brave Browser.app",
manifest_dir_under_home:
"Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts",
is_firefox: false,
},
UnixBrowserPath {
name: "Microsoft Edge",
app_marker: "/Applications/Microsoft Edge.app",
manifest_dir_under_home: "Library/Application Support/Microsoft Edge/NativeMessagingHosts",
is_firefox: false,
},
UnixBrowserPath {
name: "Chromium",
app_marker: "/Applications/Chromium.app",
manifest_dir_under_home: "Library/Application Support/Chromium/NativeMessagingHosts",
is_firefox: false,
},
UnixBrowserPath {
name: "Arc",
app_marker: "/Applications/Arc.app",
manifest_dir_under_home: "Library/Application Support/Arc/User Data/NativeMessagingHosts",
is_firefox: false,
},
UnixBrowserPath {
name: "Firefox",
app_marker: "/Applications/Firefox.app",
manifest_dir_under_home: "Library/Application Support/Mozilla/NativeMessagingHosts",
is_firefox: true,
},
];
#[cfg(target_os = "linux")]
const LINUX_PATHS: &[UnixBrowserPath] = &[
UnixBrowserPath {
name: "Google Chrome",
app_marker: "/usr/bin/google-chrome",
manifest_dir_under_home: ".config/google-chrome/NativeMessagingHosts",
is_firefox: false,
},
UnixBrowserPath {
name: "Chromium",
app_marker: "/usr/bin/chromium",
manifest_dir_under_home: ".config/chromium/NativeMessagingHosts",
is_firefox: false,
},
UnixBrowserPath {
name: "Microsoft Edge",
app_marker: "/usr/bin/microsoft-edge",
manifest_dir_under_home: ".config/microsoft-edge/NativeMessagingHosts",
is_firefox: false,
},
UnixBrowserPath {
name: "Brave Browser",
app_marker: "/usr/bin/brave-browser",
manifest_dir_under_home: ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts",
is_firefox: false,
},
UnixBrowserPath {
name: "Firefox",
app_marker: "/usr/bin/firefox",
manifest_dir_under_home: ".mozilla/native-messaging-hosts",
is_firefox: true,
},
];
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[derive(serde::Serialize)]
struct UnixManifestChromium {
name: &'static str,
description: &'static str,
path: String,
#[serde(rename = "type")]
ty: &'static str,
allowed_origins: Vec<String>,
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[derive(serde::Serialize)]
struct UnixManifestFirefox {
name: &'static str,
description: &'static str,
path: String,
#[serde(rename = "type")]
ty: &'static str,
allowed_extensions: Vec<String>,
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn unix_host_exe_path() -> Result<std::path::PathBuf> {
let tsafe = std::env::current_exe().context("could not resolve path to tsafe binary")?;
if let Some(dir) = tsafe.parent() {
let sibling = dir.join("tsafe-nativehost");
if sibling.is_file() {
return Ok(sibling);
}
}
if let Some(home) = directories::UserDirs::new().map(|d| d.home_dir().to_path_buf()) {
let cargo_bin = home.join(".cargo/bin/tsafe-nativehost");
if cargo_bin.is_file() {
return Ok(cargo_bin);
}
}
anyhow::bail!(
"tsafe-nativehost binary not found next to `tsafe` or in ~/.cargo/bin/.\n\
Run `cargo install --path crates/tsafe-nativehost --locked --force` (from the tsafe repo)\n\
or place the binary next to the tsafe executable."
)
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn unix_register_impl(extension_id: &str, paths: &[UnixBrowserPath]) -> Result<()> {
use std::fs;
if extension_id.trim().is_empty() {
anyhow::bail!("--extension-id is required (and must not be empty)");
}
let host = unix_host_exe_path()?;
let host_str = host
.to_str()
.ok_or_else(|| anyhow::anyhow!("native host path is not valid UTF-8"))?
.to_string();
let home = directories::UserDirs::new()
.map(|d| d.home_dir().to_path_buf())
.context("could not resolve $HOME")?;
let mut wrote_any = false;
let mut skipped: Vec<&str> = Vec::new();
for browser in paths {
let marker = std::path::Path::new(browser.app_marker);
if !marker.exists() {
skipped.push(browser.name);
continue;
}
let target_dir = home.join(browser.manifest_dir_under_home);
fs::create_dir_all(&target_dir)
.with_context(|| format!("create directory {}", target_dir.display()))?;
let manifest_path = target_dir.join("com.tsafe.host.json");
let json = if browser.is_firefox {
serde_json::to_string_pretty(&UnixManifestFirefox {
name: "com.tsafe.host",
description: "tsafe vault native messaging host",
path: host_str.clone(),
ty: "stdio",
allowed_extensions: vec![extension_id.to_string()],
})?
} else {
serde_json::to_string_pretty(&UnixManifestChromium {
name: "com.tsafe.host",
description: "tsafe vault native messaging host",
path: host_str.clone(),
ty: "stdio",
allowed_origins: vec![format!("chrome-extension://{extension_id}/")],
})?
};
fs::write(&manifest_path, json)
.with_context(|| format!("write manifest {}", manifest_path.display()))?;
eprintln!(" ✓ {} → {}", browser.name, manifest_path.display());
wrote_any = true;
}
if !skipped.is_empty() {
eprintln!(" - skipped (not installed): {}", skipped.join(", "));
}
if !wrote_any {
anyhow::bail!(
"no supported browsers detected; nothing to register.\n\
Checked: {}",
paths.iter().map(|p| p.name).collect::<Vec<_>>().join(", ")
);
}
eprintln!("\nRegistered native messaging host com.tsafe.host with extension {extension_id}");
eprintln!("Reload the extension in your browser if it's already loaded.");
Ok(())
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn unix_unregister_impl(paths: &[UnixBrowserPath]) -> Result<()> {
use std::fs;
let home = directories::UserDirs::new()
.map(|d| d.home_dir().to_path_buf())
.context("could not resolve $HOME")?;
let mut removed_any = false;
for browser in paths {
let manifest_path = home
.join(browser.manifest_dir_under_home)
.join("com.tsafe.host.json");
if manifest_path.is_file() {
match fs::remove_file(&manifest_path) {
Ok(()) => {
eprintln!(" ✓ removed {}", manifest_path.display());
removed_any = true;
}
Err(e) => {
eprintln!(
" warning: could not remove {}: {e}",
manifest_path.display()
);
}
}
}
}
if !removed_any {
eprintln!("No tsafe native-host manifests found; nothing to remove.");
}
Ok(())
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn unix_detect_impl(paths: &[UnixBrowserPath]) -> Result<()> {
let tsafe = std::env::current_exe().context("could not resolve path to tsafe binary")?;
let sibling = tsafe.parent().map(|d| d.join("tsafe-nativehost"));
let cargo_bin =
directories::UserDirs::new().map(|d| d.home_dir().join(".cargo/bin/tsafe-nativehost"));
let host_found_at: Option<std::path::PathBuf> = sibling
.filter(|p| p.is_file())
.or_else(|| cargo_bin.filter(|p| p.is_file()));
let home = directories::UserDirs::new()
.map(|d| d.home_dir().to_path_buf())
.context("could not resolve $HOME")?;
eprintln!("=== tsafe browser-native-host detect ===");
eprintln!();
match &host_found_at {
Some(p) => {
eprintln!(" native-host binary : FOUND");
eprintln!(" {}", p.display());
}
None => {
eprintln!(" native-host binary : NOT FOUND");
if let Some(ref s) = sibling {
eprintln!(" Checked sibling : {}", s.display());
}
if let Some(ref c) = cargo_bin {
eprintln!(" Checked cargo/bin: {}", c.display());
}
eprintln!(" Install via: cargo install --path crates/tsafe-nativehost --locked");
}
}
eprintln!();
eprintln!(" Manifest paths that `register` would write (installed browsers only):");
let mut detected_any = false;
let mut not_installed: Vec<&str> = Vec::new();
for browser in paths {
let marker = std::path::Path::new(browser.app_marker);
if marker.exists() {
let manifest_path = home
.join(browser.manifest_dir_under_home)
.join("com.tsafe.host.json");
let current_status = if manifest_path.is_file() {
" [already registered]"
} else {
""
};
eprintln!(
" {} — {}{}",
browser.name,
manifest_path.display(),
current_status
);
detected_any = true;
} else {
not_installed.push(browser.name);
}
}
if !not_installed.is_empty() {
eprintln!(" Not installed (skipped): {}", not_installed.join(", "));
}
if !detected_any {
eprintln!(" (no supported browsers detected)");
}
eprintln!();
if host_found_at.is_some() && detected_any {
eprintln!("Next step:");
eprintln!(" 1. Load the extension in your browser.");
eprintln!(" 2. Find the extension ID:");
eprintln!(" Chrome/Edge/Brave: chrome://extensions (Developer mode)");
eprintln!(" Firefox: the addon ID from your manifest.json (tsafe@tsafe.dev)");
eprintln!(" 3. Run:");
eprintln!(" tsafe browser-native-host register --extension-id <id>");
} else if host_found_at.is_none() {
eprintln!("Fix the missing binary first, then re-run detect, then register.");
} else {
eprintln!("Install a supported browser first, then re-run detect.");
}
Ok(())
}