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:
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,