lib

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

commit e467432b5237c4ef7accfed051b0b0161bc828fc
parent f87f676a6938ddc9aff63788b54ad6df26628f8d
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:
Acrates/studio_application/src/config.rs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/studio_application/src/lib.rs | 4++++
2 files changed, 109 insertions(+), 0 deletions(-)

diff --git a/crates/studio_application/src/config.rs b/crates/studio_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/crates/studio_application/src/lib.rs b/crates/studio_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,