Initial scaffold for kestrove-desktop

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-15 18:30:14 +09:00
co-authored by Claude Opus 5.5
commit d2cbd56ff7
19 changed files with 1802 additions and 0 deletions
+151
View File
@@ -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");
}