feat(vault): vault lifecycle commands; seal summary; decrypt on export (T8.8)

This commit is contained in:
iamdoubz
2026-07-03 08:06:05 -05:00
parent 0f7af4576e
commit e194b76f6f
+66 -7
View File
@@ -1403,12 +1403,22 @@ async fn export_meeting_to(
std::fs::create_dir_all(dest_path)
.map_err(|e| WaError::new("export", e.to_string()))?;
let source_dir = meeting_dir(meeting_id);
for name in ["audio.wav", "transcript.json"] {
let src = source_dir.join(name);
if src.exists() {
std::fs::copy(&src, dest_path.join(name))
.map_err(|e| WaError::new("export", e.to_string()))?;
}
// Audio is stored plaintext — copy as-is.
let audio_src = source_dir.join("audio.wav");
if audio_src.exists() {
std::fs::copy(&audio_src, dest_path.join("audio.wav"))
.map_err(|e| WaError::new("export", e.to_string()))?;
}
// transcript.json may be vault-sealed; export the decrypted content
// so the bundle is usable (T8.8).
let transcript_src = source_dir.join("transcript.json");
if transcript_src.exists() {
let bytes = std::fs::read(&transcript_src)
.map_err(|e| WaError::new("export", e.to_string()))?;
let plain =
crate::vault::open(&bytes).map_err(|e| WaError::new("vault", e.to_string()))?;
std::fs::write(dest_path.join("transcript.json"), plain)
.map_err(|e| WaError::new("export", e.to_string()))?;
}
std::fs::write(dest_path.join("notes.md"), &meeting.notes_markdown)
.map_err(|e| WaError::new("export", e.to_string()))?;
@@ -1645,7 +1655,10 @@ pub async fn generate_summary(
};
let json = serde_json::to_string_pretty(&summary_file)
.map_err(|e| WaError::new("llm", e.to_string()))?;
let _ = std::fs::write(meeting_dir(&meeting_id).join("summary.json"), json);
// Seal at rest when the vault is unlocked (T8.8, FR-SEC-3); passthrough otherwise.
if let Ok(sealed) = crate::vault::seal(json.as_bytes()) {
let _ = std::fs::write(meeting_dir(&meeting_id).join("summary.json"), sealed);
}
let _ = app.emit(
"llm://done",
@@ -2139,6 +2152,52 @@ pub async fn set_sync_enabled(enabled: bool) -> WaResult<()> {
save_settings(&settings)
}
// ---- At-rest encryption vault (Phase 8, T8.8, FR-SEC-3) ----
#[tauri::command]
pub async fn vault_status() -> WaResult<serde_json::Value> {
Ok(serde_json::json!({
"enabled": crate::vault::is_enabled(),
"unlocked": crate::vault::is_unlocked(),
}))
}
#[derive(Deserialize)]
pub struct VaultPasswordArgs {
pub password: String,
}
/// Turn on the vault: derive a key from the password and start sealing artifacts
/// at rest. Leaves the vault unlocked for the session.
#[tauri::command]
pub async fn enable_vault(args: VaultPasswordArgs) -> WaResult<()> {
crate::vault::enable(&args.password).map_err(|e| WaError::new("vault", e.to_string()))
}
#[tauri::command]
pub async fn unlock_vault(args: VaultPasswordArgs) -> WaResult<()> {
crate::vault::unlock(&args.password).map_err(|e| WaError::new("vault", e.to_string()))
}
#[tauri::command]
pub async fn lock_vault() -> WaResult<()> {
crate::vault::lock();
Ok(())
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChangeVaultPasswordArgs {
pub old_password: String,
pub new_password: String,
}
#[tauri::command]
pub async fn change_vault_password(args: ChangeVaultPasswordArgs) -> WaResult<()> {
crate::vault::change_password(&args.old_password, &args.new_password)
.map_err(|e| WaError::new("vault", e.to_string()))
}
// ---- Sync durable queue (T9.5) ----
/// Give up auto-retrying after this many failures; the job stays `failed` with