//! Vault lock controller (port of `security/vaultLockController.js`): inactivity //! auto-lock, optional lock on focus loss, and the ordered lock sequence (key wiped //! synchronously first, then module windows, diagnostics and SSH sessions). use crate::core::app::Core; use crate::core::vault::VaultStatus; use parking_lot::Mutex; use serde_json::{json, Value}; use std::time::{Duration, Instant}; pub struct LockController { last_activity: Mutex, locking: tokio::sync::Mutex<()>, } impl Default for LockController { fn default() -> Self { Self { last_activity: Mutex::new(Instant::now()), locking: tokio::sync::Mutex::new(()) } } } impl LockController { pub fn activity(&self) { *self.last_activity.lock() = Instant::now(); } pub fn idle_for(&self) -> Duration { self.last_activity.lock().elapsed() } pub async fn check(&self, core: &Core) { let minutes = core.config.get()["security"]["autoLockMinutes"].as_u64().unwrap_or(0); if minutes > 0 && core.vault.is_unlocked() && self.idle_for() >= Duration::from_secs(minutes * 60) { self.lock(core, "inactivity").await; } } pub async fn lost_focus(&self, core: &Core) { if core.config.get()["security"]["lockOnBlur"].as_bool() == Some(true) && core.vault.is_unlocked() { self.lock(core, "focus-loss").await; } } pub async fn lock(&self, core: &Core, reason: &str) -> VaultStatus { let _guard = self.locking.lock().await; core.services.biometrics.cancel(); if reason != "manual" { core.services.biometrics.arm_auto_unlock(); } // Clear the key synchronously, before waiting for potentially slow SSH cleanup. let status = core.vault.lock(); core.services.modules.close_all(core); core.services.diagnostics.stop_all(); core.services.sessions.disconnect_all(&format!("vault-{reason}")).await; let mut payload: Value = serde_json::to_value(&status).unwrap_or_else(|_| json!({})); payload["reason"] = Value::String(reason.to_string()); core.emit_main("vault:locked", payload); status } }