289 lines
12 KiB
Rust
289 lines
12 KiB
Rust
// 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<String>,
|
|
/// Current session state.
|
|
pub(crate) state: WalletSessionStateDto,
|
|
/// Root-scoped identifier when a Wallet is selected.
|
|
pub(crate) wallet_id: std::option::Option<String>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
/// 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<WalletNoteDto>,
|
|
/// 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<String>,
|
|
/// Root-scoped destination filename.
|
|
pub(crate) filename: String,
|
|
/// Optional first protected note stored inside the Wallet.
|
|
pub(crate) initial_note: std::option::Option<String>,
|
|
/// 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<String>,
|
|
}
|
|
|
|
/// 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<ksp_wallet_lib::WalletView>,
|
|
},
|
|
/// 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<ksp_wallet_lib::WalletOwner>,
|
|
/// 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;
|