// file: crates/ksp-app-wallet-desk/src/wallet_session.rs // version: 8 //! Durable root-scoped Wallet session lifecycle for Wallet Desk. use ts_rs::TS; // rust-rules: trait-import /// Safe state code projected for the current Wallet session. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)] #[serde(rename_all = "snake_case")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletSessionStateDto.ts")] pub(crate) enum WalletSessionStateDto { /// No Wallet is selected. NoSelection, /// One Wallet is selected but remains locked. Locked, /// One explicit privileged Wallet operation is running in Rust. PrivilegedOperation, /// One Wallet is open with VIEW capability. ViewOpen, /// One Wallet is open with OWNER capability. OwnerOpen, } /// Minimal non-secret projection of the backend Wallet session. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletSessionDto.ts")] pub(crate) struct WalletSessionDto { /// Filename when a Wallet is selected. pub(crate) filename: std::option::Option, /// Current session state. pub(crate) state: WalletSessionStateDto, /// Root-scoped identifier when a Wallet is selected. pub(crate) wallet_id: std::option::Option, } /// One protected Wallet note exposed only from an authorized session. #[derive(serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletNoteDto.ts")] pub(crate) struct WalletNoteDto { /// Stable note identifier generated by Wallet. pub(crate) id: String, /// Protected note text available only after authorization. pub(crate) text: String, } /// Authorized Wallet projection returned after explicit VIEW/OWNER authorization. #[derive(serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletAuthorizedDto.ts")] pub(crate) struct WalletAuthorizedDto { /// Protected internal alias when configured. pub(crate) alias: std::option::Option, /// Capability that produced this projection. pub(crate) capability: String, /// Number of effective Config-owned Wallet password candidates without their names or values. pub(crate) configured_secret_candidate_count: usize, /// Native Wallet filename without its parent path. pub(crate) filename: String, /// Native Wallet format version. pub(crate) format_version: u32, /// Protected notes available to the authorized session. pub(crate) notes: std::vec::Vec, /// Authorized Solana public key. pub(crate) pubkey: String, /// Root-scoped identifier accepted by later Wallet Desk operations. pub(crate) wallet_id: String, /// Whether the Wallet has an enabled VIEW slot. pub(crate) view_enabled: bool, } /// Create request moved from the frontend directly into Wallet password wrappers. #[derive(serde::Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletCreateRequestDto.ts")] pub(crate) struct WalletCreateRequestDto { /// Optional protected internal alias stored inside the Wallet. pub(crate) alias: std::option::Option, /// Root-scoped destination filename. pub(crate) filename: String, /// Optional first protected note stored inside the Wallet. pub(crate) initial_note: std::option::Option, /// OWNER password transported only frontend -> Rust. pub(crate) owner_password: String, /// Optional VIEW password; `None` keeps VIEW disabled. pub(crate) view_password: std::option::Option, } /// Manual unlock request whose password exists only for the explicit frontend -> Rust call. #[derive(serde::Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletUnlockRequestDto.ts")] pub(crate) struct WalletUnlockRequestDto { /// Manual password transported only frontend -> Rust. pub(crate) password: String, } /// Capability targeted by one explicit unlock operation. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum WalletUnlockCapability { /// VIEW metadata-only authorization. View, /// OWNER full authorization. Owner, } impl WalletUnlockCapability { /// Returns a safe lowercase capability label for logs and projections. #[must_use] pub(crate) const fn label(self) -> &'static str { return match self { Self::View => "view", Self::Owner => "owner", }; } } /// Backend Wallet session. Full paths, configured secret names and authorized handles never cross IPC. pub(crate) enum WalletSession { /// No current Wallet selection. NoSelection, /// One re-inspected locked Wallet. Locked { /// Native Wallet filename / root-scoped identifier. wallet_id: String, /// Full root-scoped filesystem path retained only in Rust. path: std::path::PathBuf, /// Locked information re-inspected from the file. locked_info: ksp_wallet_lib::LockedWalletInfo, }, /// One explicit unlock operation currently owns the selected locked state. PrivilegedOperation { /// Capability being attempted. capability: WalletUnlockCapability, /// Native Wallet filename / root-scoped identifier. wallet_id: String, /// Full root-scoped filesystem path retained only in Rust. path: std::path::PathBuf, /// Locked information retained so a failed attempt can return to `Locked` without another KDF. locked_info: ksp_wallet_lib::LockedWalletInfo, }, /// One Wallet kept open with VIEW capability. View { /// Native Wallet filename / root-scoped identifier. wallet_id: String, /// Full root-scoped filesystem path retained only in Rust. path: std::path::PathBuf, /// Authorized VIEW handle retaining metadata capability only in Rust. wallet: std::boxed::Box, }, /// One VIEW-authorized self-service mutation temporarily owns the handle outside the session mutex. ViewOperation { /// Native Wallet filename / root-scoped identifier. wallet_id: String, /// Full root-scoped filesystem path retained only in Rust. path: std::path::PathBuf, /// Authorized Solana identity used only to reject stale completion. pubkey: ksp_core_lib::Pubkey, }, /// One Wallet kept open with OWNER capability. Owner { /// Native Wallet filename / root-scoped identifier. wallet_id: String, /// Full root-scoped filesystem path retained only in Rust. path: std::path::PathBuf, /// Authorized OWNER handle retaining secret material only in Rust. wallet: std::boxed::Box, /// Whether the authenticated Wallet currently exposes a VIEW slot. view_enabled: bool, }, /// One OWNER-authorized privileged operation temporarily owns the handle outside the session mutex. OwnerOperation { /// Native Wallet filename / root-scoped identifier. wallet_id: String, /// Full root-scoped filesystem path retained only in Rust. path: std::path::PathBuf, /// Authorized Solana identity used only to reject stale completion. pubkey: ksp_core_lib::Pubkey, /// Whether the authenticated Wallet currently exposes a VIEW slot. view_enabled: bool, }, } impl WalletSession { /// Creates an empty application session. #[must_use] pub(crate) const fn no_selection() -> Self { return Self::NoSelection; } /// Returns a minimal safe session projection. #[must_use] pub(crate) fn safe_projection(&self) -> WalletSessionDto { return match self { Self::NoSelection => WalletSessionDto { filename: std::option::Option::None, state: WalletSessionStateDto::NoSelection, wallet_id: std::option::Option::None, }, Self::Locked { wallet_id, locked_info, .. } => { let _ = locked_info.format_version(); WalletSessionDto { filename: std::option::Option::Some(wallet_id.clone()), state: WalletSessionStateDto::Locked, wallet_id: std::option::Option::Some(wallet_id.clone()), } }, Self::PrivilegedOperation { wallet_id, .. } => WalletSessionDto { filename: std::option::Option::Some(wallet_id.clone()), state: WalletSessionStateDto::PrivilegedOperation, wallet_id: std::option::Option::Some(wallet_id.clone()), }, Self::View { wallet_id, .. } => WalletSessionDto { filename: std::option::Option::Some(wallet_id.clone()), state: WalletSessionStateDto::ViewOpen, wallet_id: std::option::Option::Some(wallet_id.clone()), }, Self::ViewOperation { wallet_id, .. } => WalletSessionDto { filename: std::option::Option::Some(wallet_id.clone()), state: WalletSessionStateDto::PrivilegedOperation, wallet_id: std::option::Option::Some(wallet_id.clone()), }, Self::Owner { wallet_id, .. } => WalletSessionDto { filename: std::option::Option::Some(wallet_id.clone()), state: WalletSessionStateDto::OwnerOpen, wallet_id: std::option::Option::Some(wallet_id.clone()), }, Self::OwnerOperation { wallet_id, .. } => WalletSessionDto { filename: std::option::Option::Some(wallet_id.clone()), state: WalletSessionStateDto::PrivilegedOperation, wallet_id: std::option::Option::Some(wallet_id.clone()), }, }; } } /// Converts an OWNER handle into the authorized response DTO without exposing secret key material. pub(crate) fn owner_projection( wallet_id: &str, view_enabled: bool, configured_secret_candidate_count: usize, owner: &ksp_wallet_lib::WalletOwner, ) -> WalletAuthorizedDto { return authorized_projection(wallet_id, view_enabled, configured_secret_candidate_count, "owner", owner.info()); } /// Converts a VIEW handle into the authorized response DTO without exposing OWNER material. pub(crate) fn view_projection( wallet_id: &str, view_enabled: bool, configured_secret_candidate_count: usize, view: &ksp_wallet_lib::WalletView, ) -> WalletAuthorizedDto { return authorized_projection(wallet_id, view_enabled, configured_secret_candidate_count, "view", view.info()); } fn authorized_projection( wallet_id: &str, view_enabled: bool, configured_secret_candidate_count: usize, capability: &str, info: &ksp_wallet_lib::WalletInfo, ) -> WalletAuthorizedDto { let notes = info .notes() .iter() .map(|note| { return WalletNoteDto { id: note.id().to_owned(), text: note.text().to_owned() }; }) .collect(); return WalletAuthorizedDto { alias: info.alias().map(|value| return value.to_owned()), capability: capability.to_owned(), configured_secret_candidate_count, filename: wallet_id.to_owned(), format_version: info.format_version(), notes, pubkey: info.pubkey().to_string(), wallet_id: wallet_id.to_owned(), view_enabled, }; } #[cfg(test)] #[path = "../unit_tests/wallet_session.rs"] mod tests;