Initial scaffold for kestrove-desktop
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
// Kestrove desktop (Windows/Linux; macOS uses kestrove-apple). Thin Tauri shell over kv-ffi.
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::{sync::Arc, thread, time::Duration};
|
||||
|
||||
use kv_ffi::{generate_password as core_generate_password, CoreError, ItemDto, ItemKindDto, VaultCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
struct App {
|
||||
// TODO(M3): persistent store + sync (kv-store/kv-sync) instead of in-memory.
|
||||
core: Arc<VaultCore>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AppState {
|
||||
has_account: bool,
|
||||
locked: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Summary {
|
||||
id: String,
|
||||
title: String,
|
||||
username: Option<String>,
|
||||
urls: Vec<String>,
|
||||
favorite: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Detail {
|
||||
id: String,
|
||||
title: String,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
urls: Vec<String>,
|
||||
totp: Option<String>,
|
||||
notes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct NewLogin {
|
||||
title: String,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
urls: Vec<String>,
|
||||
}
|
||||
|
||||
type CmdResult<T> = Result<T, String>;
|
||||
|
||||
fn err(e: CoreError) -> String {
|
||||
e.to_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn state(app: State<'_, App>) -> AppState {
|
||||
AppState { has_account: app.core.has_account(), locked: app.core.is_locked() }
|
||||
}
|
||||
|
||||
// Argon2id takes ~0.5 s, so these run as async commands off the main thread.
|
||||
#[tauri::command]
|
||||
async fn create_account(app: State<'_, App>, master_password: String) -> CmdResult<String> {
|
||||
app.core.create_account(master_password).map_err(err)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn unlock(app: State<'_, App>, master_password: String, secret_key: String) -> CmdResult<()> {
|
||||
app.core.unlock(master_password, secret_key).map_err(err)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn lock(app: State<'_, App>) {
|
||||
app.core.lock();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_items(app: State<'_, App>) -> CmdResult<Vec<Summary>> {
|
||||
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 })
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_item(app: State<'_, App>, id: String) -> CmdResult<Detail> {
|
||||
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 })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn save_item(app: State<'_, App>, item: NewLogin) -> CmdResult<String> {
|
||||
app.core
|
||||
.save_item(ItemDto {
|
||||
id: String::new(),
|
||||
kind: ItemKindDto::Login,
|
||||
title: item.title,
|
||||
username: item.username,
|
||||
password: item.password,
|
||||
urls: item.urls,
|
||||
totp: None,
|
||||
notes: None,
|
||||
favorite: false,
|
||||
tags: vec![],
|
||||
modified_at: 0,
|
||||
})
|
||||
.map_err(err)
|
||||
}
|
||||
|
||||
/// Copies the password from Rust so it never needs to round-trip through JS, then clears it after 90 s.
|
||||
/// TODO(M3): on Windows also set ExcludeClipboardContentFromMonitorProcessing (docs/04).
|
||||
#[tauri::command]
|
||||
fn copy_secret(app: State<'_, App>, id: String) -> CmdResult<()> {
|
||||
let secret = app.core.get_item(id).map_err(err)?.password.unwrap_or_default();
|
||||
let mut cb = arboard::Clipboard::new().map_err(|e| e.to_string())?;
|
||||
cb.set_text(secret.clone()).map_err(|e| e.to_string())?;
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_secs(90));
|
||||
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, true)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.manage(App { core: VaultCore::new_in_memory() })
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
state,
|
||||
create_account,
|
||||
unlock,
|
||||
lock,
|
||||
list_items,
|
||||
get_item,
|
||||
save_item,
|
||||
copy_secret,
|
||||
generate_password
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running Kestrove");
|
||||
}
|
||||
Reference in New Issue
Block a user