// Kestrove desktop (Windows/Linux; macOS uses kestrove-apple, but this builds and runs there for // development). Thin Tauri shell over kv-ffi: the web UI only renders and never sees keys. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; use std::{io, thread}; use kestrove_desktop::autolock::{self, Activity, Reason}; use kestrove_desktop::broker::{Broker, PairingStore, PairingView, SaveView, Ui}; use kestrove_desktop::device_key::DeviceKey; use kestrove_desktop::hello::{self, HelloError}; use kestrove_desktop::passkey::PasskeyView; use kestrove_desktop::sync::{ self, AddDeviceView, DeviceView, JoinConfirmView, JoinPollView, JoinView, SetupView, SignIn, SyncService, SyncStatus, }; use kestrove_desktop::updates::{UpdateInfo, Updates}; use kestrove_desktop::{ipc, private_file}; use kv_ffi::{generate_password as core_generate_password, totp_code, CoreError, ItemDto, ItemKindDto, VaultCore}; use serde::{Deserialize, Serialize}; use tauri::menu::{Menu, MenuItem}; use tauri::tray::TrayIconBuilder; use tauri::{AppHandle, Emitter, Manager, State, WindowEvent}; use zeroize::Zeroizing; const CLIPBOARD_CLEAR: Duration = Duration::from_secs(90); struct App { core: Arc, sync: Arc, broker: Arc, activity: Arc, data_dir: PathBuf, ipc_error: Mutex>, idle_minutes: Arc, /// Windows Hello is set up on this computer (checked off the main thread; see `refresh_hello`). hello_available: Arc, updates: Arc, // Kept alive: on X11 the clipboard owner must outlive the copy. clipboard: Mutex>, } impl App { fn settings_path(&self) -> PathBuf { self.data_dir.join("settings.json") } fn save_settings(&self) -> CmdResult<()> { let s = Settings { idle_minutes: self.idle_minutes.load(Ordering::Relaxed), check_updates: self.updates.automatic(), }; let bytes = serde_json::to_vec(&s).map_err(|e| e.to_string())?; private_file::write(&self.settings_path(), &bytes).map_err(|e| e.to_string()) } /// Re-checks Windows Hello in the background (it can be set up or removed while Kestrove runs). fn refresh_hello(&self) { let available = self.hello_available.clone(); let _ = thread::Builder::new().name("hello-check".into()).spawn(move || { available.store(hello::is_supported(), Ordering::Relaxed); }); } } #[derive(Serialize, Deserialize)] struct Settings { idle_minutes: u64, /// Check for a new version once a day (Windows; see `updates`). #[serde(default = "yes")] check_updates: bool, } fn yes() -> bool { true } #[derive(Serialize)] struct AppState { has_account: bool, locked: bool, /// The Secret Key is kept on this device, so unlock only needs the master password. remembers_secret_key: bool, /// Browser integration could not start (e.g. another instance is running). ipc_error: Option, idle_minutes: u64, /// Unlock with Windows Hello: can be turned on, is on, is offered on the unlock screen now. hello: hello::Status, } #[derive(Serialize)] struct Summary { id: String, title: String, username: Option, urls: Vec, favorite: bool, has_totp: bool, tags: Vec, } #[derive(Serialize)] struct Detail { id: String, title: String, username: Option, password: Option, urls: Vec, totp: Option, notes: Option, tags: Vec, /// "Google · sami@example.com" when the account signs in with a provider. sign_in_with: Option, } #[derive(Serialize, Deserialize)] struct SignInView { provider: String, #[serde(default)] provider_name: String, account: Option, } #[derive(Serialize)] struct ProviderView { id: String, name: String, } #[derive(Deserialize)] struct NewLogin { title: String, username: Option, password: Option, urls: Vec, totp: Option, #[serde(default)] tags: Vec, #[serde(default)] sign_in_with: Option, } #[derive(Serialize)] struct Totp { code: String, seconds_remaining: u64, period: u64, } #[derive(Serialize)] struct PairingSummary { id: String, name: String, caller: String, words: Vec, created_at: u64, } type CmdResult = Result; fn err(e: CoreError) -> String { e.to_string() } /// Tauri events to the UI; the window is brought forward so the user sees the prompt. struct TauriUi { handle: AppHandle, activity: Arc, } impl TauriUi { fn surface(&self) { if let Some(w) = self.handle.get_webview_window("main") { let _ = w.show(); let _ = w.unminimize(); let _ = w.set_focus(); } } } impl Ui for TauriUi { fn pairing_requested(&self, req: &PairingView) { self.surface(); let _ = self.handle.emit("pairing-request", req); } fn save_requested(&self, req: &SaveView) { self.surface(); let _ = self.handle.emit("save-request", req); } fn passkey_requested(&self, req: &PasskeyView) { self.surface(); let _ = self.handle.emit("passkey-request", req); } fn passkey_settled(&self, id: &str) { let _ = self.handle.emit("passkey-settled", id); } fn activity(&self) { self.activity.touch(); } } #[tauri::command] fn state(app: State<'_, App>) -> AppState { AppState { has_account: app.core.has_account(), locked: app.core.is_locked(), remembers_secret_key: app.sync.stored_secret_key().is_some(), ipc_error: app.ipc_error.lock().unwrap_or_else(|e| e.into_inner()).clone(), idle_minutes: app.idle_minutes.load(Ordering::Relaxed), hello: hello::status(&app.core, &app.data_dir, app.hello_available.load(Ordering::Relaxed)), } } // Argon2id takes ~0.5 s, so these run as async commands off the main thread. /// "Create a new account": a new vault here, registered with the server using its setup code or an invite. #[tauri::command] async fn create_account( app: State<'_, App>, server_url: String, pins: String, setup_code: String, email: String, master_password: String, ) -> CmdResult { let master_password = Zeroizing::new(master_password); let sk = app.sync.create_account(&server_url, sync::parse_pins(&pins), &setup_code, &email, master_password).await; app.activity.touch(); sk.map(|s| s.to_string()) } /// `secret_key` may be omitted once this device remembers it. #[tauri::command] async fn unlock(app: State<'_, App>, master_password: String, secret_key: Option) -> CmdResult<()> { let master_password = Zeroizing::new(master_password); let typed = secret_key.map(Zeroizing::new).filter(|s| !s.trim().is_empty()); let sk = typed.clone().or_else(|| app.sync.stored_secret_key()).ok_or("Enter your Secret Key.")?; app.core.unlock(master_password.to_string(), sk.trim().to_string()).map_err(err)?; if let Some(sk) = typed { app.sync.remember_secret_key(&sk); } hello::password_unlocked(&app.data_dir); app.activity.touch(); app.sync.request_sync(); Ok(()) } /// Windows Hello calls block until the prompt closes, so they run off the main thread and the async runtime. async fn off_thread(f: impl FnOnce() -> T + Send + 'static) -> CmdResult { tauri::async_runtime::spawn_blocking(f).await.map_err(|e| e.to_string()) } /// Unlock with Windows Hello. `false` when the prompt was cancelled (the password field stays). #[tauri::command] async fn hello_unlock(app: State<'_, App>) -> CmdResult { let (core, dir) = (app.core.clone(), app.data_dir.clone()); match off_thread(move || hello::unlock(&core, &dir)).await? { Ok(()) => { app.activity.touch(); app.sync.request_sync(); Ok(true) } Err(HelloError::Cancelled) => Ok(false), Err(e) => Err(e.to_string()), } } /// Settings → "Unlock with Windows Hello". `false` when turning it on was cancelled. #[tauri::command] async fn set_hello(app: State<'_, App>, enabled: bool) -> CmdResult { let (core, dir) = (app.core.clone(), app.data_dir.clone()); let r = off_thread(move || if enabled { hello::enable(&core, &dir) } else { hello::disable(&core, &dir) }).await?; app.refresh_hello(); match r { Ok(()) => Ok(true), Err(HelloError::Cancelled) => Ok(false), Err(e) => Err(e.to_string()), } } #[tauri::command] async fn lock(app: State<'_, App>) -> CmdResult<()> { app.core.lock(); app.sync.on_lock().await; Ok(()) } /// UI input (throttled in JS): feeds the idle timer where the OS has no system idle time. #[tauri::command] fn touch(app: State<'_, App>) { app.activity.touch(); } #[tauri::command] fn set_idle_minutes(app: State<'_, App>, minutes: u64) -> CmdResult<()> { let minutes = minutes.clamp(1, 240); app.idle_minutes.store(minutes, Ordering::Relaxed); app.save_settings() } // ---------------------------------------------------------------- updates (Windows; see `updates`) #[tauri::command] fn update_info(app: State<'_, App>, handle: AppHandle) -> UpdateInfo { app.updates.info(&handle) } #[tauri::command] async fn check_update(app: State<'_, App>, handle: AppHandle) -> CmdResult { Ok(app.updates.check(&handle).await) } #[tauri::command] fn set_update_checks(app: State<'_, App>, enabled: bool) -> CmdResult<()> { app.updates.set_automatic(enabled); app.save_settings() } /// "Update and restart": returns only if it failed; on success the installer replaces and restarts the app. #[tauri::command] async fn install_update(app: State<'_, App>, handle: AppHandle) -> CmdResult<()> { let core = app.core.clone(); app.updates.install(&handle, move || core.lock()).await } #[tauri::command] fn list_items(app: State<'_, App>) -> CmdResult> { Ok(app .core .list_items() .map_err(err)? .into_iter() .map(|s| Summary { id: s.id, title: s.title, username: s.username, urls: s.urls, favorite: s.favorite, has_totp: s.has_totp, tags: s.tags, }) .collect()) } #[tauri::command] fn get_item(app: State<'_, App>, id: String) -> CmdResult { let d = app.core.get_item(id).map_err(err)?; Ok(Detail { id: d.id, title: d.title, username: d.username, password: d.password, urls: d.urls, totp: d.totp, notes: d.notes, tags: d.tags, sign_in_with: d.sign_in_with.map(|w| SignInView { provider_name: kv_ffi::sign_in_provider_name(w.provider.clone()), provider: w.provider, account: w.account, }), }) } #[tauri::command] fn sign_in_providers() -> Vec { kv_ffi::sign_in_providers().into_iter().map(|p| ProviderView { id: p.id, name: p.name }).collect() } /// Replaces an item's tags (the core normalises them); everything else about the item stays. #[tauri::command] fn set_tags(app: State<'_, App>, id: String, tags: Vec) -> CmdResult<()> { let mut item = app.core.get_item(id).map_err(err)?; item.tags = tags; app.core.save_item(item).map_err(err)?; app.sync.request_sync(); Ok(()) } #[tauri::command] fn save_item(app: State<'_, App>, item: NewLogin) -> CmdResult { app.core .save_item(ItemDto { id: String::new(), kind: ItemKindDto::Login, title: item.title, username: item.username, password: item.password, urls: item.urls, totp: item.totp.filter(|t| !t.trim().is_empty()), notes: None, favorite: false, tags: item.tags, sign_in_with: item.sign_in_with.map(|w| kv_ffi::SignInWithDto { provider: w.provider, account: w.account }), modified_at: 0, }) .map_err(err) .inspect(|_| app.sync.request_sync()) } #[tauri::command] fn delete_item(app: State<'_, App>, id: String) -> CmdResult<()> { app.core.delete_item(id).map_err(err)?; app.sync.request_sync(); Ok(()) } #[tauri::command] fn totp(app: State<'_, App>, id: String) -> CmdResult> { let Some(secret) = app.core.get_item(id).map_err(err)?.totp else { return Ok(None) }; let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs()); let c = totp_code(secret, now).map_err(err)?; Ok(Some(Totp { code: c.code, seconds_remaining: c.seconds_remaining, period: c.period })) } /// Copies from Rust so the secret never round-trips through JS, then clears it after 90 s. #[tauri::command] fn copy_secret(app: State<'_, App>, id: String, field: Option) -> CmdResult<()> { let item = app.core.get_item(id).map_err(err)?; let secret = Zeroizing::new(match field.as_deref() { Some("username") => item.username.unwrap_or_default(), Some("totp") => { let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs()); item.totp.map(|t| totp_code(t, now)).transpose().map_err(err)?.map(|c| c.code).unwrap_or_default() } _ => item.password.unwrap_or_default(), }); let mut guard = app.clipboard.lock().unwrap_or_else(|e| e.into_inner()); if guard.is_none() { *guard = Some(arboard::Clipboard::new().map_err(|e| e.to_string())?); } let cb = guard.as_mut().expect("clipboard initialized above"); #[cfg(windows)] let copied = { // docs/04: keep secrets out of clipboard history, cloud sync and monitors. use arboard::SetExtWindows; cb.set().exclude_from_monitoring().exclude_from_cloud().exclude_from_history().text(secret.to_string()) }; #[cfg(not(windows))] let copied = cb.set_text(secret.to_string()); copied.map_err(|e| e.to_string())?; drop(guard); thread::spawn(move || { thread::sleep(CLIPBOARD_CLEAR); if let Ok(mut cb) = arboard::Clipboard::new() { if cb.get_text().ok().as_deref() == Some(secret.as_str()) { let _ = cb.clear(); } } }); Ok(()) } #[tauri::command] fn generate_password(length: u32) -> String { core_generate_password(length.clamp(8, 128), true) } // ---------------------------------------------------------------- browser pairing + saves #[tauri::command] fn pending_pairing(app: State<'_, App>) -> Option { app.broker.pending_pairing() } #[tauri::command] fn resolve_pairing(app: State<'_, App>, id: String, accept: bool) -> CmdResult<()> { app.broker.resolve_pairing(&id, accept) } #[tauri::command] fn list_pairings(app: State<'_, App>) -> Vec { app.broker .pairings() .into_iter() .map(|p| PairingSummary { id: p.id, name: p.name, caller: p.caller, words: p.words, created_at: p.created_at }) .collect() } #[tauri::command] fn revoke_pairing(app: State<'_, App>, id: String) -> CmdResult<()> { app.broker.revoke_pairing(&id) } #[tauri::command] fn pending_saves(app: State<'_, App>) -> Vec { app.broker.pending_saves() } #[tauri::command] fn resolve_save(app: State<'_, App>, id: String, accept: bool) -> CmdResult<()> { if app.broker.resolve_save(&id, accept)?.is_some() { app.sync.request_sync(); } Ok(()) } /// The colour theme and appearance saved for all the account's devices (docs/02 "App settings"). #[derive(Serialize)] struct ThemeView { theme: Option, appearance: Option, } #[tauri::command] fn app_theme(app: State<'_, App>) -> CmdResult { let t = app.core.app_theme().map_err(err)?; Ok(ThemeView { theme: t.theme, appearance: t.appearance }) } #[tauri::command] fn set_app_theme(app: State<'_, App>, theme: Option, appearance: Option) -> CmdResult<()> { app.core.set_app_theme(theme, appearance).map_err(err)?; app.sync.request_sync(); Ok(()) } #[tauri::command] fn pending_passkeys(app: State<'_, App>) -> Vec { app.broker.pending_passkeys() } /// A page's passkey request (docs/05 "Passkeys in the browser"). Accepting needs the master password: the user /// verification every passkey use requires, where the app has no biometrics to ask. #[tauri::command] fn resolve_passkey( app: State<'_, App>, id: String, accept: bool, credential_id: Option, master_password: Option, ) -> CmdResult<()> { if accept { let pw = Zeroizing::new(master_password.unwrap_or_default()); app.core.confirm_master_password(pw.to_string()).map_err(|e| match e { CoreError::BadCredentials => "Wrong master password.".to_string(), e => err(e), })?; } app.broker.resolve_passkey(&id, accept, credential_id.as_deref())?; app.activity.touch(); if accept { app.sync.request_sync(); } Ok(()) } // ---------------------------------------------------------------- sync server (docs/03) #[tauri::command] fn sync_status(app: State<'_, App>) -> SyncStatus { app.sync.status() } #[tauri::command] async fn sync_now(app: State<'_, App>) -> CmdResult { app.sync.sync_now().await } /// Settings: server URL and optional pins for a vault created before sync (or to change them). #[tauri::command] fn configure_server(app: State<'_, App>, server_url: String, pins: String) -> CmdResult<()> { app.sync.configure(&server_url, sync::parse_pins(&pins)) } #[tauri::command] async fn server_setup_open(app: State<'_, App>) -> CmdResult { app.sync.setup_open().await } #[tauri::command] async fn register_server(app: State<'_, App>, setup_code: String, email: String) -> CmdResult<()> { app.sync.register(&setup_code, &email).await } #[tauri::command] async fn list_devices(app: State<'_, App>) -> CmdResult> { app.sync.devices().await } #[tauri::command] async fn approve_device(app: State<'_, App>, device_id: String) -> CmdResult<()> { app.sync.approve_device(&device_id).await } #[tauri::command] async fn revoke_device(app: State<'_, App>, device_id: String) -> CmdResult<()> { app.sync.revoke_device(&device_id).await } // ---------------------------------------------------------------- onboarding (docs/10) #[tauri::command] async fn join_start(app: State<'_, App>, server_url: String) -> CmdResult { app.sync.join_start(&server_url).await } #[tauri::command] async fn join_poll(app: State<'_, App>) -> CmdResult { app.sync.join_poll().await } #[tauri::command] fn join_cancel(app: State<'_, App>) { app.sync.join_cancel(); } #[tauri::command] fn setup_from_text(app: State<'_, App>, text: String) -> CmdResult { app.sync.setup_from_text(&Zeroizing::new(text)) } /// "Open Emergency Kit…": the file's bytes (raw IPC body), read by the web view's file picker. #[tauri::command] async fn setup_from_file(app: State<'_, App>, request: tauri::ipc::Request<'_>) -> CmdResult { let tauri::ipc::InvokeBody::Raw(bytes) = request.body() else { return Err("Expected the file's bytes.".into()) }; app.sync.setup_from_file(bytes) } /// The theme's `bg` and `text` for the native title bar (Windows 11 only; see `title_bar`). #[tauri::command] fn set_title_bar_colors(window: tauri::WebviewWindow, bg: String, text: String) { #[cfg(windows)] kestrove_desktop::title_bar::paint(&window, &bg, &text); #[cfg(not(windows))] let _ = (window, bg, text); } #[tauri::command] fn clear_setup(app: State<'_, App>) { app.sync.clear_setup(); } #[tauri::command] async fn finish_setup(app: State<'_, App>, master_password: String) -> CmdResult { let r = app.sync.finish_setup(Zeroizing::new(master_password)).await; app.activity.touch(); r } #[tauri::command] async fn add_device(app: State<'_, App>) -> CmdResult { app.sync.add_device().await } #[tauri::command] async fn cancel_add_device(app: State<'_, App>) -> CmdResult<()> { app.sync.cancel_add_device().await; Ok(()) } #[tauri::command] async fn approve_join(app: State<'_, App>, join_code: String) -> CmdResult { app.sync.approve_join(&join_code).await } #[tauri::command] async fn send_join(app: State<'_, App>) -> CmdResult<()> { app.sync.send_join().await } #[tauri::command] fn kit_qr(app: State<'_, App>) -> CmdResult { app.sync.kit_qr() } // ---------------------------------------------------------------- startup fn start_ipc(broker: Arc) -> io::Result<()> { let listener = ipc::Listener::bind()?; thread::Builder::new().name("ipc-accept".into()).spawn(move || loop { match listener.accept() { Ok(conn) => { let b = broker.clone(); let _ = thread::Builder::new().name("ipc-conn".into()).spawn(move || { let _ = b.serve(conn); }); } Err(e) => { eprintln!("kestrove: IPC accept failed: {e}"); thread::sleep(Duration::from_secs(1)); } } })?; Ok(()) } fn lock_and_notify(handle: &AppHandle, sync: &Arc, reason: &str) { sync.core().lock(); let sync = sync.clone(); tauri::async_runtime::spawn(async move { sync.on_lock().await }); let _ = handle.emit("locked", reason); } fn setup(app: &mut tauri::App) -> Result<(), Box> { // KESTROVE_DATA_DIR runs a second instance with its own vault (development and the two-device tests). let data_dir = match std::env::var_os("KESTROVE_DATA_DIR") { Some(d) => PathBuf::from(d), None => app.path().app_data_dir()?, }; std::fs::create_dir_all(&data_dir)?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&data_dir, std::fs::Permissions::from_mode(0o700))?; } let core = VaultCore::open(data_dir.join("vault.sqlite").to_string_lossy().into_owned())?; let key = if std::env::var_os("KESTROVE_DEVICE_KEY").is_some_and(|v| v == "file") { DeviceKey::load_or_create_file(&data_dir) } else { DeviceKey::load_or_create(&data_dir) }; let sync = { let handle = app.handle().clone(); SyncService::new( core.clone(), &data_dir, key.map_err(|e| e.to_string()), Box::new(move |s: &SyncStatus| { let _ = handle.emit("sync-status", s); }), ) }; tauri::async_runtime::spawn(sync.clone().run()); let activity = Activity::new(); let ui = Arc::new(TauriUi { handle: app.handle().clone(), activity: activity.clone() }); let broker = Broker::new(core.clone(), ui, PairingStore::open(data_dir.join("pairings.json"))?); let settings = private_file::read(&data_dir.join("settings.json")) .ok() .flatten() .and_then(|b| serde_json::from_slice::(&b).ok()); let idle_minutes = settings.as_ref().map_or(autolock::DEFAULT_IDLE.as_secs() / 60, |s| s.idle_minutes); let idle_minutes = Arc::new(AtomicU64::new(idle_minutes)); let updates = Updates::new(settings.as_ref().is_none_or(|s| s.check_updates)); updates.clone().spawn(app.handle().clone()); let ipc_error = start_ipc(broker.clone()).err().map(|e| { eprintln!("kestrove: browser integration disabled: {e}"); if e.kind() == io::ErrorKind::AddrInUse { "Another Kestrove instance is serving the browser extension.".to_string() } else { format!("Browser integration could not start: {e}") } }); { let (handle, sync, minutes) = (app.handle().clone(), sync.clone(), idle_minutes.clone()); let unlocked = core.clone(); autolock::spawn( activity.clone(), move || Duration::from_secs(minutes.load(Ordering::Relaxed) * 60), move || !unlocked.is_locked(), move |reason| { let why = match reason { Reason::Idle => "idle", Reason::ScreenLocked => "screen-locked", Reason::Sleep => "sleep", }; lock_and_notify(&handle, &sync, why); }, ); } // Tray: the app keeps running (for the browser extension) when its window is closed. let open = MenuItem::with_id(app, "open", "Open Kestrove", true, None::<&str>)?; let lock_item = MenuItem::with_id(app, "lock", "Lock", true, None::<&str>)?; let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; let menu = Menu::with_items(app, &[&open, &lock_item, &quit])?; let mut tray = TrayIconBuilder::with_id("main").menu(&menu).tooltip("Kestrove").on_menu_event({ let sync = sync.clone(); move |handle, event| match event.id().as_ref() { "open" => { if let Some(w) = handle.get_webview_window("main") { let _ = w.show(); let _ = w.set_focus(); } } "lock" => lock_and_notify(handle, &sync, "manual"), "quit" => { sync.core().lock(); // Revoke an "Add a device" ticket still on screen (bounded: never hang Quit on the network). let s = sync.clone(); let _ = tauri::async_runtime::block_on(async move { tokio::time::timeout(Duration::from_secs(2), s.on_lock()).await }); handle.exit(0); } _ => {} } }); if let Some(icon) = app.default_window_icon() { tray = tray.icon(icon.clone()); } tray.build(app)?; app.manage(App { core, sync, broker, activity, data_dir, ipc_error: Mutex::new(ipc_error), idle_minutes, hello_available: Arc::new(AtomicBool::new(false)), updates, clipboard: Mutex::new(None), }); app.state::().refresh_hello(); Ok(()) } fn main() { let builder = tauri::Builder::default(); #[cfg(windows)] let builder = builder.plugin(tauri_plugin_updater::Builder::new().build()); builder .setup(setup) .on_window_event(|window, event| { if let WindowEvent::CloseRequested { api, .. } = event { // Keep serving the extension from the tray; Quit is in the tray menu. api.prevent_close(); let _ = window.hide(); } }) .invoke_handler(tauri::generate_handler![ state, set_title_bar_colors, create_account, unlock, hello_unlock, set_hello, lock, touch, set_idle_minutes, list_items, get_item, save_item, delete_item, totp, copy_secret, generate_password, pending_pairing, resolve_pairing, list_pairings, revoke_pairing, pending_saves, pending_passkeys, set_tags, app_theme, set_app_theme, sign_in_providers, resolve_passkey, resolve_save, sync_status, sync_now, configure_server, server_setup_open, register_server, list_devices, approve_device, revoke_device, join_start, join_poll, join_cancel, setup_from_text, setup_from_file, clear_setup, finish_setup, add_device, cancel_add_device, approve_join, send_join, kit_qr, update_info, check_update, set_update_checks, install_update ]) .run(tauri::generate_context!()) .expect("error while running Kestrove"); }