feat(vault): vault lifecycle commands; seal summary; decrypt on export (T8.8)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user