commit 4f922d5cea7240d6f1750297aca1b40891ba63f2
parent 1bce0fbf74b7bbd8e1c745a909ce025bc91e9163
Author: triesap <tyson@radroots.org>
Date: Sun, 2 Aug 2026 19:08:16 +0000
core(session): implement sign out semantics
- clear active account and profile session state
- retain saved accounts selected identity and credentials
- make repeated sign out idempotent without revision churn
- keep the resulting public snapshot free of secret material
Diffstat:
2 files changed, 61 insertions(+), 0 deletions(-)
diff --git a/crates/studio_application/src/session.rs b/crates/studio_application/src/session.rs
@@ -7,6 +7,18 @@ use crate::{
};
impl AppCore {
+ /// Drops the active session while retaining accounts, selection, and credentials.
+ ///
+ /// # Errors
+ ///
+ /// Returns a safe application-state error if the transition cannot be applied.
+ pub fn sign_out(&self) -> Result<AppSnapshot, SafeError> {
+ if matches!(self.snapshot().session(), crate::SessionState::SignedOut) {
+ return Ok(self.snapshot());
+ }
+ self.apply_transition(StateTransition::SignOut)
+ }
+
/// Validates and prepares a saved local account before replacing the active session.
///
/// # Errors
@@ -195,4 +207,44 @@ mod tests {
Some(first)
);
}
+
+ #[test]
+ fn sign_out_retains_saved_account_selection_and_credential() {
+ let core = AppCore::in_memory(RelayConfiguration::default());
+ let accounts = InMemoryAccountRepository::default();
+ let secrets = InMemorySecretStore::default();
+ let journal = InMemoryOperationJournal::default();
+ let profiles = EmptyProfiles;
+ core.bootstrap().expect("bootstrap");
+ let public_key = core
+ .import_secret_key(
+ input("7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7"),
+ &accounts,
+ &accounts,
+ &secrets,
+ &journal,
+ &FixedClock,
+ )
+ .expect("import")
+ .account()
+ .public_key();
+ core.activate_account(
+ public_key,
+ &accounts,
+ &accounts,
+ &profiles,
+ &secrets,
+ &FixedClock,
+ )
+ .expect("activate");
+
+ let signed_out = core.sign_out().expect("sign out");
+ let repeated = core.sign_out().expect("idempotent sign out");
+ assert_eq!(signed_out, repeated);
+ assert_eq!(signed_out.session(), SessionState::SignedOut);
+ assert!(signed_out.active_account().is_none());
+ assert_eq!(signed_out.accounts().len(), 1);
+ assert_eq!(signed_out.selected_account(), Some(public_key));
+ assert!(secrets.contains(public_key).expect("credential retained"));
+ }
}
diff --git a/crates/studio_storage/src/application_adapter.rs b/crates/studio_storage/src/application_adapter.rs
@@ -119,6 +119,15 @@ impl PersistentAppCore {
)
}
+ /// Signs out while retaining durable account data and credentials.
+ ///
+ /// # Errors
+ ///
+ /// Returns a safe application-state error if sign out cannot complete.
+ pub fn sign_out(&self) -> Result<AppSnapshot, SafeError> {
+ self.core.sign_out()
+ }
+
#[must_use]
pub const fn core(&self) -> &AppCore {
&self.core