diff --git a/crates/sprout-relay/src/config.rs b/crates/sprout-relay/src/config.rs index 9aa00aa7d..23b2cf4a7 100644 --- a/crates/sprout-relay/src/config.rs +++ b/crates/sprout-relay/src/config.rs @@ -39,7 +39,8 @@ pub struct Config { pub send_buffer_size: usize, /// Authentication provider configuration. pub auth: sprout_auth::AuthConfig, - /// Whether clients must authenticate via NIP-42 before sending events. + /// Whether REST API requests must present a valid token. Independent of + /// WebSocket protocol auth, which is *always* required by REQ/EVENT/COUNT. pub require_auth_token: bool, /// Comma-separated list of allowed CORS origins. /// If empty, permissive CORS is used (dev mode). @@ -197,8 +198,8 @@ impl Config { if !require_auth_token { warn!( - "SPROUT_REQUIRE_AUTH_TOKEN is false — relay accepts unauthenticated connections. \ - Set to true for production." + "SPROUT_REQUIRE_AUTH_TOKEN is false — REST API requests bypass token auth. \ + WebSocket protocol auth is unaffected. Set to true for production." ); } diff --git a/crates/sprout-relay/src/nip11.rs b/crates/sprout-relay/src/nip11.rs index 1a1694c22..d3cf6d73d 100644 --- a/crates/sprout-relay/src/nip11.rs +++ b/crates/sprout-relay/src/nip11.rs @@ -48,7 +48,8 @@ pub struct RelayLimitation { pub max_subid_length: Option, /// Minimum proof-of-work difficulty required for events. pub min_pow_difficulty: Option, - /// Whether NIP-42 authentication is required before sending events. + /// Whether NIP-42 authentication is required before subscribing or + /// publishing events. pub auth_required: bool, /// Whether payment is required to use the relay. pub payment_required: bool, @@ -56,12 +57,32 @@ pub struct RelayLimitation { pub restricted_writes: bool, } +/// Canonical `RelayLimitation` advertised by this relay. +/// +/// `auth_required` is always `true`: the REQ, EVENT, and COUNT handlers +/// unconditionally reject connections that are not in +/// `AuthState::Authenticated`. This is independent of the REST API token +/// toggle (`config.require_auth_token`). +fn relay_limitation() -> RelayLimitation { + RelayLimitation { + max_message_length: Some(MAX_FRAME_BYTES as u64), + max_subscriptions: Some(1024), + max_filters: Some(10), + max_limit: Some(10_000), + max_subid_length: Some(256), + min_pow_difficulty: None, + auth_required: true, + payment_required: false, + restricted_writes: true, + } +} + impl RelayInfo { - /// Builds a `RelayInfo` document from the relay's runtime config. + /// Builds the relay's NIP-11 information document. /// /// `relay_pubkey` is the relay's own signing pubkey (hex), advertised as the /// NIP-11 `self` field for NIP-43 membership verification. - pub fn from_config(config: &crate::config::Config, relay_pubkey: Option<&str>) -> Self { + pub fn build(relay_pubkey: Option<&str>) -> Self { Self { name: "Sprout Relay".to_string(), description: "Sprout — private team communication relay".to_string(), @@ -70,17 +91,7 @@ impl RelayInfo { supported_nips: SUPPORTED_NIPS.to_vec(), software: "https://github.com/sprout-rs/sprout".to_string(), version: env!("CARGO_PKG_VERSION").to_string(), - limitation: Some(RelayLimitation { - max_message_length: Some(MAX_FRAME_BYTES as u64), - max_subscriptions: Some(1024), - max_filters: Some(10), - max_limit: Some(10_000), - max_subid_length: Some(256), - min_pow_difficulty: None, - auth_required: config.require_auth_token, - payment_required: false, - restricted_writes: true, - }), + limitation: Some(relay_limitation()), relay_self: relay_pubkey.map(|s| s.to_string()), } } @@ -97,10 +108,7 @@ pub async fn relay_info_handler( } else { None }; - axum::response::Json(RelayInfo::from_config( - &state.config, - relay_pubkey.as_deref(), - )) + axum::response::Json(RelayInfo::build(relay_pubkey.as_deref())) } #[cfg(test)] @@ -129,6 +137,15 @@ mod tests { ); } + #[test] + fn auth_required_is_advertised_true() { + // REQ, EVENT, and COUNT all unconditionally require + // `AuthState::Authenticated` (see `crates/sprout-relay/src/handlers/`). + // The NIP-11 doc must reflect that or clients (e.g. the desktop pair + // flow) misroute unauthenticated peers. + assert!(relay_limitation().auth_required); + } + #[test] fn supported_nips_are_sorted() { let mut sorted = SUPPORTED_NIPS.to_vec(); diff --git a/crates/sprout-relay/src/router.rs b/crates/sprout-relay/src/router.rs index eed52be78..8cfdd3acb 100644 --- a/crates/sprout-relay/src/router.rs +++ b/crates/sprout-relay/src/router.rs @@ -160,7 +160,7 @@ async fn nip11_or_ws_handler( }; if accept.contains("application/nostr+json") { - let info = RelayInfo::from_config(&state.config, relay_pubkey.as_deref()); + let info = RelayInfo::build(relay_pubkey.as_deref()); return Json(info).into_response(); } @@ -179,7 +179,7 @@ async fn nip11_or_ws_handler( } } // Not a WS request and not asking for nostr+json — serve NIP-11 as fallback. - let info = RelayInfo::from_config(&state.config, relay_pubkey.as_deref()); + let info = RelayInfo::build(relay_pubkey.as_deref()); Json(info).into_response() } } diff --git a/crates/sprout-test-client/tests/e2e_relay.rs b/crates/sprout-test-client/tests/e2e_relay.rs index 3876f45f1..30f77fe21 100644 --- a/crates/sprout-test-client/tests/e2e_relay.rs +++ b/crates/sprout-test-client/tests/e2e_relay.rs @@ -605,12 +605,12 @@ async fn test_nip11_relay_info() { Some(1024), "limitation.max_subscriptions must be 1024" ); - assert!( - limitation - .get("auth_required") - .and_then(|v| v.as_bool()) - .is_some(), - "limitation.auth_required must be a boolean" + // The REQ, EVENT, and COUNT handlers unconditionally require an + // authenticated connection, so the NIP-11 doc must advertise that. + assert_eq!( + limitation.get("auth_required").and_then(|v| v.as_bool()), + Some(true), + "limitation.auth_required must be true — REQ/EVENT/COUNT require NIP-42 auth" ); }