lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 8165cd3ccb20798ac41e26219d720347a464117c
parent 7d6a2a51131babfcf658ebf201abccc03af1ba8e
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 19:05:04 +0000

core(accounts): persist generated and imported accounts

- route account commands through the SQLite-backed core adapter
- reuse the durable operation journal for cross-resource writes
- restore generated and imported accounts selected but signed out
- verify restart survival without secret material in database bytes

Diffstat:
Mcrates/studio_storage/src/application_adapter.rs | 106++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 102 insertions(+), 4 deletions(-)

diff --git a/crates/studio_storage/src/application_adapter.rs b/crates/studio_storage/src/application_adapter.rs @@ -1,7 +1,10 @@ use std::path::Path; -use radroots_studio_application::{AppCore, AppSnapshot, RelayConfiguration}; -use radroots_studio_domain::SafeError; +use radroots_studio_application::{ + AppCore, AppSnapshot, Clock, GenerateAccountReceipt, ImportAccountReceipt, RelayConfiguration, + SecretStore, +}; +use radroots_studio_domain::{SafeError, SecretKeyInput}; use crate::Database; @@ -45,6 +48,46 @@ impl PersistentAppCore { self.core.bootstrap_from(&self.database, &self.database) } + /// Generates and durably persists one selected, signed-out local account. + /// + /// # Errors + /// + /// Returns a safe credential, storage, key, or application-state error. + pub fn generate_account( + &self, + secrets: &(impl SecretStore + ?Sized), + clock: &(impl Clock + ?Sized), + ) -> Result<GenerateAccountReceipt, SafeError> { + self.core.generate_account( + &self.database, + &self.database, + secrets, + &self.database, + clock, + ) + } + + /// Imports and durably persists one selected, signed-out local account. + /// + /// # Errors + /// + /// Returns a safe credential, storage, key, or application-state error. + pub fn import_secret_key( + &self, + input: SecretKeyInput, + secrets: &(impl SecretStore + ?Sized), + clock: &(impl Clock + ?Sized), + ) -> Result<ImportAccountReceipt, SafeError> { + self.core.import_secret_key( + input, + &self.database, + &self.database, + secrets, + &self.database, + clock, + ) + } + #[must_use] pub const fn core(&self) -> &AppCore { &self.core @@ -61,11 +104,12 @@ mod tests { use std::fs; use radroots_studio_application::{ - AccountRepository, AppLifecycle, AppStateRepository, RelayConfiguration, SessionState, + AccountRepository, AppLifecycle, AppStateRepository, Clock, InMemorySecretStore, + RelayConfiguration, SecretStore, SessionState, }; use radroots_studio_domain::{ AccountCreatedAt, AccountSummary, KeyAvailability, Npub, PublicKey, SafeErrorCode, - SignerKind, UnixTimestamp, + SecretKeyInput, SignerKind, UnixTimestamp, }; use tempfile::tempdir; @@ -85,6 +129,14 @@ mod tests { ) } + struct FixedClock; + + impl Clock for FixedClock { + fn now(&self) -> UnixTimestamp { + UnixTimestamp::from_seconds(25).expect("time") + } + } + #[test] fn persistent_bootstrap_handles_fresh_and_existing_signed_out_state() { let directory = tempdir().expect("directory"); @@ -130,4 +182,50 @@ mod tests { b"not a sqlite database" ); } + + #[test] + fn persisted_generate_and_import_survive_restart_without_secret_bytes() { + let directory = tempdir().expect("directory"); + let path = directory.path().join("studio.sqlite3"); + let secrets = InMemorySecretStore::default(); + let selected; + { + let adapter = + PersistentAppCore::open(&path, RelayConfiguration::default()).expect("adapter"); + adapter.bootstrap().expect("bootstrap"); + let generated = adapter + .generate_account(&secrets, &FixedClock) + .expect("generate"); + assert!( + secrets + .contains(generated.account().public_key()) + .expect("generated credential") + ); + let imported = adapter + .import_secret_key( + SecretKeyInput::parse( + "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7" + .to_owned(), + ) + .expect("secret"), + &secrets, + &FixedClock, + ) + .expect("import"); + selected = imported.account().public_key(); + assert_eq!(adapter.core().snapshot().accounts().len(), 2); + } + + let bytes = fs::read(&path).expect("database bytes"); + assert!(!bytes.windows(5).any(|value| value == b"nsec1")); + assert!(!bytes.windows(64).any(|value| { + value == b"7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7" + })); + let reopened = + PersistentAppCore::open(&path, RelayConfiguration::default()).expect("reopen"); + let restored = reopened.bootstrap().expect("restore"); + assert_eq!(restored.accounts().len(), 2); + assert_eq!(restored.selected_account(), Some(selected)); + assert_eq!(restored.session(), SessionState::SignedOut); + } }