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:
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);
+ }
}