app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
Mcore/crates/application/src/session.rs | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/storage/src/application_adapter.rs | 9+++++++++
Mdocs/implementation/nostr-runtime-rcld.md | 2+-
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.