commit 9fccab186c11e9dbae435bc90a8f7c665645a81e
parent 725f8bcb3bca375de4483e09ee324aa24af37767
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:
3 files changed, 62 insertions(+), 1 deletion(-)
diff --git a/core/crates/application/src/session.rs b/core/crates/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/core/crates/storage/src/application_adapter.rs b/core/crates/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
diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md
@@ -507,7 +507,7 @@ handoff commit sequence.
- [x] 31. Implement select account command.
- [x] 32. Implement activate account with safe replacement ordering.
-- [ ] 33. Implement sign out command.
+- [x] 33. Implement sign out command.
- [ ] 34. Implement revision-bound removal request/confirmation flow.
- [ ] 35. Implement removal journal recovery.