app

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

commit 305aa2e78fde7063976aa525ac86c54e18560afa
parent 6cc74b684653b02019a389be07316a6159a89b1a
Author: triesap <tyson@radroots.org>
Date:   Mon,  2 Feb 2026 19:29:51 +0000

app: add init setup status checks

- add init helper that returns setup status
- detect corrupt or missing keystore state
- flag eula mismatch as corruption
- expand init unit tests for outcomes

Diffstat:
Mapp/src/init.rs | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mapp/src/lib.rs | 1+
2 files changed, 131 insertions(+), 3 deletions(-)

diff --git a/app/src/init.rs b/app/src/init.rs @@ -30,6 +30,9 @@ use crate::{ RadrootsAppConfig, RadrootsAppConfigError, RadrootsAppKeyMapConfig, + RadrootsAppSetupStatus, + APP_EULA_HASH, + APP_EULA_VERSION, }; #[cfg(target_arch = "wasm32")] @@ -389,6 +392,39 @@ pub async fn app_init_needs_setup<T: RadrootsClientDatastore, K: RadrootsClientK } } +pub async fn app_init_setup_status<T: RadrootsClientDatastore, K: RadrootsClientKeystoreNostr>( + datastore: &T, + keystore: &K, + key_maps: &RadrootsAppKeyMapConfig, +) -> RadrootsAppInitResult<RadrootsAppSetupStatus> { + let has_state = app_datastore_has_state(datastore, key_maps).await?; + if !has_state { + return Ok(RadrootsAppSetupStatus::Required); + } + let state = match app_datastore_read_state(datastore, key_maps).await { + Ok(state) => state, + Err(RadrootsAppInitError::State(RadrootsAppStateError::Corrupt)) + | Err(RadrootsAppInitError::State(RadrootsAppStateError::UnsupportedVersion(_))) => { + return Ok(RadrootsAppSetupStatus::Corrupt); + } + Err(err) => return Err(err), + }; + if !app_state_is_initialized(&state) { + return Ok(RadrootsAppSetupStatus::Required); + } + if state.eula_version != APP_EULA_VERSION || state.eula_hash != APP_EULA_HASH { + return Ok(RadrootsAppSetupStatus::Corrupt); + } + match keystore.read(&state.active_key).await { + Ok(_) => Ok(RadrootsAppSetupStatus::Configured), + Err(RadrootsClientKeystoreError::MissingKey) + | Err(RadrootsClientKeystoreError::NostrNoResults) => { + Ok(RadrootsAppSetupStatus::Corrupt) + } + Err(err) => Err(RadrootsAppInitError::Keystore(err)), + } +} + pub async fn app_init_backends(config: RadrootsAppConfig) -> RadrootsAppInitResult<RadrootsAppBackends> { let _ = app_log_debug_emit("log.app.init.backends", "start", None); config.validate().map_err(RadrootsAppInitError::Config)?; @@ -422,6 +458,7 @@ mod tests { app_init_backends, app_init_assets, app_init_needs_setup, + app_init_setup_status, app_init_timing_context, app_init_progress_add, app_init_state_default, @@ -440,6 +477,9 @@ mod tests { RadrootsAppState, RadrootsAppStateError, RadrootsAppStateRecord, + RadrootsAppSetupStatus, + APP_EULA_HASH, + APP_EULA_VERSION, }; use radroots_app_core::datastore::{ RadrootsClientDatastore, @@ -813,6 +853,15 @@ mod tests { } } + fn ready_state() -> RadrootsAppState { + let mut state = RadrootsAppState::default(); + state.active_key = "pub".to_string(); + state.eula_date = "2025-01-01T00:00:00Z".to_string(); + state.eula_version = APP_EULA_VERSION.to_string(); + state.eula_hash = APP_EULA_HASH.to_string(); + state + } + #[test] fn app_init_needs_setup_when_state_missing() { let datastore = SetupDatastore { @@ -875,9 +924,7 @@ mod tests { #[test] fn app_init_needs_setup_is_false_when_ready() { - let mut state = RadrootsAppState::default(); - state.active_key = "pub".to_string(); - state.eula_date = "2025-01-01T00:00:00Z".to_string(); + let state = ready_state(); let datastore = SetupDatastore { state: Some(state), record: RefCell::new(None), @@ -894,4 +941,84 @@ mod tests { .expect("needs setup"); assert!(!needs_setup); } + + #[test] + fn app_init_setup_status_required_when_state_missing() { + let datastore = SetupDatastore { + state: None, + record: RefCell::new(None), + }; + let keystore = SetupKeystore { + read_result: Ok("secret".to_string()), + }; + let key_maps = app_key_maps_default(); + let status = futures::executor::block_on(app_init_setup_status( + &datastore, + &keystore, + &key_maps, + )) + .expect("setup status"); + assert_eq!(status, RadrootsAppSetupStatus::Required); + } + + #[test] + fn app_init_setup_status_corrupt_when_eula_mismatch() { + let mut state = ready_state(); + state.eula_version = "0.0.0".to_string(); + let datastore = SetupDatastore { + state: Some(state), + record: RefCell::new(None), + }; + let keystore = SetupKeystore { + read_result: Ok("secret".to_string()), + }; + let key_maps = app_key_maps_default(); + let status = futures::executor::block_on(app_init_setup_status( + &datastore, + &keystore, + &key_maps, + )) + .expect("setup status"); + assert_eq!(status, RadrootsAppSetupStatus::Corrupt); + } + + #[test] + fn app_init_setup_status_corrupt_when_keystore_missing() { + let state = ready_state(); + let datastore = SetupDatastore { + state: Some(state), + record: RefCell::new(None), + }; + let keystore = SetupKeystore { + read_result: Err(RadrootsClientKeystoreError::MissingKey), + }; + let key_maps = app_key_maps_default(); + let status = futures::executor::block_on(app_init_setup_status( + &datastore, + &keystore, + &key_maps, + )) + .expect("setup status"); + assert_eq!(status, RadrootsAppSetupStatus::Corrupt); + } + + #[test] + fn app_init_setup_status_configured_when_ready() { + let state = ready_state(); + let datastore = SetupDatastore { + state: Some(state), + record: RefCell::new(None), + }; + let keystore = SetupKeystore { + read_result: Ok("secret".to_string()), + }; + let key_maps = app_key_maps_default(); + let status = futures::executor::block_on(app_init_setup_status( + &datastore, + &keystore, + &key_maps, + )) + .expect("setup status"); + assert_eq!(status, RadrootsAppSetupStatus::Configured); + } } diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -190,6 +190,7 @@ pub use init::{ app_init_fetch_asset, app_init_has_completed, app_init_needs_setup, + app_init_setup_status, app_init_mark_completed, app_init_progress_add, app_init_reset,