Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions crates/sprout-relay/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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."
);
}

Expand Down
53 changes: 35 additions & 18 deletions crates/sprout-relay/src/nip11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,41 @@ pub struct RelayLimitation {
pub max_subid_length: Option<u32>,
/// Minimum proof-of-work difficulty required for events.
pub min_pow_difficulty: Option<u32>,
/// 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,
/// Whether writes are restricted to authorized pubkeys.
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(),
Expand All @@ -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()),
}
}
Expand All @@ -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)]
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions crates/sprout-relay/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -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()
}
}
Expand Down
12 changes: 6 additions & 6 deletions crates/sprout-test-client/tests/e2e_relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}

Expand Down
Loading