app

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

commit af00441a500a28ef1cdd0e974e68f258a3f37303
parent a7a45d4df766f9c20dab6be86a5f4cae886a382f
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 19:16:04 +0000

core(config): add relay environment configuration

- parse the ordered comma-separated Rust relay setting
- trim and deduplicate valid WebSocket relay addresses
- restrict localhost fallback to development and tests
- fail packaged runtime safely when configuration is absent

Diffstat:
Acore/crates/application/src/config.rs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/application/src/lib.rs | 4++++
Mdocs/implementation/nostr-runtime-rcld.md | 2+-
Mdocs/runbooks/local-relay-development.md | 7+++++++
4 files changed, 117 insertions(+), 1 deletion(-)

diff --git a/core/crates/application/src/config.rs b/core/crates/application/src/config.rs @@ -0,0 +1,105 @@ +use radroots_studio_domain::{SafeError, SafeErrorCode, SafeMessage, normalize_relay_urls}; + +use crate::RelayConfiguration; + +pub const RELAY_ENVIRONMENT_VARIABLE: &str = "RADROOTS_NOSTR_RELAYS"; +const DEVELOPMENT_RELAY: &str = "ws://localhost:8080"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RelayRuntimeMode { + Development, + Packaged, +} + +/// Reads the process relay configuration once through the Rust-owned boundary. +/// +/// # Errors +/// +/// Returns a safe configuration error for missing Unicode or invalid relay data. +pub fn relay_configuration_from_environment( + mode: RelayRuntimeMode, +) -> Result<RelayConfiguration, SafeError> { + let value = match std::env::var(RELAY_ENVIRONMENT_VARIABLE) { + Ok(value) => Some(value), + Err(std::env::VarError::NotPresent) => None, + Err(std::env::VarError::NotUnicode(_)) => return Err(invalid_configuration()), + }; + relay_configuration_from_value(value.as_deref(), mode) +} + +/// Parses an injected comma-separated relay list without mutating process state. +/// +/// # Errors +/// +/// Returns a safe configuration error when an entry is invalid or packaged mode +/// has no configured relay. +pub fn relay_configuration_from_value( + value: Option<&str>, + mode: RelayRuntimeMode, +) -> Result<RelayConfiguration, SafeError> { + let configured = value.unwrap_or_default().trim(); + let source = if configured.is_empty() { + match mode { + RelayRuntimeMode::Development => DEVELOPMENT_RELAY, + RelayRuntimeMode::Packaged => return Err(invalid_configuration()), + } + } else { + configured + }; + let normalized = normalize_relay_urls(source.split(',').map(str::trim))?; + if normalized.is_empty() { + return Err(invalid_configuration()); + } + Ok(RelayConfiguration::new(normalized)) +} + +const fn invalid_configuration() -> SafeError { + SafeError::new( + SafeErrorCode::InvalidRelayConfiguration, + SafeMessage::new("The Nostr relay configuration is invalid."), + ) +} + +#[cfg(test)] +mod tests { + use radroots_studio_domain::SafeErrorCode; + + use super::{RelayRuntimeMode, relay_configuration_from_value}; + + #[test] + fn relay_config_uses_localhost_fallback_only_for_development() { + for value in [None, Some(""), Some(" ")] { + let development = relay_configuration_from_value(value, RelayRuntimeMode::Development) + .expect("development fallback"); + assert_eq!(development.relays()[0].as_str(), "ws://localhost:8080/"); + let packaged = relay_configuration_from_value(value, RelayRuntimeMode::Packaged) + .expect_err("packaged configuration required"); + assert_eq!(packaged.code(), SafeErrorCode::InvalidRelayConfiguration); + } + } + + #[test] + fn relay_config_trims_deduplicates_and_preserves_order() { + let configuration = relay_configuration_from_value( + Some(" wss://relay.one ,wss://relay.two,wss://relay.one/ "), + RelayRuntimeMode::Packaged, + ) + .expect("configuration"); + let relays = configuration + .relays() + .iter() + .map(radroots_studio_domain::RelayUrl::as_str) + .collect::<Vec<_>>(); + assert_eq!(relays, ["wss://relay.one/", "wss://relay.two/"]); + } + + #[test] + fn relay_config_rejects_any_invalid_comma_separated_entry() { + let error = relay_configuration_from_value( + Some("wss://relay.one,https://not-a-relay.test"), + RelayRuntimeMode::Packaged, + ) + .expect_err("invalid entry"); + assert_eq!(error.code(), SafeErrorCode::InvalidRelayConfiguration); + } +} diff --git a/core/crates/application/src/lib.rs b/core/crates/application/src/lib.rs @@ -2,6 +2,7 @@ pub mod accounts; pub mod app_core; +pub mod config; pub mod ports; pub mod recovery; pub mod secrets; @@ -14,6 +15,9 @@ pub use accounts::{ InMemoryOperationJournal, }; pub use app_core::{AppCore, AppObserver, ObserverHandle, RemovalConfirmationToken}; +pub use config::{ + RelayRuntimeMode, relay_configuration_from_environment, relay_configuration_from_value, +}; pub use ports::{ AccountNamespaceRepository, AccountOperationKind, AccountOperationPhase, AccountPreferenceKey, AccountRepository, AppStateRepository, BoxFuture, CachedProfile, Clock, NostrClient, diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -513,7 +513,7 @@ handoff commit sequence. ### RCLD-08 -- [ ] 36. Add relay configuration source and environment parser. +- [x] 36. Add relay configuration source and environment parser. - [ ] 37. Implement Nostr event verification and kind-0 parsing adapter. - [ ] 38. Add Nostr client port implementation and local relay fixture. - [ ] 39. Implement cache-first active profile refresh orchestration. diff --git a/docs/runbooks/local-relay-development.md b/docs/runbooks/local-relay-development.md @@ -1,5 +1,12 @@ # Local relay development +Rust reads the ordered comma-separated relay list from +`RADROOTS_NOSTR_RELAYS`, trims entries, validates the WebSocket policy, and +deduplicates while preserving first-seen order. Development and tests may use +`ws://localhost:8080` when the variable is absent or empty. Packaged builds +have no fallback and report `InvalidRelayConfiguration` until at least one +valid relay is configured. + ## Status Initial runbook contract. Exact commands will be added with the relay fixture.